Field mapping: exposing contact fields on users
The CRM module provides a field mapping system that exposes CRM person contact fields directly on user entities through computed field wrappers. This allows users to view and edit their contact data through their user account interface. The feature is a separate concern from the entity and the event but relies on the user-contact mapping entity. It has its own plugin system for determining who can set mapped fields.
Key components
UserFieldMappingService
Service ID: crm.user_field_mapping. Handles field discovery and mapping operations.
Key methods:
getPersonContactFields()– Returns all field definitions for the person contact typegetMappedFields()– Returns configured field mappings from configgetFieldMapping(string $user_field_name)– Gets mapping config for a specific user fieldgetMappedContact(UserInterface $user)– Loads the contact entity mapped to a userisComputedField(string $field_name)– Checks if a contact field is computed (read-only)isSystemField(string $field_name)– Checks if a field is a system field (id,uuid,created,changed)
Computed field item lists
MappedContactFieldItemList (src/Field/MappedContactFieldItemList.php)
Computed field item list that reads and writes values from the mapped contact entity. Used for standard field types.
MappedEntityReferenceFieldItemList (src/Field/MappedEntityReferenceFieldItemList.php)
Extends MappedContactFieldItemList and implements EntityReferenceFieldItemListInterface. Used for entity reference fields (including primary_entity_reference fields for emails, addresses, telephones) so formatters and widgets that expect entity references work correctly.
Access control plugin system
Pluggable access control for mapped fields uses the PHP attribute #[UserFieldAccess]. Plugins are discovered via this attribute and the plugin type.
Plugin attribute: #[UserFieldAccess]
Existing plugins
| Plugin ID | Class | Description |
|---|---|---|
user_entity |
UserEntityFieldAccess |
Access based on user entity permissions. If you have operation permissions on a user entity, you also have them on the mapped contact fields. Treats mapped CRM fields like native user fields. |
contact_entity |
ContactEntityFieldAccess |
Access based on contact entity permissions. If you have operation permissions on the mapped contact entity, you also have them on the mapped user fields. Requires permission to view/edit the mapped contact. |
- user_entity –
src/Plugin/crm/UserFieldAccess/UserEntityFieldAccess.php. Label: "User Entity Access". Description: "Treats mapped fields like native user fields. If you can edit the user, you can edit these fields." - contact_entity –
src/Plugin/crm/UserFieldAccess/ContactEntityFieldAccess.php. Label: "Contact Entity Access". Description: "Requires permission to view/edit the mapped contact entity (or the specific contact field when a field name is provided)."
Creating custom access plugins
Extend UserFieldAccessBase, implement checkAccess(string $operation, AccountInterface $account, ?UserInterface $user = NULL, ?ContactInterface $contact = NULL, ?string $contact_field_name = NULL, array $mapping = []): AccessResultInterface, and use the attribute with id, label, and description:
<?php
declare(strict_types=1);
namespace Drupal\my_module\Plugin\crm\UserFieldAccess;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\crm\Attribute\UserFieldAccess;
use Drupal\crm\Entity\ContactInterface;
use Drupal\crm\Plugin\crm\UserFieldAccess\UserFieldAccessBase;
use Drupal\user\UserInterface;
/**
* Custom access control plugin example.
*/
#[UserFieldAccess(
id: 'my_custom_access',
label: new TranslatableMarkup('Custom Access Control'),
description: new TranslatableMarkup('Custom logic for field access.'),
)]
class MyCustomFieldAccess extends UserFieldAccessBase {
/**
* {@inheritdoc}
*/
public function checkAccess(
string $operation,
AccountInterface $account,
?UserInterface $user = NULL,
?ContactInterface $contact = NULL,
?string $contact_field_name = NULL,
array $mapping = [],
): AccessResultInterface {
return AccessResult::allowedIfHasPermission($account, 'my custom permission');
}
}
Configuration
Field mappings are stored in crm.user_contact_mapping.settings under the field_mappings key.
Schema structure:
field_mappings:
- contact_field_name: full_name # Source field on crm_contact
user_field_machine_name: contact_name # Target field name (prefixed with crm__)
user_field_label: 'Contact Name' # Label shown on user forms
form_display: true # Show on user edit/registration forms
view_display: true # Show on user account view
access_control_plugin: user_entity # Plugin ID for access control
enabled: true # Whether this mapping is active
Field name convention: Mapped fields on the user entity are prefixed with crm__ (e.g., crm__contact_name).
Form integration
User edit forms
For existing users with mapped contacts, the mapped fields render automatically using Drupal's Field UI. Changes are saved back to the contact entity via a form submit handler.
User registration forms
During registration, the contact does not exist yet. The flow is:
- Form alter adds CRM fields to the registration form
- Validation handler extracts field values and stores them in
$form_state - Submit handler (prepended to run before user save) stores values in
UserContactFieldValuesStorage - hook_user_insert runs when the user is saved. If
fire_event_user_insertis enabled or there are flushed registration field values, it callsUserContactMappingService::ensureContactForUser($user)(which dispatches the user contact mapping event so a contact is found or created) - The hook then applies the flushed field values to the contact and saves the contact
So field values are applied in the hook after the event, not on the event. When any mapped field is shown on the form, at least one of "Lookup contact" or "Create contact" must be enabled so that a contact can be ensured. See Event for the event and subscriber.
User edit (profile) forms
When a user saves their profile and the form includes CRM-mapped fields, the submit handler that writes values to the contact runs only if the user has a mapped contact. If the user has no contact but has submitted values for CRM fields, a preceding submit handler calls ensureContactForUser($user) so that a contact exists before the values are applied. The same find-or-create logic used on user insert is used on profile save when needed.
Display configuration
Mapped fields integrate with Drupal's Field UI for display configuration:
- Form display:
/admin/config/people/accounts/form-display - View display:
/admin/config/people/accounts/display
Computed fields (e.g. age) and system fields (id, uuid, created, changed) are automatically excluded from form display configuration because they cannot be edited.
When you save the field mapping settings form, the person contact bundle's current view and form display settings are applied once to the user entity (by display mode name). There is no ongoing sync: user displays are normal Drupal config afterwards and can be changed in Field UI without being overwritten. For each user display mode at save time:
- If the person contact bundle has a display with the same mode name, the contact field's display (or form display) component settings are copied to the corresponding user field on that user display; if the person has the field hidden on that display, the user field is hidden there too.
- If the person contact bundle has no display with that name (e.g. user has a "register" form mode but person does not), the mapped fields are hidden on that user display.
API usage
<?php
declare(strict_types=1);
$field_mapping_service = \Drupal::service('crm.user_field_mapping');
// Get all available person contact fields.
$contact_fields = $field_mapping_service->getPersonContactFields();
// Get configured field mappings.
$mappings = $field_mapping_service->getMappedFields();
// Get the contact for a user.
$user = \Drupal\user\Entity\User::load($user_id);
$contact = $field_mapping_service->getMappedContact($user);
// Check field types.
$is_computed = $field_mapping_service->isComputedField('age'); // TRUE
$is_system = $field_mapping_service->isSystemField('id'); // TRUE
// Access a mapped field value on a user.
$user = \Drupal\user\Entity\User::load($user_id);
if ($user->hasField('crm__contact_name')) {
$name_value = $user->get('crm__contact_name')->getValue();
}