Skip to content

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 created
  • changed – 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 user
  • edit 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;
  }
}