Skip to content

User mapping event and flow

The user mapping 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 contact mapping 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).

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
lookup_contact: false                    # Try to find existing contacts by email before creating new ones
  • Display Name Override – Enable/disable global display name override feature
  • Fire Event on User Insert – Enable the user contact mapping event when users are created
  • Create Contact – Automatically create contact entities during the mapping process
  • Lookup Contact – Attempt to find existing contacts by email before creating new ones

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 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 find or create a contact
  4. Event processingUserContactMappingSubscriber looks up by email (if lookup_contact is enabled) and/or creates a new person contact (if auto_create_user_contact_mapping is enabled)
  5. Mapping save – The service saves the mapping and returns the contact
  6. Apply field values – If there were registration field values, the hook applies them to the contact and saves the contact (field values are not passed via the event; they are applied in the hook after the event)
sequenceDiagram
    participant Hook as hook_user_insert
    participant Config as Configuration
    participant Dispatcher as EventDispatcher
    participant Subscriber as UserContactMappingSubscriber
    participant Storage as EntityStorage

    Hook->>Config: Check fire_event_user_insert
    Config-->>Hook: Enabled
    Hook->>Dispatcher: Dispatch UserContactMappingEvent
    Dispatcher->>Subscriber: onUserContactMapping
    Subscriber->>Subscriber: Lookup or create contact
    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 find or 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) handles contact resolution only (lookup and create). It does not apply registration or form field values.

Contact lookup process (when lookup_contact is enabled):

  1. Search for existing contact methods with matching email address
  2. Find contacts associated with those contact methods
  3. If exactly one matching 'person' contact is found, set it on the mapping

Contact creation process (when no existing contact is found and auto_create_user_contact_mapping is enabled):

  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 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 found or created). 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');

// 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');
$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');
$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 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_id = $sync_service->getContactIdFromUserId($user_id);