Skip to content

User integration event and flow

The user integration event provides a uniform way to map new or existing users to CRM person contacts. It is currently fired when a user is created (via hook_user_insert); the same event can be used in other contexts in the future. This document covers configuration, the event system, the service API, and troubleshooting.

Configuration

User integration settings are in crm.user_contact_mapping.settings and can be configured at /admin/config/crm/user/settings (implemented by UserContactMappingSettingsForm in src/Form/UserContactMappingSettingsForm.php). Field mapping controls are configured separately at /admin/config/crm/user/field (implemented by UserContactFieldForm in src/Form/UserContactFieldForm.php).

Available settings:

display_name: false                      # Override user name with CRM contact name
fire_event_user_insert: false           # Fire user contact mapping event when users are created
auto_create_user_contact_mapping: false  # Automatically create contact mappings on user creation
  • Display Name Override – Enable/disable global display name override feature
  • Fire Event on User Insert – Enable the user integration event when users are created
  • Create Contact – Create a new person during the mapping process when that email is not already on a contact. A matching email never claims an existing person. Attach a user to an existing person from the mapping form.

Create-contact override

Users with override crm user contact mapping see a CRM contact mapping group on user_register_form (public registration and /admin/people/create) when Fire event on user insert is enabled. The Create contact checkbox defaults to auto_create_user_contact_mapping. Implemented by UserContactMappingFormHooks in src/Hook/UserContactMappingFormHooks.php.

  • Checked – Same create path as the site setting. A matching email never claims an existing person, and no mapping is saved in that case.
  • Unchecked – The user is created with no mapping and no new person, even when site Create contact is on.

The form stores the choice on the crm.user_contact_mapping_create_overrides service (UserContactMappingCreateOverridesStore in src/Service/UserContactMappingCreateOverridesStore.php). Each value is a UserContactMappingCreateOverrides (src/Service/UserContactMappingCreateOverrides.php) with mapOnInsert (copied from the site fire-event setting) and createIfMissing (the checkbox). hook_user_insert uses mapOnInsert when present instead of fire_event_user_insert. The subscriber uses createIfMissing when present instead of auto_create_user_contact_mapping. Programmatic user creation does not set overrides unless code calls the store.

User creation hook

When a user is created, the system can automatically create a corresponding contact mapping through the hook_user_insert() implementation in src/Hook/UserHooks.php.

Process flow:

  1. User creation trigger – A new user is created via hook_user_insert()
  2. Condition – Runs when the request override mapOnInsert is true, or, when no override is set, when fire_event_user_insert is enabled, or when there are flushed registration field values (so a contact is needed to store them)
  3. Ensure contact – The hook calls UserContactMappingService::ensureContactForUser($user), which creates an unsaved mapping, dispatches UserContactMappingEvent, and lets the subscriber create a contact
  4. Event processing – UserContactMappingSubscriber creates a new person contact when create-if-missing is enabled and the email is not already on a contact method. Create-if-missing is the request override createIfMissing when set, otherwise auto_create_user_contact_mapping. It never links the user to an existing person.
  5. Mapping save – The service saves the mapping and returns the contact, or NULL when no contact was created
  6. Apply field values – If a contact was created and there were registration field values, the hook applies them to that new contact and saves it (field values are not passed via the event; they are applied in the hook after the event). Values are not applied when no contact was created, so an existing person is left unchanged.
sequenceDiagram
    participant Hook as hook_user_insert
    participant Store as CreateOverrides
    participant Config as Configuration
    participant Dispatcher as EventDispatcher
    participant Subscriber as UserContactMappingSubscriber
    participant Storage as EntityStorage

    Hook->>Store: mapOnInsert when a request override exists
    alt No request override
        Hook->>Config: Check fire_event_user_insert
        Config-->>Hook: Enabled
    end
    Hook->>Dispatcher: Dispatch UserContactMappingEvent
    Dispatcher->>Subscriber: onUserContactMapping
    Subscriber->>Store: createIfMissing when a request override exists
    Subscriber->>Subscriber: Create contact when email is unused
    Subscriber->>Storage: Save mapping

Event system

UserContactMappingEvent

Located in src/Event/UserContactMappingEvent.php. Dispatched when establishing a user–contact mapping (e.g. from ensureContactForUser).

  • Event name: crm_user_contact_mapping_event
  • Event data: Contains only a UserContactMappingInterface object (the mapping entity). Subscribers may create a contact and set it on the mapping. Field values are not passed on the event; they are applied by the caller (e.g. the hook or the user form submit handler) after the mapping has a contact.

UserContactMappingSubscriber

The default event subscriber (src/EventSubscriber/UserContactMappingSubscriber.php) creates a contact when create-if-missing is enabled. That flag is the request override createIfMissing when one was stored for the user, otherwise auto_create_user_contact_mapping. It does not apply registration or form field values, and it does not claim an existing person by email. To attach a user to an existing person, create the mapping from the mapping form.

Contact creation process (when create-if-missing is enabled and the email is not already on a contact method):

  1. Create a new 'person' contact
  2. Create an email contact method with the user's email address
  3. Set the contact's full name to the user's display name
  4. Associate the email method with the contact and save the contact
  5. Set the contact on the mapping

Registration-time field values are applied in hook_user_insert after the event (see Field mapping).

Service: UserContactMappingService

The crm.user_contact_mapping service (src/Service/UserContactMappingService.php) provides programmatic access to user-contact mappings and is the preferred way to create or resolve mappings.

Key methods

  • getContactIdFromUserId(int $user_id) – Contact ID for a user, or NULL
  • getUserIdFromContactId(int $contact_id) – User ID for a contact, or NULL
  • getRelationIdFromUserId(int $user_id) – Mapping entity ID for a user, or NULL
  • getRelationIdFromContactId(int $contact_id) – Mapping entity ID for a contact, or NULL
  • map(UserInterface $user, ?Contact $contact = NULL) – Create a mapping; if no contact is provided, a new person contact is created (when configured). Throws if a mapping already exists.
  • ensureContactForUser(UserInterface $user) – Ensures the user has a mapped contact: if one already exists, returns it; otherwise creates an unsaved mapping, dispatches UserContactMappingEvent, saves the mapping, and returns the contact (or NULL if none was created). A matching email does not attach an existing person. Used by the user insert hook and by the user form submit handler when saving CRM field data.

API usage

<?php

declare(strict_types=1);

use Drupal\Core\Session\AccountInterface;
use Drupal\crm\Entity\Contact;

$sync_service = \Drupal::service('crm.user_contact_mapping');

// Get contact ID from user ID.
$contact_id = $sync_service->getContactIdFromUserId($user_id);

if ($contact_id === NULL) {
  \Drupal::logger('my_module')->info('No contact mapped to user @uid', ['@uid' => $user_id]);
}
else {
  \Drupal::logger('my_module')->info('User @uid is mapped to contact @cid', [
    '@uid' => $user_id,
    '@cid' => $contact_id,
  ]);
}

// Get user ID from contact ID.
$user_id = $sync_service->getUserIdFromContactId($contact_id);

if ($user_id === NULL) {
  \Drupal::logger('my_module')->info('No user mapped to contact @cid', ['@cid' => $contact_id]);
}

// Get mapping entity ID from user ID or contact ID.
$relation_id = $sync_service->getRelationIdFromUserId($user_id);
$relation_id = $sync_service->getRelationIdFromContactId($contact_id);

// Create a user-contact mapping (with existing contact or auto-create).
$user_account = \Drupal\user\Entity\User::load($user_id);

if ($user_account === NULL) {
  \Drupal::logger('my_module')->error('User @uid not found.', ['@uid' => $user_id]);
  return;
}

try {
  $contact = $sync_service->map($user_account, $contact_entity);
  \Drupal::logger('my_module')->info('Mapped user @uid to contact @cid', [
    '@uid' => $user_account->id(),
    '@cid' => $contact->id(),
  ]);
}
catch (\Exception $e) {
  \Drupal::logger('my_module')->error('Failed to create user-contact mapping: @message', [
    '@message' => $e->getMessage(),
  ]);
}

Manual relationship creation

Option 1: Map user to an existing contact

<?php

declare(strict_types=1);

use Drupal\crm\Entity\Contact;
use Drupal\user\Entity\User;

$sync_service = \Drupal::service('crm.user_contact_mapping');
$user = User::load($user_id);

if ($user === NULL) {
  return;
}

$existing_contact = Contact::load($contact_id);

if ($existing_contact === NULL) {
  return;
}

try {
  $contact = $sync_service->map($user, $existing_contact);
  \Drupal::logger('my_module')->info('Mapped user @uid to existing contact @cid', [
    '@uid' => $user->id(),
    '@cid' => $contact->id(),
  ]);
}
catch (\Exception $e) {
  \Drupal::logger('my_module')->error('Failed to map user to contact: @message', [
    '@message' => $e->getMessage(),
  ]);
}

Option 2: Map user with automatic contact creation

<?php

declare(strict_types=1);

use Drupal\user\Entity\User;

$sync_service = \Drupal::service('crm.user_contact_mapping');
$user = User::load($user_id);

if ($user === NULL) {
  return;
}

try {
  $contact = $sync_service->map($user);
  \Drupal::logger('my_module')->info('Created new contact @cid for user @uid', [
    '@uid' => $user->id(),
    '@cid' => $contact->id(),
  ]);
}
catch (\Exception $e) {
  \Drupal::logger('my_module')->error('Failed to create contact for user: @message', [
    '@message' => $e->getMessage(),
  ]);
}

Important considerations:

  • Each user can only be mapped to one contact (enforced by unique constraint)
  • Each contact can only be mapped to one user (enforced by unique constraint)
  • Only 'person' type contacts can be mapped to users
  • The map() method throws an exception if a mapping already exists

Custom event subscriber

You can subscribe to UserContactMappingEvent for custom mapping logic, notifications, or external system updates:

<?php

declare(strict_types=1);

namespace Drupal\my_module\EventSubscriber;

use Drupal\crm\Event\UserContactMappingEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Custom subscriber for user-contact mapping events.
 */
class CustomUserContactMappingSubscriber implements EventSubscriberInterface {

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    return [
      UserContactMappingEvent::EVENT_NAME => 'onUserContactMapping',
    ];
  }

  /**
   * Handle user contact mapping event.
   *
   * @param \Drupal\crm\Event\UserContactMappingEvent $event
   *   The event.
   */
  public function onUserContactMapping(UserContactMappingEvent $event): void {
    $mapping = $event->getUserContactMapping();

    \Drupal::logger('my_module')->info('User @uid mapped to contact @cid', [
      '@uid' => $mapping->get('user')->target_id,
      '@cid' => $mapping->get('crm_contact')->target_id,
    ]);
  }

}

Extensibility

The event-driven architecture allows you to:

  • Subscribe to UserContactMappingEvent for custom mapping logic
  • Override or extend the default contact creation behavior
  • Add additional data synchronization between users and contacts

Performance

  • Use the crm.user_contact_mapping service instead of direct entity queries for lookups.
  • Unique reference constraints and indexing on reference fields keep lookups efficient.

Inefficient:

$storage = \Drupal::entityTypeManager()->getStorage('crm_user_contact_mapping');
$mappings = $storage->loadByProperties(['user' => $user_id]);
$mapping = reset($mappings);
$contact_id = $mapping ? $mapping->get('crm_contact')->target_id : NULL;

Efficient:

$sync_service = \Drupal::service('crm.user_contact_mapping');
$contact_id = $sync_service->getContactIdFromUserId($user_id);