One of the key features of the Views module is supporting for relationships, a mechanism that allows data from different entities or database tables to be joined. This enables access to fields from related entities directly within a single view.
By default, Views provides a wide range of predefined relationships. For example, it can link nodes to their authors (users), taxonomy terms, or files. However, in more complex cases, such as integrating with custom entities, you may need to define a custom relationship to access data that is not available in Views out of the box.
Such a need arises, for instance, when working with the LocalGov Workflow module, which adds extra Review date functionality. This information is stored in a separate custom review_date entity and is not linked to node-based views by default. Therefore, if you want to display the review date in the administrative content view (/admin/content), you need to manually define a new relationship between the node entity and the review_date entity.
Creating a custom Views relationship involves three main steps:
- Registering the views relationship in the Views data
- Defining a custom views relationship plugin
- And finally using the relationship through the Views UI
Let's look at how to implement this in practice and display the review date in the administrative content view.
Registering the views relationship in the Views data
The initial step in establishing a customized relationship involves registering it within the Views system. This is achieved by implementing the hook_views_data_alter() hook in a custom module (@see hook_views_data_alter() in views.api.php). During this process, we notify Views regarding the method to join the "review_date" entity table with the node table, thereby enabling their linkage within a view.

<?php
/**
* @file
* Contains bbd_custom.views.inc.
*/
/**
* Implements hook_views_data_alter().
*/
function bbd_custom_views_data_alter(array &$data) {
if (isset($data['node'])) {
$data['node']['bbd_custom_review_date'] = [
'title' => t('Review date'),
'relationship' => [
'group' => t('Node'),
'title' => t('Review date'),
'help' => t('List of review dates.'),
// The table to which the join will occur ('review_date').
'base' => 'review_date',
// The field in the 'review_date' table that contains the node reference (nid).
'base field' => 'entity',
// The field in the 'node' table that establishes the relationship.
'relationship field' => 'nid',
// The ID of the custom relationship plugin that will handle the join.
'id' => 'bbd_custom_review_date',
],
];
}
}
In this example, a new relationship bbd_custom_review_date is added to the node table. It is specified that the join occurs with the "review_date" table via the entity field, which contains the node ID. The ID of the bbd_custom_review_date plugin is defined, which will be implemented in the next step.
After registering this relationship, Views will be aware of the possibility to join node with review_date, but the join logic is not yet implemented. We will implement the join logic in the next step.
Defining a custom views relationship plugin
Each views relationship plugin should be placed in the directory “Plugin/views/relationship”. Let's create our new custom relationship plugin with the id bbd_custom_review_date, defined in the hook on the previous step, and use RelationshipPluginBase as base.
<?php
namespace Drupal\bbd_custom\Plugin\views\relationship;
use Drupal\views\Attribute\ViewsRelationship;
use Drupal\views\Plugin\views\relationship\RelationshipPluginBase;
use Drupal\views\Plugin\ViewsHandlerManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Relationship plugin to display review date for the nodes.
*
* @ingroup views_relationship_handlers
*/
#[ViewsRelationship("bbd_custom_review_date")]
class ReviewDate extends RelationshipPluginBase {
/**
* {@inheritdoc}
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
protected ViewsHandlerManager $join_manager,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('plugin.manager.views.join')
);
}
/**
* {@inheritdoc}
*/
public function query() {
// Ensure that the current view's table ('node') is available in the query.
$this->ensureMyTable();
// Get the relationship parameters defined in hook_views_data_alter().
$def = $this->definition;
// Specify the name of the table we are joining to - 'review_date'.
// This will be taken from the plugin definition.
// See 'bbd_custom_views_data_alter' in the first step 'base' => 'review_date').
$def['table'] = $this->definition['base'];
// Specify the field in the 'review_date' table that references the node - 'entity'.
// This will be taken from the plugin definition.
// See 'bbd_custom_views_data_alter' in the first step 'base field' => 'entity').
$def['field'] = $this->definition['base field'];
// The table from which the join is executed - 'node'.
$def['left_table'] = $this->tableAlias;
// The field from the 'node' table used for the join - 'nid'.
$def['left_field'] = $this->realField;
$def['adjusted'] = TRUE;
// Set the join type: LEFT or INNER.
$def['type'] = empty($this->options['required']) ? 'LEFT' : 'INNER';
// Create an instance of a standard join with the specified parameters.
/** @var \Drupal\views\Plugin\views\join\JoinPluginBase $join */
$join = $this->join_manager->createInstance('standard', $def);
// Manually generate a unique name for the joined table 'review_date_node_items'.
// This will be the alias for the review_date table in the SQL query created by Views.
$alias = $def['table'] . '_' . $this->table . '_' . 'items';
$this->alias = $this->query->addRelationship($alias, $join, $this->definition['base'], $this->relationship);
}
}
The query() method defines how the SQL JOIN between node and "review_date" is constructed. Once the plugin is created and active, it will be available in the Views user interface.
Using the relationship through the Views UI
After clearing cache, the new relationship Review date will be available in the Relationships section of any view that uses the node base table. Let’s add it to the /admin/content view to display the review date.
Navigate to the administrative content view at /admin/structure/views/view/content. In the Relationships section, add the new relationship “Review date (Node. List of review dates.)” - this is the label defined in hook_views_data_alter().


Add a field from the related entity in the Fields section. The available fields of this entity are shown in Fig. 4. Locate a field associated with the review_date entity, such as Review. Make sure that the correct relationship “entity” is selected in the field settings.


Now, the content list on the /admin/content page will display an additional field - the review time from the "review_date" entity associated with each node.
