Defining custom Views handlers or pluginsfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Views is arguably the single largest consumer of Drupal's plugin system — every Views handler type you've configured (fields, filters, sort criteria, argument handlers, access plugins, style plugins) is its own plugin type, discovered exactly the same way as the Blocks and Fields you've already built. This lesson wraps up the Plugins topic by building a custom Views field plugin — one that computes and displays a value no existing Views field can.

What you'll learn in this lesson

  • Why Views handlers are plugins rather than hooks, given how many types there are
  • The @ViewsField annotation and the render() method every field handler needs
  • How hook_views_data() makes a custom field selectable in the Views UI at all

Why Views leans so heavily on plugins

A single View can mix and match dozens of independently-contributed field handlers, filter handlers, and sort handlers — core provides many, and contributed modules add more constantly. That's the exact shape plugins are built for: many interchangeable implementations of a handful of shared concepts (a "field handler," a "filter handler"), discovered automatically so any module can add a new one without Views core needing to know about it in advance.

A custom Views field plugin

namespace Drupal\my_module\Plugin\views\field;

use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\ResultRow;

/**
 * Field handler that displays a computed "days since" value.
 *
 * @ViewsField("my_module_days_since")
 */
class DaysSinceField extends FieldPluginBase {

  public function query() {
    // Intentionally empty: this field computes its value from data
    // already loaded by another field, rather than adding its own
    // column to the query.
  }

  public function render(ResultRow $values) {
    $created = $this->getValue($values, 'created');
    $days = floor((\Drupal::time()->getRequestTime() - $created) / 86400);
    return $this->t('@days days ago', ['@days' => $days]);
  }

}

Notice the annotation format here is different from what you've seen so far — @ViewsField("my_module_days_since") takes the plugin ID directly as a single argument, rather than named properties in parentheses. Views' plugin system predates the more elaborate annotation style used by Blocks and Fields, so several of its handler types use this shorter form. render() is the one method every field handler must implement — it receives one row of query results and returns the markup for that row's cell.

Making the field appear in the Views UI

/**
 * Implements hook_views_data().
 */
function my_module_views_data() {
  $data['node']['my_module_days_since'] = [
    'title' => t('Days since created'),
    'help' => t('Displays how many days ago this content was created.'),
    'field' => [
      'id' => 'my_module_days_since',
    ],
  ];
  return $data;
}

Writing the plugin class alone isn't enough — hook_views_data() is what actually registers this field as selectable in the "Add fields" dialog when someone is building a View, and tells Views which base table (node, here) it applies to. This is one of the few places a hook and a plugin cooperate directly: the hook handles registration and UI presentation, the plugin handles the actual behavior.

An empty query() method is a deliberate, common pattern — not an oversight. Some field handlers need to add a join or a column to the underlying SQL query; others, like this one, only need data another field has already loaded (here, the node's created timestamp). Leaving query() empty tells Views "I don't need anything extra from the database for this field."

Quick check: if two different contributed modules both implemented hook_views_data() adding fields to the node base table, would they conflict? (No — hook_views_data() follows the normal hook pattern of every implementation contributing its own array keys, merged together by Views. As long as each module gives its field a unique machine name, there's no collision, exactly like any other hook that builds up a shared array.)

Key takeaways

  • Views handler types (field, filter, sort, argument, access, style) are each their own plugin type, which is why Views can support so many independently-contributed handlers without core needing to know about them in advance.
  • Some Views plugin annotations (like @ViewsField("id")) use a shorter single-argument form rather than the named-property style seen on Blocks and Fields.
  • A field plugin's render() method returns the markup for one row; an empty query() method is valid when the field only reuses data another field already loaded.
  • hook_views_data() is what makes a Views plugin actually selectable in the Views UI — the plugin class alone isn't enough.

Coming up next

You've now built every major plugin type this tutorial covers — Block, three Field plugins, and a Views plugin — all from the same underlying architecture. Next, the tutorial turns to a different way of reacting to what happens on your site: Drupal's event system, and how it compares to the hooks you learned much earlier.