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:
- User creation trigger – A new user is created via
hook_user_insert() - Condition – Runs when the request override
mapOnInsertis true, or, when no override is set, whenfire_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 create a contact - Event processing –
UserContactMappingSubscribercreates 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 overridecreateIfMissingwhen set, otherwiseauto_create_user_contact_mapping. It never links the user to an existing person. - Mapping save – The service saves the mapping and returns the contact, or
NULLwhen no contact was created - 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
UserContactMappingInterfaceobject (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):
- 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_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, 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 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
UserContactMappingEventfor 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_mappingservice 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);
Related documentation
- User Integration index — overview and permissions
- Entity — mapping entity schema and CRUD
- Field mapping — field sync after mapping
- Contact entity — person contact fields