UserContactMapping entity
The crm_user_contact_mapping entity type stores the one-to-one relationship between a Drupal user account and a CRM person contact. This document covers the entity structure, permissions, administrative interface, direct operations, and security.
For creating mappings programmatically (including automatic contact creation and email lookup), use the UserContactMappingService and event system instead of direct entity operations.
Entity structure
Database schema (ERD)
erDiagram
user {
int uid PK
}
crm_user_contact_mapping {
int id PK
int user FK
int crm_contact FK
}
crm_contact {
int id PK
}
user ||--o| crm_user_contact_mapping : "mapped to"
crm_contact ||--o| crm_user_contact_mapping : "mapped from"
Class architecture
classDiagram
class crm_user_contact_mapping {
int id PK
int user FK
int crm_contact FK
string uuid
int created
int changed
}
UserContactMapping entity (crm_user_contact_mapping)
The UserContactMapping entity is defined in src/Entity/UserContactMapping.php and implements UserContactMappingInterface.
Key fields:
user– Entity reference to a Drupal user (required, unique)crm_contact– Entity reference to a CRM contact of type 'person' (required, unique)created– Timestamp when the mapping was createdchanged– Timestamp when the mapping was last modified
Unique constraints:
Both the user and crm_contact fields have UniqueReference constraints, ensuring:
- Each user can only be mapped to one contact
- Each contact can only be mapped to one user
Permissions
Permission types
Administrative permissions
administer crm– Full administrative access to CRM functionality, including user contact mappings
Contact access permissions
view mapped crm contact– View contact records that are mapped to the current useredit mapped crm contact– Edit contact records that are mapped to the current user
Contact creation permissions
create any crm contact– Create new contact records
Display name permissions
alter crm user display name– Allow users to customize their display name format using their associated contact information
Permission implementation
The mapping system integrates with Drupal's access control through:
- Entity access control handlers
- Relationship-based permissions (mapped vs. any)
- Integration with the broader CRM permission system
Administrative interface
User contact mappings can be managed at:
- List all mappings:
/admin/config/crm/user/list - Add new mapping:
/admin/config/crm/user/add - Edit mapping:
/admin/config/crm/user/{crm_user_contact_mapping}/edit - Delete mapping:
/admin/config/crm/user/{crm_user_contact_mapping}/delete
Configuration (display name, event firing, auto-create, lookup) is documented in Event.
Direct entity operations
You can create and load mappings via the entity storage API. Prefer the UserContactMappingService when you need automatic contact creation, email lookup, or event dispatching.
<?php
declare(strict_types=1);
use Drupal\crm\Entity\UserContactMapping;
// Create a new user contact mapping directly.
$user_contact_mapping = UserContactMapping::create([
'user' => $user_id,
'crm_contact' => $contact_id,
]);
try {
$user_contact_mapping->save();
\Drupal::logger('my_module')->info('Created user contact mapping with ID @id', [
'@id' => $user_contact_mapping->id(),
]);
}
catch (\Exception $e) {
// This will fail if the user or contact is already mapped.
\Drupal::logger('my_module')->error('Failed to create mapping: @message', [
'@message' => $e->getMessage(),
]);
}
// Load existing mapping by user ID.
$storage = \Drupal::entityTypeManager()->getStorage('crm_user_contact_mapping');
$mappings = $storage->loadByProperties(['user' => $user_id]);
if (empty($mappings)) {
\Drupal::logger('my_module')->info('No mapping found for user @uid', ['@uid' => $user_id]);
}
else {
$mapping = reset($mappings);
$contact_id = $mapping->get('crm_contact')->target_id;
\Drupal::logger('my_module')->info('User @uid is mapped to contact @cid', [
'@uid' => $user_id,
'@cid' => $contact_id,
]);
}
// Load existing mapping by contact ID.
$mappings = $storage->loadByProperties(['crm_contact' => $contact_id]);
if (empty($mappings)) {
\Drupal::logger('my_module')->info('No mapping found for contact @cid', ['@cid' => $contact_id]);
}
else {
$mapping = reset($mappings);
$user_id = $mapping->get('user')->target_id;
\Drupal::logger('my_module')->info('Contact @cid is mapped to user @uid', [
'@cid' => $contact_id,
'@uid' => $user_id,
]);
}
Best practice: Use the crm.user service for programmatic mapping; see Event.
Error handling and validation
Unique reference constraints
Both user and contact fields have unique reference constraints that prevent:
- Multiple mappings for the same user
- Multiple mappings for the same contact
Access control
All entity queries include proper access checking so users can only access mappings they have permission to view or modify.
Logging
The system logs mapping creation events for audit purposes:
User @user @uid has been synchronized to the contact @contact_id, relation @rid has been created.
Security considerations
Unique constraint enforcement
The system enforces one-to-one relationships to prevent data integrity issues:
<?php
declare(strict_types=1);
use Drupal\crm\Entity\UserContactMapping;
// Attempting to create a duplicate mapping will fail.
$mapping1 = UserContactMapping::create([
'user' => $user_id,
'crm_contact' => $contact_id_1,
]);
$mapping1->save(); // Success
// This will fail due to unique constraint on user field.
$mapping2 = UserContactMapping::create([
'user' => $user_id, // Same user!
'crm_contact' => $contact_id_2,
]);
try {
$mapping2->save();
}
catch (\Exception $e) {
// Constraint violation exception will be thrown.
\Drupal::logger('my_module')->error('Duplicate mapping error: @message', [
'@message' => $e->getMessage(),
]);
}
Permission checks
Always verify permissions when working with user contact mappings:
<?php
declare(strict_types=1);
use Drupal\Core\Session\AccountInterface;
/**
* Check if a user can access their mapped contact.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The user account.
* @param string $operation
* The operation (view, edit).
*
* @return bool
* TRUE if access is granted.
*/
function check_mapped_contact_access(AccountInterface $account, string $operation): bool {
$permission = $operation === 'view' ? 'view mapped crm contact' : 'edit mapped crm contact';
if (!$account->hasPermission($permission)) {
return FALSE;
}
$sync_service = \Drupal::service('crm.user');
$contact_id = $sync_service->getContactIdFromUserId($account->id());
if ($contact_id === NULL) {
return FALSE;
}
$contact = \Drupal\crm\Entity\Contact::load($contact_id);
if ($contact === NULL) {
return FALSE;
}
return $contact->access($operation, $account);
}
Protecting sensitive data
When displaying user-contact information, ensure proper access control:
<?php
declare(strict_types=1);
use Drupal\Core\Access\AccessResult;
/**
* Access callback for user contact information routes.
*/
function user_contact_access(\Drupal\user\UserInterface $user, \Drupal\Core\Session\AccountInterface $account): AccessResult {
if ($user->id() === $account->id() && $account->hasPermission('view mapped crm contact')) {
return AccessResult::allowed()
->cachePerUser()
->addCacheableDependency($user);
}
if ($account->hasPermission('administer crm')) {
return AccessResult::allowed()
->cachePerPermissions();
}
return AccessResult::forbidden()
->cachePerPermissions()
->cachePerUser();
}
Troubleshooting
Issue: Cannot delete mapping
Problem: Attempting to delete a user-contact mapping fails.
Solution: Ensure proper permissions and handle dependencies:
<?php
declare(strict_types=1);
use Drupal\crm\Entity\UserContactMapping;
/**
* Safely delete a user-contact mapping.
*
* @param int $mapping_id
* The mapping entity ID.
*
* @return bool
* TRUE if successful, FALSE otherwise.
*/
function safe_delete_mapping(int $mapping_id): bool {
$mapping = UserContactMapping::load($mapping_id);
if ($mapping === NULL) {
\Drupal::logger('my_module')->warning('Mapping @id not found.', ['@id' => $mapping_id]);
return FALSE;
}
$current_user = \Drupal::currentUser();
if (!$mapping->access('delete', $current_user)) {
\Drupal::logger('my_module')->error('User @uid lacks permission to delete mapping @id', [
'@uid' => $current_user->id(),
'@id' => $mapping_id,
]);
return FALSE;
}
try {
$mapping->delete();
\Drupal::logger('my_module')->info('Deleted mapping @id', ['@id' => $mapping_id]);
return TRUE;
}
catch (\Exception $e) {
\Drupal::logger('my_module')->error('Failed to delete mapping: @message', [
'@message' => $e->getMessage(),
]);
return FALSE;
}
}