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:
- User creation trigger – A new user is created via
hook_user_insert() - Condition – Runs when
fire_event_user_insertis enabled or when there are flushed registration field values (so a contact is needed to store them) - Ensure contact – The hook calls
UserContactMappingService::ensureContactForUser($user), which creates an unsaved mapping, dispatchesUserContactMappingEvent, and lets the subscriber find or create a contact - Event processing –
UserContactMappingSubscriberlooks up by email (iflookup_contactis enabled) and/or creates a new person contact (ifauto_create_user_contact_mappingis enabled) - Mapping save – The service saves the mapping and returns the contact
- 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
UserContactMappingInterfaceobject (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):
- Search for existing contact methods with matching email address
- Find contacts associated with those contact methods
- 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):
- Create a new 'person' contact
- Create an email contact method with the user's email address
- Set the contact's full name to the user's display name
- Associate the email method with the contact and save the contact
- 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, orNULLgetUserIdFromContactId(int $contact_id)– User ID for a contact, orNULLgetRelationIdFromUserId(int $user_id)– Mapping entity ID for a user, orNULLgetRelationIdFromContactId(int $contact_id)– Mapping entity ID for a contact, orNULLmap(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, dispatchesUserContactMappingEvent, saves the mapping, and returns the contact (orNULLif 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
UserContactMappingEventfor custom mapping logic - Override or extend the default contact creation behavior
- Add additional data synchronization between users and contacts
Performance
- Use the
crm.userservice 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);