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}:aand{type_id}:b, labelled usinglabel_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:
- Add the
crm_contact__relationship_statisticsrelationship to your View (relationship handler: standard). - In the Filter criteria section, add the "Relationship Type" filter from that table.
- 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
@todoto 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.
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.
Related documentation
- Contact entity — full contact field reference
- Relationship entity — symmetric vs asymmetric relationship type details
- User integration — user/contact mapping concepts and field mapping
- Comment integration — commenting on contacts