Using computed fields in Drupal 11 and Views

By Kevin , November 24th, 2025

Every so often I run into something in Drupal that reminds me why I still enjoy working with it after all these years. Recently, while working with the Group module, I needed to display a few pieces of dynamic information: the number of members in each group and a handful of user thumbnails. I wanted these values both on the Group entity itself and inside a Views listing like a “Group Directory” page.

That led me to computed fields, how they behave on entities, how they behave in Views, and why you often need two different implementations even though the logic is the same.

If you’ve never worked with computed fields before this post should help you understand how to use them in Drupal.

What Exactly Is a Computed Field?

A computed field is a field whose value does not come from a user input field. Instead, the value is calculated at runtime. When the entity loads the value is re-computed based on the logic you define.

Different scenarios that can be facilitated by a computed field can be simple:

  • Counting related entities
  • Combining multiple field values
  • Deriving metadata
  • Generating dynamic URLs or arrays

Or they can be more involved, like aggregating external data or building structured JSON for an API. 

Common use cases for Computed Fields

Here are a few situations where computed fields shine:

  • Counts: number of members in a group, number of comments, number of open tasks.
  • Derived text: “Full Name” field, summaries, or combined metadata.
  • Dynamic relationships: listing related entity URLs or IDs without storing them.
  • API-focused data: grouping multiple values into a map so JSON output is easier.
  • Lightweight display-only logic: when storing it in the DB adds unnecessary overhead.

If you don’t need sorting or filtering on the value, a computed field saves effort and avoids extra schema updates.

Pure vs stored Computed Fields

It helps to separate two ideas:

Pure computed field

  • Value is always computed
  • No storage
  • Great for entity displays, JSON, and UI render arrays
  • Cannot be used directly in Views without extra work

Stored computed field

  • Value is calculated and then saved to storage
  • Can be sorted, filtered, indexed
  • Useful for dense views or search indexing
  • Requires reacting to events (save, insert, update)

In this article we’ll focus on pure computed fields since this is where the Group directory example gets interesting. We’re using the Group module to manage groups. We want a list of groups on the site that shows the number of members in the group along with a handful of user pictures of members in that group.

Views and the Entity API do not share computed fields automatically. Each needs its own implementation.

Adding Computed Fields to Views

The Views plugin system expects fields to be backed by database columns, unless you explicitly tell it otherwise. So for a pure computed field, you have to:

  1. Create a Views field plugin
  2. Override query() so Views doesn’t try to query a real column. Leave this method empty.
  3. Provide your value in getValue()
  4. Register the field via hook_views_data_alter() using a virtual table

Let’s walk through the two fields.

Views Plugin: Member Count

The member count plugin will:

  • Use a #[ViewsField] attribute to define the plugin
  • Override query() with an empty method
  • Implement getValue() to return an integer count of members

This makes it easy for Views to include the field without attempting SQL against a non-existent column. The implementation is small:

<?php

namespace Drupal\mymodule\Plugin\views\field;

use Drupal\group\Entity\GroupMembership;
use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\ResultRow;

/**
 * Views field plugin to display a list of group member total.
 */
#[ViewsField('member_count')]
class MemberCount extends FieldPluginBase {

  /**
   * {@inheritdoc}
   */
  public function query() {}

  /**
   * {@inheritdoc}
   */
  public function getValue(ResultRow $values, $field = NULL): int {
    $group = $values->_entity;
    $group_memberships = GroupMembership::loadByGroup($group);
    return count($group_memberships);
  }

  /**
   * {@inheritdoc}
   */
  public function render(ResultRow $values) {
    return $this->getValue($values);
  }

}

Views Plugin: Member Image List

The image list plugin will:

  • Also override query() to avoid SQL
  • Return an array of strings — for example, URLs to styled thumbnails
  • Play nicely with Views REST exports or any JSON serializer

No HTML is required unless you specifically want an HTML output mode. You can keep it as clean data. You can add configuration options to the plugin - in my case, a list of image styles and how many images to show:

Image
Views config

Like the member count field, we get the Group entity from the view, loop the members and add the member image URLs, formatted with the selected image style option from the View configuration.

Virtual Tables: Why They’re Needed

A pure computed field doesn’t exist in the database. By default, Views will try to join and select a column for every field. For computed fields, we don’t want that.

To avoid it, we create a virtual table in hook_views_data_alter():

  • The “table” isn’t real in the database.
  • It defines a table that attaches to the real base table (for example, groups_field_data).
  • It declares the field and sets real field = FALSE.
  • It ensures Views uses the custom field plugin instead of generating SQL for a missing column.

This pattern lets you plug dynamic values into Views in a safe, predictable way. You can use the new OOP Hook(s) in Drupal to add that:

<?php

declare(strict_types=1);

namespace Drupal\mymodule\Hook;

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

/**
 * Performs views data alterations.
 */
#[Hook('views_data_alter')]
class ViewsDataAlter {

  use StringTranslationTrait;

  /**
   * Implements hook_views_data_alter().
   */
  public function __invoke(array &$data): array {
    $data['group__member_count'] = [];

    $data['group__member_count']['table']['join'] = [
      'groups_field_data' => [
        'left_field' => 'id',
        'field' => 'id',
      ],
    ];

    $data['group__member_count']['member_count'] = [
      'title' => $this->t('Member count'),
      'group' => $this->t('Group'),
      'help' => $this->t('Total number of members.'),
      'field' => [
        'id' => 'member_count',
        'real field' => FALSE,
      ],
    ];
  }
  
}

Adding Computed Fields to the Entity

Once the Views side is working, we add the same data as proper entity fields. The goal here is:

  • Use the values directly in templates.
  • Expose them in Manage Display.
  • Make them available to JSON or other normalization layers.
  • Support Layout Builder and any entity renderer.

For the Group example, we’ll add two fields on the entity: a member count and a list of member image URLs.

1. member_count (integer)

The member_count field on the entity:

  • Uses the integer field type.
  • Is marked as computed and read-only.
  • Uses a FieldItemList subclass with ComputedItemListTrait.
  • Implements computeValue() to count group members and populate a single item.

Because it’s an integer field, you can rely on the default integer formatter in Manage Display. It shows up like any other field but is completely derived. Like the Views plugin, the implementation is simple:

<?php

namespace Drupal\mymodule\Plugin\Field;

use Drupal\Core\Field\FieldItemList;
use Drupal\Core\TypedData\ComputedItemListTrait;

/**
 * Computed field for providing a member count.
 */
class MemberCount extends FieldItemList {
  use ComputedItemListTrait;

  /**
   * {@inheritdoc}
   */
  protected function computeValue(): void {
    $entity = $this->getEntity();
    $this->list[0] = $this->createItem(0, count($entity->getMembers()));
  }

}

2. member_image_urls (map of URLs)

For a list of image URLs, a map field type works well:

  • It stores structured data, like ['urls' => ['url1', 'url2', ...]].
  • Is also marked as computed and read-only.
  • Uses a FieldItemList subclass that builds a list of styled image URLs.

Field Formatters and Render Arrays

Even though these are computed fields, they still participate in the normal field system:

  • They show up in Manage Display.
  • They are part of the entity’s render array.
  • They can be accessed in Twig as {{ content.member_count }} or a more specific structure.

This makes them feel like first-class fields, even though they never touch the database.

Why You Need Both

Here’s the key takeaway from all of this:

  • The entity computed field makes your value available on the entity itself – for templates, JSON, Manage Display, Layout Builder, and anything that works with entity fields.
  • The Views computed field makes your value available in Views – for listings, REST exports through Views, and any display built with the Views UI.

Views does not automatically use entity computed fields, and entity rendering does not automatically know about Views-only field plugins. They are two different layers, and each needs its own integration.

In practice, this means:

  • Use a Views field plugin + virtual table for listing pages.
  • Use an entity computed field for entity render arrays, templates, and JSON.
  • Share the underlying business logic so the values stay in sync.

Putting it all together

With both fields defined, I can now use the Views REST Serializer Extra module I developed. This feeds a decoupled front end with all the information they need to build the UI on the page:

Image
JSON

Computed fields give you dynamic values without expanding your storage schema, and when you wire them up on both sides (entity and Views), they feel like a natural part of the system. The trick is understanding that each layer needs its own piece:

  • Entity: computed field definitions and item list classes.
  • Views: field plugins and virtual table definitions.

If you’re building something like a Group directory with member counts and thumbnails, this two-part pattern will give you a consistent experience across entity pages, Views listings, and JSON outputs.