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 |
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:
- Administrative Access: Users with
administer crm
permission have full access - Inherited Access: Contact detail operations inherit permissions from the parent contact
- 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