Skip to content

Views Integration

The CRM module exposes all of its content entities to Drupal's Views system. Site builders can create contact lists and reports from the Views UI using the base tables below; the rest of this page documents handlers, plugins, and Views data hooks for developers extending that data. 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 hook implementations add cross-entity relationships and render-only admin fields that cannot be expressed through entity metadata alone.

Available base tables

Content entities produce a Views base table automatically through their views_data handler.

Source 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. The content entities other than contact 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_data ↔ crm_user_contact_mapping ↔ crm_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.uid → crm_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.id → crm_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.

Access

Contact listings use the query tag crm_contact_access, the same tag entity queries add when loading contacts. Views on the crm_contact base table pick it up automatically. ContactQueryAccessHooks implements hook_query_crm_contact_access_alter() and restricts rows to contacts the account may view.

Users with administer crm or view any crm contact see every contact. Everyone else sees only contacts that match:

  • a bundle-level view permission
  • a mapped person contact, when they have view mapped crm contact
  • a matching crm_contact_access grant from hook_crm_contact_grants() (see Hooks)

Accounts with none of those paths get an empty result. The alter resolves the crm_contact table alias from the query: entity queries use base_table; Views uses the table name.

Custom Views that join crm_contact without that tag do not get this filter. Add the tag on the query, or join through a View whose base table is already crm_contact.

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 [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\Kernel\Views\ContactViewsAccessTest Kernel crm_contact View results filtered by crm_contact_access
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.