Category: Development

How to create a Custom Views Pseudo Field on Drupal

Author(s)

Kevin
Kevin
Every multilingual Drupal project eventually runs into the same quiet frustration: your Admin View shows all content (including translated ones) and contributors lose track of which content is the original and which are its translations. The fix is a Views pseudo field — a field that appears in the Views UI like any other, but computes its output at render time.
In this article I am going to show you how to create one from scratch.
The Content View listing Nodes. A Title (original language) pseudo field would make it immediately clear which entries are translations from the original content.
Scope: I will not explain how to create a custom field for a node (i.e. a FieldType/FieldFormatter/FieldWidget Plugin). The focus here is exclusively on creating a field that appears and is rendered inside a View — a so-called pseudo field.
By default, Views module can display data contained in any field attached to an entity that is exposed in Views, and the content of any database column exposed to Views via an implementation of hook_views_data().
It’s possible to create pseudo fields. These appear in the Views UI like any other field, but don’t map exclusively to the data in a database.
Instead, they allow the data to be preprocessed at render time.
This could mean performing a calculation, combining multiple fields into one, loading related entity data or anything else you can express in Drupal.
The mechanism works in two steps:
  1. First, you use hook_views_data_alter (or hook_views_data) to register your pseudo field against a base table — telling Views “this field exists and here is its plugin ID”.
  2. Second, you provide the corresponding Views field plugin class that implements the actual logic.

The code

The examples use the module machine name my_module. Replace this with your own module's machine name throughout.

For this article, I will build a pseudo field that displays a node’s title in its original source language. The standard Views Title field renders in the currently active interface language, which means translated nodes and their source all look identical in the listing. A dedicated Title (original language) column solves that instantly.

Step 1 — Register the Pseudo Field

The code targets Drupal 11. It uses Object-Oriented Hooks, a modern way to implement Drupal hooks introduced recently and replacing procedural functions Class.

Views discovers available fields by scanning the data registered through hook_views_data and hook_views_data_alter. We use the alter variant here because we want to attach our pseudo field to an existing base table — node_field_data — that is already registered by Drupal core.

The field.id key in the registration array is the plugin ID of our Views field plugin. We will create that plugin in the next step.

<?php

namespace Drupal\my_module\Hook;

use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\StringTranslation\StringTranslationTrait;

/**
 * Hook Views Data implementations.
 */
#[Hook('views_data_alter')]
final class ViewsData {

  use StringTranslationTrait;

  /**
   * Implements hook_views_data_alter().
   *
   * Registers the my_module_node_original_title field on the node_field_data
   * base table so it is available in any View that uses Content as its base.
   */
  public function __invoke(array &$data): void {
    $data['node_field_data']['my_module_node_original_title'] = [
      'title' => $this->t('Title (original language)'),
      'help'  => $this->t('The node title rendered in its original (source) language, regardless of the current interface language.'),
      'field' => [
        'id' => 'my_module_node_original_title',
      ],
    ];
  }
}

Step 2 — Implement the Views Field Plugin

Now we need the actual plugin that Views will call to render our field.
All Views field plugins extend FieldPluginBase and are placed in src/Plugin/views/field/.
The key methods we override are:
  • query() — called by Views when building the SQL query. Because our field does not need an extra database column (we retrieve the data from the already-loaded entity), we leave this empty.
  • render() — called for each row in the result set. This is where we retrieve the node entity from the result row and return its untranslated title using getUntranslated()->getTitle().
  • usesGroupBy() — we return FALSE because grouping by a computed pseudo field does not make sense.
<?php

namespace Drupal\my_module\Plugin\views\field;

use Drupal\node\NodeInterface;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\ResultRow;

/**
 * A Views field that always renders the node title in its original language.
 *
 * Useful when Views is displaying a translated context but the original
 * (source) language title is needed regardless of the current UI language.
 */
#[ViewsField('my_module_node_original_title')]
final class NodeOriginalTitle extends FieldPluginBase {

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function query(): void {
    // No extra query needed - the title is fetched from the loaded entity.
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function render(ResultRow $values): string {
    $entity = $this->getEntity($values);

    if (!$entity instanceof NodeInterface) {
      return '';
    }

    return $entity->getUntranslated()->getTitle() ?? '';
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function usesGroupBy(): bool {
    return FALSE;
  }

}

Wrapup

After completing all steps, your module directory should contain these new files alongside your existing ones:
web/modules/custom/my_module/
├── my_module.info.yml                       # updated: added drupal:views dependency
└── src/
    ├── Hook/
    │   └── ViewsData.php                    # new: hook_views_data_alter() implementation
    └── Plugin/
        └── views/
            └── field/
                └── NodeOriginalTitle.php    # new: Views field plugin
Once the field is added to the Content View, editors will see a dedicated column showing the original language title for every node row — regardless of which language the interface is currently set to. Pair it with the standard Title column and the distinction between source content and its translations becomes immediately obvious at a glance, no content-opening required.
A small addition to your codebase, but a meaningful improvement to the daily editorial experience.
❤️ Love on your keyboards.

Sources

Drupalize Me (October 2024) — Define a Custom Views Pseudo Field Plugin.
https://drupalize.me/tutorial/define-custom-views-pseudo-field-plugin
Daniel Sipos (June 2015) — Creating a custom Views field in Drupal 8.
https://www.webomelette.com/creating-custom-views-field-drupal-8

More articles

Author(s)

Akira
Akira

Making of : “La nuit des Losers”

lanuitdeslosers.ch, built fast: paper wireframes, a bold hand-drawn identity, then Claude Code to prototype and ship. A mouse-tracking hero animation, a bowling game made of sponsor logos, and a few bugs we’re keeping.
lanuitdeslosers.ch, built fast: paper wireframes, a bold hand-drawn identity, then Claude Code to prototype and ship. A mouse-tracking hero animation, a bowling game made of sponsor logos, and a few bugs we’re keeping.
Making of : “La nuit des Losers”

Author(s)

Pierre
Pierre

Dark mode: necessity or trend

Nous faisons le tri entre idées reçues, réalités techniques et bonnes pratiques de design.
Nous faisons le tri entre idées reçues, réalités techniques et bonnes pratiques de design.
Dark mode: necessity or trend

Author(s)

Yann
Pierre
Yann and Pierre

Experiment #2 – Surway, a social experiments game

Pour notre deuxième expérimentation, nous avons relevé le défi de sortir de notre zone de confort en développant Surway, un jeu vidéo jouable directement dans le navigateur, où les actions de chaque joueur restent inscrites pour tous ceux qui suivront.
Pour notre deuxième expérimentation, nous avons relevé le défi de sortir de notre zone de confort en développant Surway, un jeu vidéo jouable directement dans le navigateur, où les actions de chaque joueur restent inscrites pour tous ceux qui suivront.
Experiment #2 – Surway, a social experiments game