Skip to content

CRM Contact Detail Entity

Overview

The crm_contact_detail entity represents additional information attached to CRM contacts, such as email addresses, telephone numbers, and postal addresses. This is a flexible entity system that allows contacts to have multiple instances of different types of contact details.

Key Features

  • Revisionable: Full revision tracking with timestamps and user information
  • Bundle-based: Three main bundle types - address, email, and telephone
  • Type Classification: Each detail can be classified with types like "home", "work", "mobile", etc.
  • Status Management: Each detail can be enabled/disabled
  • Access Control: Inherits access permissions from parent contact

Entity Relationships

Contact details belong to CRM contacts and are classified by both bundle type and detail type, providing a flexible classification system.

Entity Structure

classDiagram
  class crm_contact_detail {
    +int id [PK]
    +uuid uuid
    +int revision_id
    +string bundle --> crm_contact_detail_type.id
    +int type --> crm_detail_type.id
    +bool status
    +int crm_contact --> crm_contact.id
    +int created [timestamp]
    +int changed [timestamp]
    +address_field address [bundle: address]
    +email_field email [bundle: email]
    +telephone_field telephone [bundle: telephone]
  }

  class crm_contact_detail_revision {
    +int revision_id [PK]
    +uuid uuid
    +int id
    +string bundle
    +int type --> crm_detail_type.id
    +bool status
    +int created [timestamp]
    +int changed [timestamp]
    +int revision_uid --> users.uid
    +int revision_timestamp [timestamp]
    +string revision_log
  }

  class crm_contact_detail_type {
    +string id [PK]
    +string label
    +string description
    +bool locked
  }

  class crm_detail_type {
    +string id [PK]
    +string label
    +string description
    +array bundles
    +bool negate
  }

  class crm_contact {
    +int id [PK]
    +string type
    +string name
  }

  %% Relationships
  crm_contact_detail ||--o{ crm_contact_detail_revision : "has revisions"
  crm_contact_detail }o--|| crm_contact_detail_type : "bundle type"
  crm_contact_detail }o--|| crm_detail_type : "classified by"
  crm_contact_detail }o--|| crm_contact : "belongs to"

  %% Bundle-specific classes
  class AddressContactDetail {
    +address_field address
    +label() string
  }

  class EmailContactDetail {
    +email_field email
    +label() string
  }

  class TelephoneContactDetail {
    +telephone_field telephone
    +label() string
  }

  %% Bundle inheritance
  AddressContactDetail --|> crm_contact_detail : "bundle: address"
  EmailContactDetail --|> crm_contact_detail : "bundle: email"
  TelephoneContactDetail --|> crm_contact_detail : "bundle: telephone"

Bundle Types

The CRM Contact Detail entity supports three main bundle types, each designed for specific types of contact information:

Address Bundle (address)

Stores postal/physical addresses using Drupal's Address field module.

  • Bundle ID: address
  • Field: address (Address field type)
  • Class: Drupal\crm\Entity\ContactDetail\AddressContactDetail
  • Configuration: Customized to hide name fields (given, additional, family, organization)
  • Required: Yes (address field is required)

Example Address Detail:

bundle: address
type: home  # or work, billing, etc.
address:
  country_code: US
  administrative_area: CA
  locality: San Francisco
  postal_code: "94102"
  address_line1: "123 Main St"
  address_line2: "Apt 4B"

Email Bundle (email)

Stores email addresses using Drupal's Email field module.

  • Bundle ID: email
  • Field: email (Email field type)
  • Class: Drupal\crm\Entity\ContactDetail\EmailContactDetail
  • Required: No
  • Validation: Email format validation

Example Email Detail:

bundle: email
type: work  # or home, main, other, etc.
email: "john.doe@example.com"

Telephone Bundle (telephone)

Stores telephone numbers using Drupal's Telephone field module.

  • Bundle ID: telephone
  • Field: telephone (Telephone field type)
  • Class: Drupal\crm\Entity\ContactDetail\TelephoneContactDetail
  • Required: No
  • Validation: Telephone format validation

Example Telephone Detail:

bundle: telephone
type: mobile  # or home, work, fax, voicemail, etc.
telephone: "+1-555-123-4567"

Detail Types

Detail types provide semantic classification for contact details, allowing you to specify the context or purpose of each detail (e.g., "home" vs "work" email).

Available Detail Types

The following detail types are available by default:

Type ID Label Description Applicable Bundles
home Home Personal/home contact information All bundles
work Work Business/work contact information All bundles
main Main Primary contact information All bundles
other Other Alternative contact information All bundles
mobile Mobile Mobile telephone number telephone only
fax Fax Fax telephone number telephone only
voicemail Voicemail Voicemail telephone number telephone only
billing Billing Billing address address only

Bundle Restrictions

Detail types can be configured to apply only to specific bundle types:

  • Global Types (home, work, main, other): Available for all bundle types
  • Telephone-Specific (mobile, fax, voicemail): Only available for telephone bundles
  • Address-Specific (billing): Only available for address bundles

Configuration

Detail types are configured in crm_detail_type configuration entities:

# config/optional/crm.crm_detail_type.mobile.yml
id: mobile
label: Mobile
description: ''
bundles:
  - telephone
negate: false

The bundles array specifies which contact detail bundles this type applies to. An empty array means it applies to all bundles.

Fields Reference

Base Fields

All contact detail entities have the following base fields:

Field Name Type Description Required Revisionable
id Integer Unique entity identifier Auto No
uuid UUID Universal unique identifier Auto No
revision_id Integer Current revision ID Auto No
bundle String Bundle type (address, email, telephone) Auto No
type Entity Reference Reference to crm_detail_type No Yes
status Boolean Whether the detail is active Yes (default: TRUE) Yes
crm_contact Entity Reference Reference to parent crm_contact Yes No
created Timestamp Creation timestamp Auto No
changed Timestamp Last modified timestamp Auto No

Revision Fields

Contact details are fully revisionable with the following revision metadata:

Field Name Type Description
revision_uid Entity Reference User who created the revision
revision_timestamp Timestamp When the revision was created
revision_log Text Revision log message

Bundle-Specific Fields

Address Bundle Fields

Field Name Type Settings Required
address Address Country list: All, Name fields hidden Yes

Address Field Configuration:

field_overrides:
  givenName:
    override: hidden
  additionalName:
    override: hidden
  familyName:
    override: hidden
  organization:
    override: hidden

Email Bundle Fields

Field Name Type Settings Required
email Email Standard email validation No

Telephone Bundle Fields

Field Name Type Settings Required
telephone Telephone Standard telephone validation No

Access Control

Contact detail access control is handled by the ContactDetailAccessControlHandler class, which implements a hierarchical permission system based on the parent contact.

Permission Model

Contact detail permissions are inherited from the parent contact entity:

  1. Administrative Access: Users with administer crm permission have full access
  2. Inherited Access: Contact detail operations inherit permissions from the parent contact
  3. No Orphan Details: Contact details without a parent contact are denied access

Operation Permissions

Operation Required Permission Fallback
view Parent contact view permission Contact view access
update Parent contact update permission Contact edit access
delete Parent contact update permission Contact edit access
create Any of: create crm_contact, edit any crm_contact, administer crm -

Access Control Logic

// View access - inherits from parent contact
if ($operation === 'view') {
    return $contact->access($operation, $account, TRUE);
}

// Edit/Delete access - requires parent contact update permission
if ($operation === 'update' || $operation === 'delete') {
    return $contact->access('update', $account, TRUE);
}

Security Features

  • Parent Contact Validation: Details must belong to a valid contact
  • Cascading Permissions: Detail permissions automatically follow contact permissions
  • Administrative Override: CRM administrators can access all details
  • No Direct Permissions: Contact details don't have standalone permissions

API Usage Examples

Creating Contact Details

Creating an Email Detail

<?php

use Drupal\crm\Entity\ContactDetail;

// Create an email contact detail
$email_detail = ContactDetail::create([
  'bundle'      => 'email',
  'type'        => 'work',  // Reference to crm_detail_type
  'status'      => TRUE,
  'crm_contact' => $contact_id,
  'email'       => 'john.doe@example.com',
]);

$email_detail->save();

Creating an Address Detail

<?php

use Drupal\crm\Entity\ContactDetail;

// Create an address contact detail
$address_detail = ContactDetail::create([
  'bundle'      => 'address',
  'type'        => 'billing',
  'status'      => TRUE,
  'crm_contact' => $contact_id,
  'address'     => [
    'country_code'         => 'US',
    'administrative_area'  => 'CA',
    'locality'            => 'San Francisco',
    'postal_code'         => '94102',
    'address_line1'       => '123 Main Street',
    'address_line2'       => 'Suite 400',
  ],
]);

$address_detail->save();

Creating a Telephone Detail

<?php

use Drupal\crm\Entity\ContactDetail;

// Create a telephone contact detail
$phone_detail = ContactDetail::create([
  'bundle'      => 'telephone',
  'type'        => 'mobile',
  'status'      => TRUE,
  'crm_contact' => $contact_id,
  'telephone'   => '+1-555-123-4567',
]);

$phone_detail->save();

Loading Contact Details

Load All Details for a Contact

<?php

use Drupal\crm\Entity\ContactDetail;

// Load all contact details for a specific contact
$details = \Drupal::entityTypeManager()
  ->getStorage('crm_contact_detail')
  ->loadByProperties(['crm_contact' => $contact_id]);

foreach ($details as $detail) {
  $bundle = $detail->bundle();
  $type = $detail->get('type')->entity?->label() ?? 'Unknown';

  switch ($bundle) {
    case 'email':
      $value = $detail->get('email')->value;
      break;
    case 'telephone':
      $value = $detail->get('telephone')->value;
      break;
    case 'address':
      $address = $detail->get('address')->first();
      $value = $address->getAddressLine1() . ', ' . $address->getLocality();
      break;
  }

  print "Detail: $type $bundle - $value\n";
}

Load Details by Bundle Type

<?php

// Load only email details for a contact
$email_details = \Drupal::entityTypeManager()
  ->getStorage('crm_contact_detail')
  ->loadByProperties([
    'crm_contact' => $contact_id,
    'bundle'      => 'email',
    'status'      => TRUE,
  ]);

Load Details by Detail Type

<?php

// Load all "work" details for a contact
$work_details = \Drupal::entityTypeManager()
  ->getStorage('crm_contact_detail')
  ->loadByProperties([
    'crm_contact' => $contact_id,
    'type'        => 'work',
    'status'      => TRUE,
  ]);

Updating Contact Details

<?php

// Update an existing contact detail
$detail = ContactDetail::load($detail_id);

if ($detail) {
  // Update the status
  $detail->set('status', FALSE);

  // Update type
  $detail->set('type', 'home');

  // Update the specific field based on bundle
  switch ($detail->bundle()) {
    case 'email':
      $detail->set('email', 'new.email@example.com');
      break;
    case 'telephone':
      $detail->set('telephone', '+1-555-987-6543');
      break;
    case 'address':
      $detail->set('address', [
        'country_code'        => 'US',
        'administrative_area' => 'NY',
        'locality'           => 'New York',
        'postal_code'        => '10001',
        'address_line1'      => '456 Broadway',
      ]);
      break;
  }

  // Save with revision
  $detail->setNewRevision(TRUE);
  $detail->setRevisionLogMessage('Updated contact detail');
  $detail->save();
}

Querying Contact Details

Using Entity Query

<?php

use Drupal\Core\Entity\Query\QueryInterface;

// Query for active telephone details with mobile type
$query = \Drupal::entityQuery('crm_contact_detail')
  ->condition('bundle', 'telephone')
  ->condition('type', 'mobile')
  ->condition('status', TRUE)
  ->accessCheck(TRUE);

$detail_ids = $query->execute();
$details = ContactDetail::loadMultiple($detail_ids);

Complex Queries with Joins

<?php

// Find all email details for contacts in a specific city
$query = \Drupal::database()->select('crm_contact_detail', 'cd');
$query->join('crm_contact_detail__address', 'cda', 'cd.id = cda.entity_id');
$query->fields('cd', ['id'])
  ->condition('cd.bundle', 'email')
  ->condition('cd.status', TRUE)
  ->condition('cda.address_locality', 'San Francisco');

$result = $query->execute()->fetchCol();
$details = ContactDetail::loadMultiple($result);

Working with Revisions

<?php

// Load a specific revision
$revision_id = 123;
$revision = \Drupal::entityTypeManager()
  ->getStorage('crm_contact_detail')
  ->loadRevision($revision_id);

// Get revision information
$revision_user = $revision->getRevisionUser();
$revision_time = $revision->getRevisionCreationTime();
$revision_log = $revision->getRevisionLogMessage();

// Load all revisions for a contact detail
$revision_ids = \Drupal::entityTypeManager()
  ->getStorage('crm_contact_detail')
  ->revisionIds($detail);

foreach ($revision_ids as $revision_id) {
  $revision = \Drupal::entityTypeManager()
    ->getStorage('crm_contact_detail')
    ->loadRevision($revision_id);

  print "Revision: " . $revision->getRevisionId() .
        " by " . $revision->getRevisionUser()->getDisplayName() .
        " at " . date('Y-m-d H:i:s', $revision->getRevisionCreationTime()) . "\n";
}

Administrative URLs

  • Contact Details List: /admin/content/crm/detail
  • Contact Detail Types: /admin/structure/crm/contact-detail-types
  • Detail Types: /admin/structure/crm/detail-type