Skip to content

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() using hook_crm_contact_access_records() and hook_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 realm
  • grant_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.