Skip to content

Views Integration

Audience: Drupal module developers extending or customizing CRM Views integration. For end-user guidance on building contact lists in the UI, see the relevant entity pages.

The CRM module exposes all of its content entities to Drupal's Views system. Each entity registers a views_data handler in its PHP attribute, the contact entity overrides that handler to customize the relationship-statistics field table, and a hook_views_data_alter() implementation adds cross-entity relationships and render-only admin fields that cannot be expressed through entity metadata alone.

Available base tables

Each CRM entity produces a Views base table automatically through its views_data handler.

Entity type Base table views_data handler
crm_contact crm_contact ContactViewsData (custom)
crm_contact_method crm_contact_method EntityViewsData (core)
crm_relationship crm_relationship EntityViewsData (core)
crm_user_contact_mapping crm_user_contact_mapping EntityViewsData (core)

The custom handler for crm_contact is Drupal\crm\ContactViewsData. All others delegate entirely to Drupal core's EntityViewsData, which automatically exposes every base field, revision field, and field storage table.

Adding a new entity to Views

If you create a new CRM entity and want it available in Views, declare EntityViewsData::class (or your own subclass) as the views_data handler in the entity PHP attribute:

handlers: [
  'views_data' => \Drupal\views\EntityViewsData::class,
  // ...
],

Relationship statistics field

The crm_contact entity has a multi-value base field named relationship_statistics (field type crm_relationship_statistics). Drupal stores it in the crm_contact__relationship_statistics table, which Views exposes automatically through EntityViewsData.

ContactViewsData::getViewsData() then customizes that table before Views sees it:

Customization Reason
Removes the relationship_statistics_count column from Views field options The count is an implementation detail; filtering by type and rendering the full summary through the formatter is the intended use-case.
Retitles relationship_statistics_value to "Relationship Type" Gives site builders a clear, domain-language label instead of the raw field column name.
Attaches the crm_relationship_statistics_type filter plugin to that column Provides relationship-type-aware filter options (see below).

crm_relationship_statistics_type filter plugin

Class: Drupal\crm\Plugin\views\filter\RelationshipStatisticsType

This plugin extends Views core InOperator. It builds its option list dynamically from all crm_relationship_type config entities at query time:

  • Symmetric relationship types – produce one option keyed by the type id; label is the type label.
  • Asymmetric relationship types – produce two options, keyed {type_id}:a and {type_id}:b, labelled using label_a / label_b (with a fallback to the main type label when those fields are empty).

Use this filter on any View whose base table is crm_contact (or that has a relationship to it) to filter contacts by the kind of relationships they participate in.

Example: contacts with a specific relationship type

In the Views UI, add a filter on the relationship-statistics table:

  1. Add the crm_contact__relationship_statistics relationship to your View (relationship handler: standard).
  2. In the Filter criteria section, add the "Relationship Type" filter from that table.
  3. The filter renders as an "is one of" select list populated with all configured relationship types.

User/contact mapping Views integration

The crm_user_contact_mapping entity type stores the one-to-one link between a Drupal user account and a CRM person contact. Because core Views cannot automatically express the users_field_datacrm_user_contact_mappingcrm_contact join chain through entity metadata alone, the module adds it in UserHooks::viewsDataAlter().

Note: This hook implementation carries a @todo to be removed once Drupal core issue #2706431 is resolved.

Cross-table relationships added by hook_views_data_alter()

The following entries are injected into the Views data array:

From users_field_data

Views data key Type Description
crm_user_contact_mapping relationship Joins users_field_data.uidcrm_user_contact_mapping.user. Use this to traverse from a Users base View to contact-mapping data.
crm_user_contact_mapping_sync_form field Exposes the crm_contact_user field plugin on user rows.
crm_core_user_sync_form field Exposes the crm_user_contact_mapping field plugin on user rows.

From crm_contact

Views data key Type Description
crm_user_contact_mapping relationship Joins crm_contact.idcrm_user_contact_mapping.crm_contact. Use this to traverse from a Contacts base View to mapping data.
crm_core_user_sync_form field Exposes the crm_user_contact_mapping field plugin on contact rows, using the user field name for its link target.

crm_user_contact_mapping Views field plugin

Class: Drupal\crm\Plugin\views\field\UserContactMappingField

This is a render-only field plugin. Its query() method is intentionally empty — it adds no SQL to the query. All output is generated at render time from the current row's entity and the crm.user_contact_mapping service.

The plugin's render() method inspects the row entity type and renders an appropriate admin action link:

Row entity No mapping exists Mapping exists
crm_contact "Add Mapping" link → entity.crm_user_contact_mapping.add_form with ?destination=… Edit link → entity.crm_user_contact_mapping.edit_form
user "Add Mapping" link → entity.crm_user_contact_mapping.add_form with ?destination=… Edit link → entity.crm_user_contact_mapping.edit_form

Because this field does not touch the query, it can be added to any View whose base table (or related table) returns crm_contact or user rows, at no SQL cost.

Architecture overview

flowchart TD
    subgraph entities [Entity views_data handlers]
        Contact["crm_contact\n(ContactViewsData)"]
        ContactMethod["crm_contact_method\n(EntityViewsData)"]
        Relationship["crm_relationship\n(EntityViewsData)"]
        Mapping["crm_user_contact_mapping\n(EntityViewsData)"]
    end

    subgraph plugins [Views plugins]
        Filter["crm_relationship_statistics_type\nfilter plugin"]
        Field["crm_user_contact_mapping\nfield plugin"]
    end

    subgraph hook [hook_views_data_alter]
        Alter["UserHooks::viewsDataAlter()"]
    end

    Contact -->|"crm_contact__relationship_statistics\ntable customization"| Filter
    Alter -->|"relationship: users_field_data → mapping"| Mapping
    Alter -->|"relationship: crm_contact → mapping"| Mapping
    Alter -->|"field: crm_core_user_sync_form"| Field
    Alter -->|"field: crm_user_contact_mapping_sync_form"| Field

Extending Views data

Adding new fields or relationships to existing CRM tables

Use hook_views_data_alter() in your own module:

<?php

declare(strict_types=1);

use Drupal\Core\Hook\Attribute\Hook;

class MyModuleHooks {

  #[Hook('views_data_alter')]
  public function viewsDataAlter(array &$data): void {
    // Add a custom relationship from crm_contact to your entity.
    $data['crm_contact']['my_module_relation'] = [
      'title' => t('My custom relation'),
      'help'  => t('Joins crm_contact to my_custom_entity.'),
      'relationship' => [
        'base'       => 'my_custom_entity',
        'base field' => 'contact_id',
        'field'      => 'id',
        'id'         => 'standard',
        'label'      => t('My entity'),
      ],
    ];
  }

}

Customizing Views data for a new CRM entity

Subclass EntityViewsData and override getViewsData():

<?php

declare(strict_types=1);

namespace Drupal\my_module;

use Drupal\views\EntityViewsData;

/**
 * Provides Views data for the My Entity entity.
 */
class MyEntityViewsData extends EntityViewsData {

  /**
   * {@inheritdoc}
   */
  public function getViewsData(): array {
    $data = parent::getViewsData();

    // Customize the auto-generated data here.
    $data['my_entity_table']['my_field']['title'] = $this->t('Custom title');

    return $data;
  }

}

Then declare the handler in the entity PHP attribute:

handlers: [
  'views_data' => \Drupal\my_module\MyEntityViewsData::class,
  // ...
],

Testing

The CRM module tests the Views integration at both the unit and kernel level.

Test class Type What it covers
Drupal\Tests\crm\Kernel\Plugin\views\RelationshipStatisticsViewsTest Kernel Views data presence for crm_contact__relationship_statistics; filter plugin discovery and option building; contact statistics with real data
Drupal\Tests\crm\Kernel\Plugin\views\UserContactMappingFieldKernelTest Kernel Field plugin discovery; noop query()
Drupal\Tests\crm\Unit\Plugin\views\filter\RelationshipStatisticsTypeTest Unit Filter option building for symmetric and asymmetric types
Drupal\Tests\crm\Unit\Plugin\views\field\UserContactMappingFieldTest Unit render() output and URL generation for contact and user rows

To run these tests in isolation:

ddev phpunit --filter=RelationshipStatisticsViewsTest
ddev phpunit --filter=UserContactMappingFieldKernelTest
ddev phpunit --filter=RelationshipStatisticsTypeTest
ddev phpunit --filter=UserContactMappingFieldTest

When writing your own Views integration tests for custom handlers or plugins, use the kernel tests as a reference: they load the views module alongside CRM, then call Views::viewsData()->getAll() to assert table keys, and use the plugin manager to assert plugin definitions.