Hooks
CRM invokes the following hooks so other modules can alter field mapping and
extend per-contact access. Canonical signatures and @see links live in
crm.api.php. Implement them in src/Hook/ with #[Hook].
TL;DR
- Omit contact fields from field mapping with
hook_crm_contact_field_mapping_exclusions_alter(). - Grant per-contact access like
hook_node_access_records()/hook_node_grants()usinghook_crm_contact_access_records()andhook_crm_contact_grants(). - After adding a records implementation, rebuild
crm_contact_access.
Field mapping exclusions
Use hook_crm_contact_field_mapping_exclusions_alter() when a module owns
contact fields that must not appear in field mapping, or when a field
type must not appear on the mapped entity form or view display.
The alter receives four lists:
| Key | Effect |
|---|---|
field_names |
Contact field names omitted from the mapping table |
field_types |
Field type plugin IDs omitted from the mapping table |
form_disallowed_types |
Types whose form-display checkbox is disabled |
view_disallowed_types |
Types whose view-display checkbox is disabled |
CRM seeds the lists with internal entity fields (type, langcode,
revision metadata, and similar), empty field_types, form-blocked
comment and crm_relationship_statistics, and view-blocked comment.
Comment and relationship-statistics fields stay in the mapping table
with form/view checkboxes disabled unless a module also adds those types
to field_types. Computed fields and system fields (id, uuid,
created, changed) stay out of form display independently of this
hook.
Omitting a field from the table does not delete an existing mapping. The next form save keeps that row. To remove a mapping, stop excluding the field (or edit config) and disable or clear the row on the form.
function hook_crm_contact_field_mapping_exclusions_alter(array &$exclusions): void {
$exclusions['field_names'][] = 'my_internal_field';
$exclusions['field_types'][] = 'my_field_type';
$exclusions['form_disallowed_types'][] = 'datetime';
$exclusions['view_disallowed_types'][] = 'datetime';
}
See Field mapping.
Contact access grants
CRM stores per-contact grants in the crm_contact_access table, mirroring
node access. Implement both records and grants together.
hook_crm_contact_access_records() runs when a contact is saved. Return
rows that will be written for that contact. Each record needs:
realm(string) — unique namespace (often your module name)gid(int) — grant ID within the realmgrant_label,grant_view,grant_update,grant_delete(0 or 1)
Only return records that grant at least one permission. All-zero rows are discarded.
hook_crm_contact_access_records_alter() can filter or rewrite the
combined records from every implementation.
hook_crm_contact_grants() runs at access-check time. Return
realm => int[] of grant IDs the account holds for the operation
(view label, view, update, or delete). For view label, a
matching row with either grant_label = 1 or grant_view = 1 is
enough.
CRM's own ContactAccessHooks (src/Hook/ContactAccessHooks.php)
uses realm crm_user_contact_mapping with gid equal to the mapped
person contact ID. See User mapping entity
for the mapping's grant_* fields.
use Drupal\Core\Session\AccountInterface;
use Drupal\crm\Entity\ContactInterface;
function hook_crm_contact_access_records(ContactInterface $contact): array {
return [
[
'realm' => 'my_realm',
'gid' => 42,
'grant_label' => 1,
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
],
];
}
function hook_crm_contact_grants(AccountInterface $account, string $operation): array {
if ($account->isAuthenticated()) {
return ['my_realm' => [42]];
}
return [];
}
See Contact and Permissions.
Rebuilding grant rows
Saving a contact or user-contact mapping refreshes that contact's rows.
Existing contacts are not rewritten when a module first implements
hook_crm_contact_access_records(). Rebuild from a post-update hook or
Drush:
\Drupal::service('crm.contact_access_grant_storage')->rebuild();
rebuild() truncates crm_contact_access and re-invokes the records
hook for every contact. On large sites, batch contacts and call
acquireGrants() plus write() instead.