Skip to content

CRM Contact Entity

The CRM Contact entity (crm_contact) is the core entity of the CRM system, representing individuals, organizations, and households that your organization interacts with. Contacts are revisionable, publishable content entities that support flexible contact management with multiple communication channels.

Overview

A contact represents any entity (person, organization, or household) that can be contacted or has a relationship with your organization. The contact system is designed to be flexible and extensible, supporting various types of contacts with their specific attributes and contact methods.

Key Features

  • Multi-type support: Three built-in contact types (Person, Organization, Household)
  • Revisionable: Full revision tracking with metadata
  • Publishable: Status-based visibility control
  • Contact details: Structured email, telephone, and address management
  • Computed fields: Age calculation for persons
  • Flexible naming: Advanced name formatting for persons
  • External identifiers: Support for external system integration
  • Relationships: Connect contacts to each other through relationships
  • Comments integration: Built-in commenting system

Contact Types (Bundles)

Person

Individual people with personal attributes.

Specific Fields: - full_name: Structured name field with components (title, given, middle, family, generational, credentials) - preferred_name: How the person prefers to be addressed - aliases: Alternative names or nicknames - comment: Internal notes about the person

Date Fields (Person-specific labels): - start_date: Date of birth - end_date: Date of death

Computed Fields: - age: Automatically calculated from date of birth

Organization

Businesses, non-profits, government agencies, and other institutional entities.

Specific Fields: - aliases: Alternative names, DBA names, or acronyms - comment: Internal notes about the organization

Date Fields (Organization-specific labels): - start_date: Date of establishment - end_date: Date of dissolution

Household

Family units or groups of people living at the same address.

Specific Fields: - comment: Internal notes about the household

Date Fields (Household-specific labels): - start_date: Date of establishment - end_date: Date of dissolution

Base Fields

All contact types share these common fields:

Core Identification

  • name (required): Display name for the contact
  • For persons: Auto-generated from full_name field using configurable format
  • For organizations/households: Manually entered
  • id: Unique internal identifier
  • uuid: Universally unique identifier
  • bundle: Contact type (person/organization/household)

Status and Dates

  • status: Active/inactive status (boolean)
  • start_date: Beginning date (context varies by type)
  • end_date: End date (context varies by type)
  • created: Entity creation timestamp
  • changed: Last modification timestamp

Contact Methods

  • emails: Multiple email addresses with types (work, home, etc.)
  • telephones: Multiple phone numbers with types (mobile, work, fax, etc.)
  • addresses: Multiple addresses with types (home, work, billing, etc.)

Each contact method is a separate entity with its own type classification, allowing for flexible organization and display.

Revision System

  • revision_id: Current revision identifier
  • revision_uid: User who created the revision
  • revision_timestamp: When the revision was created
  • revision_log: Description of changes

Entity Features

Automatic Name Generation (Persons)

For person contacts, the name field is automatically generated from the full_name field using a configurable name format. The system supports: - Multiple name components (title, given, middle, family, generational, credentials) - Preferred name integration - Alternative names from aliases - Configurable name formatting patterns

Computed Age Field

Person contacts include an automatically computed age field that calculates the current age based on the date of birth (start_date).

Revision Management

All contacts are revisionable by default: - New revisions created on every save - Complete revision history with user and timestamp tracking - Configurable per contact type

Access Control

Built-in access control system with: - View, create, edit, delete permissions - Type-specific permissions - Integration with Drupal's permission system

Contact Methods Integration

Contacts integrate seamlessly with the Contact Method system:

---
title: Contact Method Integration
---
erDiagram
    Contact ||--o{ Email: has
    Contact ||--o{ Telephone: has
    Contact ||--o{ Address: has
    Contact {
        int id
        string name
        string bundle
        boolean status
    }
    Email {
        int id
        string email
        string type
    }
    Telephone {
        int id
        string telephone
        string type
    }
    Address {
        int id
        string address_line1
        string city
        string type
    }

Configuration Options

Contact Types

  • Create custom contact types beyond the default three
  • Configure fields per contact type
  • Set default revision behavior
  • Configure date field labels
  • Lock/unlock contact types

Name Formatting (Persons)

Configure how person names are formatted: - Default format patterns - Component requirements - Display preferences

Field Configuration

  • Add custom fields to any contact type
  • Configure display modes
  • Set up form displays
  • Configure field permissions

Administrative Features

List Management

  • Sortable contact lists
  • Filter by contact type
  • Bulk operations support
  • Search functionality

Form Integration

  • Inline entity forms for contact methods
  • Auto-complete for contact selection
  • Validation and constraints
  • Multi-step forms support

API Examples

Creating a Person Contact

<?php

declare(strict_types=1);

use Drupal\crm\Entity\Contact;

// Create a new person contact with full name components.
$contact = Contact::create([
  'type' => 'person',
  'full_name' => [
    'title' => 'Dr.',
    'given' => 'Jane',
    'family' => 'Smith',
    'credentials' => 'PhD',
  ],
  'preferred_name' => 'Jane',
  'status' => TRUE,
  'start_date' => '1980-05-15',
]);

try {
  $contact->save();
  \Drupal::logger('my_module')->info('Created contact with ID @id', ['@id' => $contact->id()]);
}
catch (\Exception $e) {
  \Drupal::logger('my_module')->error('Failed to create contact: @message', ['@message' => $e->getMessage()]);
}

Adding Contact Methods

<?php

declare(strict_types=1);

use Drupal\crm\Entity\ContactMethod;

// Create an email contact method.
$email = ContactMethod::create([
  'type' => 'email',
  'email' => 'jane.smith@example.com',
  'detail' => 'work',
  'status' => TRUE,
]);

try {
  $email->save();

  // Add the email to the contact.
  $contact->get('emails')->appendItem($email);
  $contact->save();

  \Drupal::logger('my_module')->info('Added email to contact @id', ['@id' => $contact->id()]);
}
catch (\Exception $e) {
  \Drupal::logger('my_module')->error('Failed to add contact method: @message', ['@message' => $e->getMessage()]);
}

Loading and Accessing Contact Data

<?php

declare(strict_types=1);

use Drupal\crm\Entity\Contact;

// Load a contact by ID.
$contact = Contact::load($contact_id);

// Always check if the entity exists.
if ($contact === NULL) {
  \Drupal::logger('my_module')->warning('Contact @id not found.', ['@id' => $contact_id]);
  return;
}

// Get the formatted display name.
$name = $contact->label();

// Get age (computed field for persons only).
if ($contact->bundle() === 'person') {
  $age = $contact->get('age')->value;
  if ($age !== NULL) {
    \Drupal::logger('my_module')->info('Contact age: @age years', ['@age' => $age]);
  }
}

// Access contact methods (email addresses).
$emails = $contact->get('emails')->referencedEntities();
foreach ($emails as $email_entity) {
  $email_address = $email_entity->get('email')->value;

  // Get the detail type label (e.g., "work", "home").
  $detail_entity = $email_entity->get('detail')->entity;
  $detail_label = $detail_entity !== NULL ? $detail_entity->label() : 'Unknown';

  \Drupal::logger('my_module')->info('Email (@detail): @email', [
    '@detail' => $detail_label,
    '@email' => $email_address,
  ]);
}

Database Schema

---
title: Database Schema
---
classDiagram
  class crm_contact {
    int id
    uuid uuid
    string name
    bool status
    datetime start_date
    datetime end_date
    int revision_id
    string type
    int created
    int changed
  }

  class crm_contact_revision {
    int id
    int revision_id
    uuid uuid
    string name
    bool status
    datetime start_date
    datetime end_date
    int revision_uid
    int revision_timestamp
    string revision_log
  }

  class crm_contact__emails {
    int entity_id
    int revision_id
    int delta
    int target_id
  }

  class crm_contact__telephones {
    int entity_id
    int revision_id
    int delta
    int target_id
  }

  class crm_contact__addresses {
    int entity_id
    int revision_id
    int delta
    int target_id
  }

  class crm_contact__full_name {
    int entity_id
    int revision_id
    int delta
    string full_name_title
    string full_name_given
    string full_name_middle
    string full_name_family
    string full_name_generational
    string full_name_credentials
  }

  class crm_contact_method {
    int id
    uuid uuid
    string type
    string name
    bool status
    int created
    int changed
  }

  crm_contact --> crm_contact__emails : emails
  crm_contact --> crm_contact__telephones : telephones
  crm_contact --> crm_contact__addresses : addresses
  crm_contact --> crm_contact__full_name : full_name
  crm_contact__emails --> crm_contact_method : target_id
  crm_contact__telephones --> crm_contact_method : target_id
  crm_contact__addresses --> crm_contact_method : target_id
  crm_contact --> crm_contact_revision : revisions

Integration Points

Views Integration

  • Contact lists and displays
  • Relationship views
  • Administrative interfaces

Search Integration

  • Full-text search across contact fields
  • Faceted search by contact type
  • Advanced filtering options

REST API

  • Full CRUD operations via REST
  • JSON API support
  • Custom endpoints for specialized operations

External Systems

  • External identifier field for system integration
  • Import/export capabilities
  • Sync relationship management

Common Pitfalls and Best Practices

Always Check for NULL After Loading

<?php

declare(strict_types=1);

use Drupal\crm\Entity\Contact;

// WRONG: Assumes entity exists.
$contact = Contact::load($contact_id);
$name = $contact->label(); // Fatal error if entity doesn't exist!

// CORRECT: Check for NULL.
$contact = Contact::load($contact_id);
if ($contact === NULL) {
  \Drupal::logger('my_module')->warning('Contact @id not found.', ['@id' => $contact_id]);
  return;
}
$name = $contact->label();

Check Entity References Before Accessing

<?php

declare(strict_types=1);

// WRONG: Assumes referenced entity exists.
$detail_entity = $contact_method->get('detail')->entity;
$label = $detail_entity->label(); // Fatal error if entity is NULL!

// CORRECT: Check for NULL.
$detail_entity = $contact_method->get('detail')->entity;
if ($detail_entity !== NULL) {
  $label = $detail_entity->label();
}
else {
  $label = 'Unknown';
}

Use Appropriate Bundle Checks

<?php

declare(strict_types=1);

// WRONG: Using wrong method.
if ($contact->type() === 'person') { // This might not work as expected
  // ...
}

// CORRECT: Use bundle() method.
if ($contact->bundle() === 'person') {
  $age = $contact->get('age')->value;
}

Handle Exceptions When Saving

<?php

declare(strict_types=1);

use Drupal\crm\Entity\Contact;

// WRONG: No error handling.
$contact = Contact::create(['type' => 'person']);
$contact->save(); // May throw exception!

// CORRECT: Wrap in try/catch.
try {
  $contact = Contact::create(['type' => 'person', 'name' => 'Test']);
  $contact->save();
}
catch (\Exception $e) {
  \Drupal::logger('my_module')->error('Save failed: @message', [
    '@message' => $e->getMessage(),
  ]);
}

Performance Considerations

Loading Multiple Contacts:

<?php

declare(strict_types=1);

use Drupal\crm\Entity\Contact;

// INEFFICIENT: Loading in a loop.
foreach ($contact_ids as $id) {
  $contact = Contact::load($id);
  // Process contact...
}

// EFFICIENT: Bulk load.
$contacts = Contact::loadMultiple($contact_ids);
foreach ($contacts as $contact) {
  // Process contact...
}

Accessing Contact Methods:

<?php

declare(strict_types=1);

// When you need only IDs, don't load full entities.
$email_ids = array_column($contact->get('emails')->getValue(), 'target_id');

// Only load full entities when you need their data.
$email_entities = $contact->get('emails')->referencedEntities();

Troubleshooting Common Issues

Issue: Contact Name Not Updating for Persons

Problem: Changing the full_name field doesn't update the name field.

Solution: The name field is computed automatically. Use $contact->save() to trigger the computation.

<?php

declare(strict_types=1);

$contact->set('full_name', [
  'given' => 'Jane',
  'family' => 'Doe',
]);
$contact->save(); // This triggers name computation.

Issue: Computed Age Field Returns NULL

Problem: The age field is NULL even though start_date is set.

Solution: Ensure: 1. The contact is a 'person' type (age only computed for persons) 2. The start_date field contains a valid date 3. The date is in the past

<?php

declare(strict_types=1);

if ($contact->bundle() === 'person' && $contact->get('start_date')->value !== NULL) {
  $age = $contact->get('age')->value;
  if ($age === NULL) {
    \Drupal::logger('my_module')->warning('Age computation failed for contact @id', [
      '@id' => $contact->id(),
    ]);
  }
}

Issue: Cannot Add Contact Methods to Contact

Problem: Contact methods don't appear after adding them.

Solution: Remember to save the contact after appending contact methods:

<?php

declare(strict_types=1);

use Drupal\crm\Entity\ContactMethod;

$email = ContactMethod::create([
  'type' => 'email',
  'email' => 'test@example.com',
  'status' => TRUE,
]);
$email->save();

$contact->get('emails')->appendItem($email);
$contact->save(); // Don't forget this!