Add support for Drupal to CiviCRM batch syncing, removal of groups etc.

Created on 30 January 2023, over 1 year ago
Updated 22 April 2023, about 1 year ago

Problem/Motivation

Hiya all/@jitendrapurohit

As I had a client that needed to sync Drupal roles to CiviCRM either in batch mode or on a per-role-change scenario, I have rewritten some parts of the code to achieve this into this module.

What it does:

  1. It adds a new button on the `admin/config/civicrm/civicrm-group-roles/manual-sync` page, called `Synchronize Drupal Roles to CiviCRM groups now (one-direction)`
  2. Renamed the existing button from `Synchronize CiviCRM groups to Drupal Roles now` to `Synchronize CiviCRM groups to Drupal Roles now (one-direction)`
  3. on `src/Batch/Sync.php` I've reworked the message interface as it was not producing any output (similar to the my patch on issue https://www.drupal.org/project/civicrm_group_roles/issues/3294804#commen... 💬 Unable to remove association rule Fixed )
  4. Altered the function `getRulesGroups`

Proposed resolution

The reason for those 2 changes on points 1 & 2 above is mainly because the first button was doing a one-way synchronization from CiviCRM Groups to Drupal Roles (which at the beginning was not clear to me), while the opposite function was missing (Drupal roles syncing to CiviCRM Groups)

To cover the case of a drupal user being removed from a role (thus also removing him/her from the associated CiviCRM groups), I had to do the following changes:

  • on `getRulesGroups` function, we now bring back all defined groups in the rules with a status message as a value, 0 being to be removed (if exists) or 1 to be added (if not there). That will help us to check if the CiviCRM group exists and needs to be removed from the contact in later stage of the syncing.
  • I've rewritting (and renamed as it now fits better) the function `addGroupsOnCreate` into `updateGroupsOnContact` as it will now cover the cases of a Drupal user being removed from his/her role. I needed to also remove the associated group once this was done.
  • As you were already covering the synchronization process for a new contact creation, I've added a trigger to perform a Drupal to Civi sync on contact *update* as well. In that hook, if we detect that there's a user role change, fire up the synchronization from Drupal to CiviCRM. I did not touch the `user_login` process as I find it correct to issue an update from CiviCRM to Drupal this time.

Code was written for the 1.2.0/1.2.x on Drupal 9.4.8

Let me know what you think

✨ Feature request
Status

Active

Version

1.2

Component

Code

Created by

🇬🇧United Kingdom VangelisP Fife, Scotland

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @VangelisP
  • 🇮🇳India jitendrapurohit

    Hi @VangelisP

    After applying the above patch, I get the following error on the Rule page -

    Deprecated function: Optional parameter $params declared before required parameter $context is implicitly treated as a required parameter in include() (line 571 of /Users/jitendra/www/d9unit/vendor/composer/ClassLoader.php).include('/Users/jitendra/www/d9unit/web/core/includes/bootstrap.inc') (Line: 571)
    Composer\Autoload\includeFile('/Users/jitendra/www/d9unit/web/modules/contrib/civicrm_group_roles/src/Batch/Sync.php') (Line: 428)
    Composer\Autoload\ClassLoader->loadClass('Drupal\civicrm_group_roles\Batch\Sync') (Line: 259)
    Drupal\Component\DependencyInjection\Container->createService(Array, 'civicrm_group_roles.batch.sync') (Line: 177)
    Drupal\Component\DependencyInjection\Container->get('civicrm_group_roles.batch.sync') (Line: 36)
    Drupal\civicrm_group_roles\Form\ManualSyncForm::create(Object) (Line: 28)
    Drupal\Core\DependencyInjection\ClassResolver->getInstanceFromDefinition('\Drupal\civicrm_group_roles\Form\ManualSyncForm') (Line: 48)
    Drupal\Core\Controller\HtmlFormController->getFormObject(Object, '\Drupal\civicrm_group_roles\Form\ManualSyncForm') (Line: 58)
    Drupal\Core\Controller\FormController->getContentResult(Object, Object)
    call_user_func_array(Array, Array) (Line: 123)
    Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 580)
    Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 124)
    Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array) (Line: 97)
    Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 169)
    Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
    Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
    Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
    Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 106)
    Drupal\page_cache\StackMiddleware\PageCache->pass(Object, 1, 1) (Line: 85)
    Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
    Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
    Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
    Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 718)
    Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
    

    which seems to be due to this part of the patch -

    /**
        * Batch API process callback.
        *
    +   * @param array $params
        * @param mixed $context
        *   Batch API context data.
        */
    -  public function process(&$context) {
    +  public function process($params = [], &$context) {
    
    

    Changing it to public function process($params, &$context) { fixes the issue.

    Also, there are fatal errors while adding a drupal role on a user after applying this patch. Can you fix that please? To replicate:

-

    - Apply the patch.
    - Create a rule to sync RoleA => GroupA.
    - Assign RoleA to UserA => GroupA is assigned to the contact.
    - Create another Rule to sync RoleB to GroupB.
    - Edit the user and select RoleB on the form and save.
    - Error.

    From the db error: it looks like it is retrying to create an entry in civicrm_group_contact for GroupA. Here's the complete error with backtrace.

    The website encountered an unexpected error. Please try again later.
    
    Drupal\Core\Entity\EntityStorageException: DB Error: already exists in Drupal\Core\Entity\Sql\SqlContentEntityStorage->save() (line 815 of core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php).
    PEAR_Error->__construct('DB Error: already exists', -5, 16, Array, 'INSERT INTO `civicrm_group_contact` (`group_id` , `contact_id` , `status` ) VALUES ( 100 ,  23 , 'Added' )  [nativecode=1062 ** Duplicate entry '23-100' for key 'UI_contact_group']') (Line: 997)
    DB_Error->__construct(-5, 16, Array, 'INSERT INTO `civicrm_group_contact` (`group_id` , `contact_id` , `status` ) VALUES ( 100 ,  23 , 'Added' )  [nativecode=1062 ** Duplicate entry '23-100' for key 'UI_contact_group']') (Line: 575)
    PEAR::_raiseError(Object, NULL, -5, 16, Array, 'INSERT INTO `civicrm_group_contact` (`group_id` , `contact_id` , `status` ) VALUES ( 100 ,  23 , 'Added' )  [nativecode=1062 ** Duplicate entry '23-100' for key 'UI_contact_group']', 'DB_Error', 1) (Line: 223)
    PEAR->__call('raiseError', Array) (Line: 1928)
    DB_common->raiseError(-5, NULL, NULL, 'INSERT INTO `civicrm_group_contact` (`group_id` , `contact_id` , `status` ) VALUES ( 100 ,  23 , 'Added' )  [nativecode=1062 ** Duplicate entry '23-100' for key 'UI_contact_group']', '1062 ** Duplicate entry '23-100' for key 'UI_contact_group'') (Line: 943)
    DB_mysqli->mysqliRaiseError() (Line: 413)
    DB_mysqli->simpleQuery('INSERT INTO `civicrm_group_contact` (`group_id` , `contact_id` , `status` ) VALUES ( 100 ,  23 , 'Added' ) ') (Line: 1234)
    DB_common->query('INSERT INTO `civicrm_group_contact` (`group_id` , `contact_id` , `status` ) VALUES ( 100 ,  23 , 'Added' ) ') (Line: 2696)
    DB_DataObject->_query('INSERT INTO `civicrm_group_contact` (`group_id` , `contact_id` , `status` ) VALUES ( 100 ,  23 , 'Added' ) ') (Line: 1245)
    DB_DataObject->insert() (Line: 683)
    CRM_Core_DAO->save() (Line: 971)
    CRM_Core_DAO::writeRecord(Array) (Line: 992)
    CRM_Core_DAO::writeRecords(Array) (Line: 47)
    Civi\Api4\Action\GroupContact\Create->write(Array) (Line: 134)
    Civi\Api4\Generic\DAOCreateAction->writeObjects(Array) (Line: 35)
    Civi\Api4\Generic\DAOCreateAction->_run(Object) (Line: 72)
    Civi\Api4\Provider\ActionObjectProvider->invoke(Object) (Line: 158)
    Civi\API\Kernel->runRequest(Object) (Line: 250)
    Civi\Api4\Generic\AbstractAction->execute() (Line: 85)
    civicrm_api4('GroupContact', 'create', Array) (Line: 423)
    Drupal\civicrm_group_roles\CivicrmGroupRoles->updateGroupsOnContact(Object, Array) (Line: 55)
    civicrm_group_roles_user_update(Object)
    call_user_func_array(Object, Array) (Line: 426)
    Drupal\Core\Extension\ModuleHandler->Drupal\Core\Extension\{closure}(Object, 'civicrm_group_roles') (Line: 405)
    Drupal\Core\Extension\ModuleHandler->invokeAllWith('user_update', Object) (Line: 433)
    Drupal\Core\Extension\ModuleHandler->invokeAll('user_update', Array) (Line: 249)
    Drupal\Core\Entity\EntityStorageBase->invokeHook('update', Object) (Line: 903)
    Drupal\Core\Entity\ContentEntityStorageBase->invokeHook('update', Object) (Line: 598)
    Drupal\Core\Entity\EntityStorageBase->doPostSave(Object, 1) (Line: 784)
    Drupal\Core\Entity\ContentEntityStorageBase->doPostSave(Object, 1) (Line: 523)
    Drupal\Core\Entity\EntityStorageBase->save(Object) (Line: 804)
    Drupal\Core\Entity\Sql\SqlContentEntityStorage->save(Object) (Line: 339)
    Drupal\Core\Entity\EntityBase->save() (Line: 46)
    Drupal\user\ProfileForm->save(Array, Object)
    call_user_func_array(Array, Array) (Line: 114)
    Drupal\Core\Form\FormSubmitter->executeSubmitHandlers(Array, Object) (Line: 52)
    Drupal\Core\Form\FormSubmitter->doSubmitForm(Array, Object) (Line: 595)
    Drupal\Core\Form\FormBuilder->processForm('user_form', Array, Object) (Line: 323)
    Drupal\Core\Form\FormBuilder->buildForm(Object, Object) (Line: 73)
    Drupal\Core\Controller\FormController->getContentResult(Object, Object)
    call_user_func_array(Array, Array) (Line: 123)
    Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 580)
    Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 124)
    Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array) (Line: 97)
    Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 169)
    Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
    Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
    Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
    Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 106)
    Drupal\page_cache\StackMiddleware\PageCache->pass(Object, 1, 1) (Line: 85)
    Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48)
    Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
    Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
    Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 718)
    Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
    
Production build 0.69.0 2024