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}: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.
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_accessgrant fromhook_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.
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
- Hooks — grant records and listing access
- Permissions — view, mapped-contact, and admin permissions