CRM Contact Method Entity
Overview
The crm_contact_method 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 methods.
Key Features
- Revisionable: Full revision tracking with timestamps and user information
- Type-based: Three main 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 contact method type and method detail type, providing a flexible classification system.
Entity Structure
classDiagram
class crm_contact_method {
+int id [PK]
+uuid uuid
+int revision_id
+string type --> crm_contact_method_type.id
+int detail --> crm_method_detail.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_method_revision {
+int revision_id [PK]
+uuid uuid
+int id
+string type
+int detail --> crm_method_detail.id
+bool status
+int created [timestamp]
+int changed [timestamp]
+int revision_uid --> users.uid
+int revision_timestamp [timestamp]
+string revision_log
}
class crm_contact_method_type {
+string id [PK]
+string label
+string description
+bool locked
}
class crm_method_detail {
+string id [PK]
+string label
+string description
+array bundles
+bool negate
}
class crm_contact {
+int id [PK]
+string type
+string name
}
crm_contact_method --|> crm_contact_method_revision : "has revisions"
crm_contact_method --|> crm_contact_method_type : "contact method type"
crm_contact_method --|> crm_method_detail : "classified by"
crm_contact_method --|> crm_contact : "belongs to"
class AddressContactMethod {
+address_field address
+label() string
}
class EmailContactMethod {
+email_field email
+label() string
}
class TelephoneContactMethod {
+telephone_field telephone
+label() string
}
AddressContactMethod --|> crm_contact_method : "address"
EmailContactMethod --|> crm_contact_method : "email"
TelephoneContactMethod --|> crm_contact_method : "telephone"
Contact Method Types
The CRM Contact Method entity supports three main types, each designed for specific types of contact information:
Address (address)
Stores postal/physical addresses using Drupal's Address field module.
- Type ID:
address - Field:
address(Address field type) - Class:
Drupal\crm\Entity\ContactMethod\AddressContactMethod - Configuration: Customized to hide name fields (given, additional, family, organization)
- Required: Yes (address field is required)
Example Address Detail:
type: address
detail: 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 (email)
Stores email addresses using Drupal's Email field module.
- Type ID:
email - Field:
email(Email field type) - Class:
Drupal\crm\Entity\ContactMethod\EmailContactMethod - Required: No
- Validation: Email format validation
Example Email Detail:
type: email
detail: work # or home, main, other, etc.
email: "john.doe@example.com"
Telephone (telephone)
Stores telephone numbers using Drupal's Telephone field module.
- Type ID:
telephone - Field:
telephone(Telephone field type) - Class:
Drupal\crm\Entity\ContactMethod\TelephoneContactMethod - Required: No
- Validation: Telephone format validation
Example Telephone Detail:
type: telephone
detail: mobile # or home, work, fax, etc.
telephone: "+1-555-123-4567"
Method Detail Types
📖 Full Documentation: For comprehensive information about method detail types, see the Method Detail Entity documentation.
Method detail types provide semantic classification for contact methods, allowing you to specify the context or purpose of each detail (e.g., "home" vs "work" email).
Available Method Detail Types
The following method 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 |
billing |
Billing | Billing address | address only |
Contact Method Type Restrictions
Method detail types can be configured to apply only to specific contact method types:
- Global Types (
home,work,main,other): Available for all contact method types - Telephone-Specific (
mobile,fax): Only available for telephone type - Address-Specific (
billing): Only available for address type
Configuration
Method details are configured in crm_method_detail configuration entities:
# config/optional/crm.crm_method_detail.mobile.yml
id: mobile
label: Mobile
description: ''
bundles:
- telephone
negate: false
The bundles array specifies which contact method types this detail type applies to. An empty array means it applies to all types.
Fields Reference
Base Fields
All contact method 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 |
type |
String | Contact method type (address, email, telephone) | Auto | No |
detail |
Entity Reference | Reference to crm_method_detail |
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 Type 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 Type Fields
| Field Name | Type | Settings | Required |
|---|---|---|---|
email |
Standard email validation | No |
Telephone Type Fields
| Field Name | Type | Settings | Required |
|---|---|---|---|
telephone |
Telephone | Standard telephone validation | No |
Access Control
Contact detail access control is handled by the ContactMethodAccessControlHandler 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 crmpermission 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 any crm contact, edit any crm contact, administer crm |
- |
Access Control Logic
<?php
declare(strict_types=1);
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
use Drupal\crm\Entity\ContactMethod;
/**
* Example: Check access to a contact method programmatically.
*
* @param int $contact_method_id
* The contact method entity ID.
* @param string $operation
* The operation to check (view, update, delete).
* @param \Drupal\Core\Session\AccountInterface|null $account
* The user account to check access for (defaults to current user).
*
* @return bool
* TRUE if access is allowed, FALSE otherwise.
*/
function check_contact_method_access(int $contact_method_id, string $operation, ?AccountInterface $account = NULL): bool {
if ($account === NULL) {
$account = \Drupal::currentUser();
}
// Load the contact method entity.
$contact_method = ContactMethod::load($contact_method_id);
if ($contact_method === NULL) {
return FALSE;
}
// Check access using the entity access system.
$access_result = $contact_method->access($operation, $account, TRUE);
return $access_result->isAllowed();
}
/**
* Example: Access control logic from ContactMethodAccessControlHandler.
*
* This demonstrates how access control works internally for contact methods.
*/
function example_access_control_logic(ContactMethod $entity, string $operation, AccountInterface $account): AccessResult {
// Administrative users have full access.
if ($account->hasPermission('administer crm')) {
return AccessResult::allowed()->cachePerPermissions();
}
// Load the parent contact.
$contact = $entity->get('crm_contact')->entity;
if ($contact === NULL) {
// No parent contact = deny access.
return AccessResult::forbidden('Contact method must have a parent contact.')
->addCacheableDependency($entity);
}
// 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);
}
// Default deny.
return AccessResult::neutral();
}
Key Points:
- Contact methods inherit access permissions from their parent contact
- Administrative users with
administer crmpermission have full access - View operations check parent contact's view permission
- Update and delete operations check parent contact's update permission
- Orphaned contact methods (without a parent) are denied access
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 Methods
Creating an Email Detail
<?php
declare(strict_types=1);
use Drupal\crm\Entity\ContactMethod;
// Create an email contact method.
$email_detail = ContactMethod::create([
'type' => 'email',
'detail' => 'work', // Reference to crm_method_detail entity.
'status' => TRUE,
'crm_contact' => $contact_id,
'email' => 'john.doe@example.com',
]);
try {
$email_detail->save();
\Drupal::logger('my_module')->info('Created email contact method with ID @id', ['@id' => $email_detail->id()]);
}
catch (\Exception $e) {
\Drupal::logger('my_module')->error('Failed to create email contact method: @message', ['@message' => $e->getMessage()]);
}
Creating an Address Detail
<?php
declare(strict_types=1);
use Drupal\crm\Entity\ContactMethod;
// Create an address contact method.
$address_detail = ContactMethod::create([
'type' => 'address',
'detail' => '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',
],
]);
try {
$address_detail->save();
\Drupal::logger('my_module')->info('Created address contact method with ID @id', ['@id' => $address_detail->id()]);
}
catch (\Exception $e) {
\Drupal::logger('my_module')->error('Failed to create address contact method: @message', ['@message' => $e->getMessage()]);
}
Creating a Telephone Detail
<?php
declare(strict_types=1);
use Drupal\crm\Entity\ContactMethod;
// Create a telephone contact method.
$phone_detail = ContactMethod::create([
'type' => 'telephone',
'detail' => 'mobile',
'status' => TRUE,
'crm_contact' => $contact_id,
'telephone' => '+1-555-123-4567',
]);
try {
$phone_detail->save();
\Drupal::logger('my_module')->info('Created telephone contact method with ID @id', ['@id' => $phone_detail->id()]);
}
catch (\Exception $e) {
\Drupal::logger('my_module')->error('Failed to create telephone contact method: @message', ['@message' => $e->getMessage()]);
}
Loading Contact Methods
Load All Details for a Contact
<?php
declare(strict_types=1);
use Drupal\crm\Entity\ContactMethod;
// Load all contact methods for a specific contact.
$storage = \Drupal::entityTypeManager()->getStorage('crm_contact_method');
$details = $storage->loadByProperties(['crm_contact' => $contact_id]);
if (empty($details)) {
\Drupal::logger('my_module')->info('No contact methods found for contact @id', ['@id' => $contact_id]);
return;
}
foreach ($details as $detail) {
$method_type = $detail->bundle();
// Get the detail type label (e.g., "work", "home").
$detail_entity = $detail->get('detail')->entity;
$detail_type = $detail_entity !== NULL ? $detail_entity->label() : 'Unknown';
$value = '';
switch ($method_type) {
case 'email':
$value = $detail->get('email')->value ?? '';
break;
case 'telephone':
$value = $detail->get('telephone')->value ?? '';
break;
case 'address':
$address = $detail->get('address')->first();
if ($address !== NULL) {
$value = $address->getAddressLine1() . ', ' . $address->getLocality();
}
break;
}
\Drupal::logger('my_module')->info('Detail: @detail_type @method_type - @value', [
'@detail_type' => $detail_type,
'@method_type' => $method_type,
'@value' => $value,
]);
}
Load Details by Contact Method Type
<?php
declare(strict_types=1);
// Load only email details for a contact.
$storage = \Drupal::entityTypeManager()->getStorage('crm_contact_method');
$email_details = $storage->loadByProperties([
'crm_contact' => $contact_id,
'type' => 'email',
'status' => TRUE,
]);
if (empty($email_details)) {
\Drupal::logger('my_module')->info('No active email addresses found for contact @id', ['@id' => $contact_id]);
}
Load Details by Method Detail Type
<?php
declare(strict_types=1);
// Load all "work" details for a contact.
$storage = \Drupal::entityTypeManager()->getStorage('crm_contact_method');
$work_details = $storage->loadByProperties([
'crm_contact' => $contact_id,
'detail' => 'work',
'status' => TRUE,
]);
if (empty($work_details)) {
\Drupal::logger('my_module')->info('No work contact methods found for contact @id', ['@id' => $contact_id]);
}
Updating Contact Methods
<?php
declare(strict_types=1);
use Drupal\crm\Entity\ContactMethod;
// Update an existing contact method.
$detail = ContactMethod::load($detail_id);
if ($detail === NULL) {
\Drupal::logger('my_module')->warning('Contact method @id not found.', ['@id' => $detail_id]);
return;
}
// Update the status.
$detail->set('status', FALSE);
// Update the detail type.
$detail->set('detail', 'home');
// Update the specific field based on bundle type.
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 a new revision.
$detail->setNewRevision(TRUE);
$detail->setRevisionLogMessage('Updated contact method');
try {
$detail->save();
\Drupal::logger('my_module')->info('Updated contact method @id', ['@id' => $detail->id()]);
}
catch (\Exception $e) {
\Drupal::logger('my_module')->error('Failed to update contact method: @message', ['@message' => $e->getMessage()]);
}
Querying Contact Methods
Using Entity Query
<?php
declare(strict_types=1);
use Drupal\crm\Entity\ContactMethod;
// Query for active telephone details with mobile type.
$query = \Drupal::entityQuery('crm_contact_method')
->condition('type', 'telephone')
->condition('detail', 'mobile')
->condition('status', TRUE)
->accessCheck(TRUE);
$detail_ids = $query->execute();
if (empty($detail_ids)) {
\Drupal::logger('my_module')->info('No active mobile phone numbers found.');
return;
}
$details = ContactMethod::loadMultiple($detail_ids);
foreach ($details as $detail) {
$phone = $detail->get('telephone')->value;
\Drupal::logger('my_module')->info('Mobile phone: @phone', ['@phone' => $phone]);
}
Complex Queries with Joins
<?php
declare(strict_types=1);
use Drupal\crm\Entity\ContactMethod;
// Find all email details for contacts with addresses in a specific city.
$database = \Drupal::database();
$query = $database->select('crm_contact_method', 'cd');
$query->join('crm_contact_method__address', 'cda', 'cd.id = cda.entity_id');
$query->fields('cd', ['id'])
->condition('cd.type', 'email')
->condition('cd.status', TRUE)
->condition('cda.address_locality', 'San Francisco');
try {
$result = $query->execute()->fetchCol();
if (empty($result)) {
\Drupal::logger('my_module')->info('No email addresses found for contacts in San Francisco.');
return;
}
$details = ContactMethod::loadMultiple($result);
foreach ($details as $detail) {
$email = $detail->get('email')->value;
\Drupal::logger('my_module')->info('Found email: @email', ['@email' => $email]);
}
}
catch (\Exception $e) {
\Drupal::logger('my_module')->error('Database query failed: @message', ['@message' => $e->getMessage()]);
}
Working with Revisions
<?php
declare(strict_types=1);
use Drupal\crm\Entity\ContactMethod;
$storage = \Drupal::entityTypeManager()->getStorage('crm_contact_method');
// Load a specific revision.
$revision_id = 123;
$revision = $storage->loadRevision($revision_id);
if ($revision === NULL) {
\Drupal::logger('my_module')->warning('Revision @id not found.', ['@id' => $revision_id]);
return;
}
// Get revision information.
$revision_user = $revision->getRevisionUser();
$revision_time = $revision->getRevisionCreationTime();
$revision_log = $revision->getRevisionLogMessage();
\Drupal::logger('my_module')->info('Revision @id by @user at @time: @log', [
'@id' => $revision->getRevisionId(),
'@user' => $revision_user !== NULL ? $revision_user->getDisplayName() : 'Unknown',
'@time' => date('Y-m-d H:i:s', $revision_time),
'@log' => $revision_log ?? 'No log message',
]);
// Load all revisions for a contact method.
$detail = ContactMethod::load($detail_id);
if ($detail === NULL) {
\Drupal::logger('my_module')->warning('Contact method @id not found.', ['@id' => $detail_id]);
return;
}
$revision_ids = $storage->revisionIds($detail);
foreach ($revision_ids as $rid) {
$revision = $storage->loadRevision($rid);
if ($revision === NULL) {
continue;
}
$user = $revision->getRevisionUser();
$user_name = $user !== NULL ? $user->getDisplayName() : 'Unknown';
\Drupal::logger('my_module')->info('Revision: @id by @user at @time', [
'@id' => $revision->getRevisionId(),
'@user' => $user_name,
'@time' => date('Y-m-d H:i:s', $revision->getRevisionCreationTime()),
]);
}
Common Pitfalls and Best Practices
Always Set the Parent Contact
Contact methods must have a valid parent contact:
<?php
declare(strict_types=1);
use Drupal\crm\Entity\ContactMethod;
// WRONG: Missing parent contact.
$email = ContactMethod::create([
'type' => 'email',
'email' => 'test@example.com',
]);
$email->save(); // This may fail or create orphaned data!
// CORRECT: Always set the parent contact.
$email = ContactMethod::create([
'type' => 'email',
'email' => 'test@example.com',
'crm_contact' => $contact_id,
'status' => TRUE,
]);
Validate Email and Phone Formats
<?php
declare(strict_types=1);
use Drupal\crm\Entity\ContactMethod;
// Validate email before creating.
$email_address = 'invalid-email';
if (!filter_var($email_address, FILTER_VALIDATE_EMAIL)) {
\Drupal::logger('my_module')->error('Invalid email address: @email', [
'@email' => $email_address,
]);
return;
}
$email = ContactMethod::create([
'type' => 'email',
'email' => $email_address,
'crm_contact' => $contact_id,
'status' => TRUE,
]);
// Run entity validation before saving.
$violations = $email->validate();
if ($violations->count() > 0) {
foreach ($violations as $violation) {
\Drupal::logger('my_module')->error('Validation error: @message', [
'@message' => $violation->getMessage(),
]);
}
return;
}
try {
$email->save();
}
catch (\Exception $e) {
\Drupal::logger('my_module')->error('Failed to save contact method: @message', [
'@message' => $e->getMessage(),
]);
}
Check Access Before Operations
<?php
declare(strict_types=1);
use Drupal\crm\Entity\ContactMethod;
$contact_method = ContactMethod::load($detail_id);
if ($contact_method === NULL) {
\Drupal::logger('my_module')->warning('Contact method @id not found.', ['@id' => $detail_id]);
return;
}
$current_user = \Drupal::currentUser();
// Check if user has permission to update this contact method.
if (!$contact_method->access('update', $current_user)) {
\Drupal::logger('my_module')->warning('User @uid does not have permission to update contact method @id', [
'@uid' => $current_user->id(),
'@id' => $detail_id,
]);
return;
}
// Safe to proceed with update.
$contact_method->set('status', FALSE);
$contact_method->save();
Handle Bundle-Specific Fields Correctly
<?php
declare(strict_types=1);
use Drupal\crm\Entity\ContactMethod;
$contact_method = ContactMethod::load($detail_id);
if ($contact_method === NULL) {
return;
}
// WRONG: Assuming field exists without checking bundle.
$email = $contact_method->get('email')->value; // Fatal error if not email type!
// CORRECT: Check bundle first.
switch ($contact_method->bundle()) {
case 'email':
if ($contact_method->hasField('email')) {
$value = $contact_method->get('email')->value;
}
break;
case 'telephone':
if ($contact_method->hasField('telephone')) {
$value = $contact_method->get('telephone')->value;
}
break;
case 'address':
if ($contact_method->hasField('address')) {
$address_field = $contact_method->get('address')->first();
if ($address_field !== NULL) {
$value = $address_field->getAddressLine1();
}
}
break;
}
Security Considerations
Access Control Inheritance
Contact methods inherit access control from their parent contact. This means:
- Data Leakage Prevention: Users cannot access contact methods unless they can access the parent contact
- Cascading Permissions: Changing contact permissions automatically affects all its contact methods
- Orphan Protection: Contact methods without valid parent contacts are denied access
<?php
declare(strict_types=1);
use Drupal\crm\Entity\ContactMethod;
// Always verify the user has access to the parent contact.
$contact_method = ContactMethod::load($detail_id);
if ($contact_method === NULL) {
return;
}
$contact = $contact_method->get('crm_contact')->entity;
if ($contact === NULL) {
\Drupal::logger('my_module')->error('Contact method @id has no parent contact.', [
'@id' => $detail_id,
]);
return;
}
$current_user = \Drupal::currentUser();
if (!$contact->access('view', $current_user)) {
\Drupal::logger('my_module')->warning('Access denied to contact @cid for user @uid', [
'@cid' => $contact->id(),
'@uid' => $current_user->id(),
]);
return;
}
// Now safe to work with the contact method.
Sanitizing Output
When displaying contact method data, always sanitize output:
<?php
declare(strict_types=1);
use Drupal\Component\Utility\Html;
$email = $contact_method->get('email')->value;
// WRONG: Direct output without sanitization.
print '<div>' . $email . '</div>'; // XSS risk!
// CORRECT: Use proper sanitization.
print '<div>' . Html::escape($email) . '</div>';
// BETTER: Use render arrays.
$build = [
'#type' => 'html_tag',
'#tag' => 'div',
'#value' => $email,
];
Performance Optimization
Bulk Loading Contact Methods
<?php
declare(strict_types=1);
use Drupal\crm\Entity\ContactMethod;
// INEFFICIENT: N+1 query problem.
$contact_ids = [1, 2, 3, 4, 5];
foreach ($contact_ids as $contact_id) {
$details = \Drupal::entityTypeManager()
->getStorage('crm_contact_method')
->loadByProperties(['crm_contact' => $contact_id]);
foreach ($details as $detail) {
// Process each detail...
}
}
// EFFICIENT: Single query.
$storage = \Drupal::entityTypeManager()->getStorage('crm_contact_method');
$query = $storage->getQuery()
->condition('crm_contact', $contact_ids, 'IN')
->condition('status', TRUE)
->accessCheck(TRUE);
$detail_ids = $query->execute();
$all_details = ContactMethod::loadMultiple($detail_ids);
// Group by contact for processing.
$grouped = [];
foreach ($all_details as $detail) {
$contact_id = $detail->get('crm_contact')->target_id;
$grouped[$contact_id][] = $detail;
}
Caching Considerations
<?php
declare(strict_types=1);
// Contact methods are cached with cache tags.
// When a contact method changes, related caches are invalidated.
// Example: Custom cache with proper tags.
$cid = 'my_module:contact_methods:' . $contact_id;
$cache = \Drupal::cache()->get($cid);
if ($cache === FALSE) {
$details = \Drupal::entityTypeManager()
->getStorage('crm_contact_method')
->loadByProperties(['crm_contact' => $contact_id]);
$data = [];
$cache_tags = ['crm_contact:' . $contact_id];
foreach ($details as $detail) {
$data[] = [
'type' => $detail->bundle(),
'value' => $detail->label(),
];
// Add cache tags for each contact method.
$cache_tags[] = 'crm_contact_method:' . $detail->id();
}
\Drupal::cache()->set($cid, $data, \Drupal\Core\Cache\CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
}
else {
$data = $cache->data;
}
Administrative URLs
- Contact Methods List:
/admin/content/crm/detail - Contact Method Types:
/admin/structure/crm/contact-detail-types - Method Details:
/admin/structure/crm/method-detail