Skip to main content
Joachim's blog

Main navigation

  • Home
  • About
  • Hire me

Blog

Putting paths first: Views support for path alias entities

By joachim, Thu, 10/09/2026 - 12:56

On a recent project, I was struck by how the content editors used spreadsheets to manage their content. This wasn't just as a planning tool in the early stages; they had spreadsheets which essentially mirrored the content in Drupal, and this is where they kept track of who was responsible for sign-off on a piece of content, what stage it was at, and so on.

Leaving aside for a future post or many the matter that if editors are needing to use spreadsheets to manage their content, it doesn't speak well of the 'M' part of the CMS and that there are surely things we need to improve in Drupal, one thing I noticed was how content was listed.

The main identifier of content, the first column in the spreadsheet, wasn't the title, or the node ID, but the path. To these editors, the path was the starting point, it represented that piece of content.

This is completely add odds with how Drupal treats paths. Paths in Drupal are like an afterthought: tacked onto entities as second-class citizens. They're not even shown in the content admin pages, you're left to discover them for yourself by hovering over the content link.

But what if we could change that, and put path aliases first? What if we could make Drupal list content in the same way that these users have in their spreadsheets? It would be a first step in providing the sort of content overview and administration tools that Drupal is currently lacking for these users. Other things are needed too, such as a sign-off user content, and more complex statuses. But paths are the starting point.

Unfortunately, the flexibility of Drupal's path system is actually a problem here: paths aren't just for entities, and an alias can be for any system path. So there's no connection in the database from the path alias to the entity it points to, and there's nothing on the entity either: the path alias field you see on a node is a computed value, obtained by querying the path_alias table for the node's canonical path.

Of course, this doesn't stop SQL: you can join two tables on anything, and Views provides a 'join' plugin type precisely for these sorts of weird cases.

So we can define Views relationships from the path_alias entity type to all content entity types, using our special join plugin. The join clause then looks like this for nodes:

... JOIN node
ON SUBSTRING(path_alias.path FROM 1 FOR 5) = 'node/'
AND SUBSTRING(path_alias.path FROM 6) = node.nid

What that's saying is that we only join if the path alias's real path starts with 'node/' and we join the numeric suffix to the node table row of the same value. We need the first part so that we don't join an alias for 'media/42' to node 42.

With this, we can make an admin view of nodes listed by their path aliases, with all the same features as the default Content admin view:

admin view of nodes with path aliases

And the wrapped up result is the Path Alias Views module, which provides the integration to show path aliases in Views: fields, sort orders, and filters.

The Views relationship is generalised to work for any content entity type that has a canonical path of the form 'something/ID'. If your custom entity type does something weird with its canonical paths (such as include the ID of a parent entity; I've done that myself with custom entity types), then you can use hook_views_data_alter() to change the relationship from path_alias entities to your entities, so that it uses your own custom Views join plugin.

The view shown in the screenshot is installed as default config, and if you also install the Client-side Hierarchical Select module, a more souped-up path component filter allows you to select the path prefix to filter on one path component at a time.

CSHS filter for path alias components

This can be used as a drop-in replacement for the default core Content Admin view, though of course it will only show nodes that have a path alias.

I'm sure there are other ways of showing paths. Replacing the URL aliases admin page with a view would be one. Reverse relationships from entity types to path aliases would open up other possibilities too. I'll be interested to see what people come up with this: do please let me know on Mastodon, in Slack, or in MRs in the issue queue.

Do you need help with doing something unspeakably twisted with Views queries? I've had plenty of experience with this sort of customisation, and I'm available for hire - contact me!

Tags

  • contrib module
  • views

Making Entity Pager module API-first

By joachim, Mon, 17/08/2026 - 16:45

It's funny how big ideas start.

I am doing some maintenance work on the Entity Pager module, which has been fixing bugs, improving the tests, and so on. And because Computed Field is also a module I maintain, and because I have at the back of my mind the idea of finding more use cases for it, I had the thought that I could add support for Computed Fields to Entity Pager.

Specifically, this would mean that Entity Pager would allow you to add computed fields to your entity type, which would be computed entity reference fields to the previous and next entities in the pager. As well as allowing you to output the previous and next links with more flexibility, and within the rendered entity rather than in a block, it would open up having these links in JSON:API (though there's a bug to fix still).

So, then, quite a good use case!

It does, however, require a bit of re-plumbing inside the EntityPager class. Currently, EntityPager, expects to be instantiated within the theming for an executed view. Our computed field needs a new API which it would call with the basic data (the view ID, display ID, and current entity), and that would take care of executing the view, extracting the data from the result, and returning it.

I suppose I could make a whole new pathway, but a lot of the code that would need is in EntityPager so it makes more sense to me to change that to allow both cases. This means adding a new way of constructing it from the factory service, and then executing the view if necessary.

So then we'd have an API for getting the previous and next entities. And that's where the big idea suggests itself: what if we used this API for everything?

Currently, the rendered entity pager is a specially-themed view. We define a custom Views style plugin, and that uses our theme template 'entity_pager' for its theming. We use the Views block system to show the pager. By the time our code is involved, the view has already been executed, and all of our code is taking place within the Views theming. This makes it tricky to do things like hiding the pager completely.

But... once we have an API, we could totally invert this. We could define a custom render element which outputs the pager. This would take the view ID and display ID as properties, and the current entity if you have it (and continue to detect it from the current route if you don't). Like this:

$build['pager] = [
  '#type' => 'entity_pager',
  '#view_id' => 'my_pager_view',
  '#view_display_id' => 'my_display',
];

This render element would then be in charge of executing the view, and getting the data it needs from the view's result. You'd still store settings for the pager on the view's style plugin, but we'd no longer rely on the theming of that — the view would just be used as a data source. The render element would have similar theming to the Views style — it could pretty much use the same Twig template. The block we provide would change to being a completely custom block plugin, which would output the pager element.

To me, this seems like a cleaner structure. Our pager is a separate render element, and our code no longer runs inside Views rendering, which feels a little bit convoluted and fragile.

If you use Entity Pager, what do you think? Would this make your use of Entity Pager simpler, more complex, or not affect you at all? It would be a big change to the module, so I'd love to hear opinions on the issue for this, as I'm still undecided about it.

Do you need help with updating a contrib module, refactoring it, or expanding its capabilities? I'm available for hire - contact me!

Tags

  • contrib module
  • Entity Pager

No data, no problem: computed fields made simple

By joachim, Sun, 05/07/2026 - 16:02

It's often useful to let the machines do the work, and output something that's dynamically computed on an entity. By that I mean something that can't be hardcoded as a fixed value for all entities of a particular type, but that varies for each entity, in a way that allows it to be generated in code rather than laboriously entered into each entity form by humans.

For example, you might want a backlink for an entity reference, or a link to a view that has an argument for the entity's ID, or something that depends on field values on the entity.

There are several ways in Drupal of putting something dynamic on the entity's display output. You can of course add something to the build array yourself, in either the entity's view handler or hook_entity_view(). The extra fields system lets you then declare your additional build array item with hook_entity_extra_field_info() which allows it to be rearranged among normal fields in the field admin UI.

This is okay, but the extra fields system is Drupal 5-era stuff. Your piece of build array is just that, some render stuff; it can't participate in any data structures and nothing else will recognise it and work with it.

A better approach is a computed field. This involves a little more boilerplate code than the extra fields technique, but there are several benefits.

The first is that you are defining a field value, not a render array, and you get access to all the field formatters that apply to your type of data. So for example, if your computed data is a URL, you get all of the link field formatters at your disposal, in core and contrib.

The second is that anything that works with fields will be aware of your computed field. So you can add it to a view as a field (though not a sort or filter of course, since it has nothing in the database). You can add it to a SearchAPI index (and there, you actually can filter on it, because SearchAPI will index the computed value into its backend).

The code

Here's what you need to do. You need two things:

  1. A FieldItemList class.
  2. A declaration of your field in an entity class or field info hook.

Unlike declaring code fields, you don't need to declare a field storage: that's because a computed field doesn't store anything!

1. The FieldItemList class

Create a subclass of \Drupal\Core\Field\FieldItemList that uses \Drupal\Core\TypedData\ComputedItemListTrait. In this class, all you need to do is implement computeValue() to return your data.

<?php

// The namespace doesn't matter, but I like to put it under \Field.
namespace Drupal\my_module\Field;

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

/**
 * Field item list class for my computed field.
 */
class MyFieldFieldItemList extends FieldItemList {

  use ComputedItemListTrait;

  /**
   * {@inheritdoc}
   */
  protected function computeValue() {
    // You have access to the complete entity, so you can use other field
    // values.
    $entity = $this->parent->getValue();

    // Create a field item for your data. You can create more than one for a
    // multi-valued field.
    $this->list[] = $this->createItem(0, [
      'value' => 'cake',
    ]);
  }
}
?>

For most field types, the key to use in the item array is 'value', but some more specialised fields use something else. You can find this by looking in the field item class for the field type. For example, in \Drupal\link\Plugin\Field\FieldType\LinkItem::propertyDefinitions() you can see that for a link field, you need these array keys:

    $this->list[] = $this->createItem(0, [
      'title' => $this->t('My link title'),
      'uri' => $some_url_that_we-compute->toUriString(),
    ]);

2. Define the field

For the field definition, there are two things to consider:

  • Is it on an entity type you control, or somebody else's?
  • Do you want a base field or a bundle field?

If it's your own entity type, you define the field in the entity class, in either the baseFieldDefinitions() or bundleFieldDefinitions() method. If it's an existing entity type, you need to use hook_entity_base_field_info() or hook_entity_bundle_field_info().

In all cases, the code is broadly similar. For a base field, it looks like this:

/**
 * Implements hook_entity_base_field_info().
 */
#[Hook('entity_base_field_info')]
function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
  if ($entity_type->id() == 'node') {
    $fields = [];
    $fields['my_computed_field'] = BaseFieldDefinition::create('link')
      ->setLabel($this->t('My computed field'))
      ->setDescription($this->t('My field is amazing.'))
      // This declares it as a computed field.
      ->setComputed(TRUE)
      // This is the class you created earlier, which provides the values.
      ->setClass(MyFieldFieldItemList::class)
      // Optional default view display options, which can be overriden in the admin UI.
      ->setDisplayOptions('view', [
        'label' => 'above',
        'type' => 'link',
        'weight' => '0',
      ]);

    return $fields;
  }
}

For a bundle field, you need the Drupal\entity\BundleFieldDefinition class from Entity module, and a few extra things need to be explicitly set on the definition because the field system doesn't handle them for you:

    $fields['my_computed_field'] = BundleFieldDefinition::create('link')
      ->setName('my_computed_field')
      ->setTargetEntityTypeId($entity_type_id)
      ->setTargetBundle($bundle)
      // Rest of the definition as above.

See my earlier blog post on bundle fields for more about their uses and their quirks.

The contrib module

If you want to do it all with less boilerplate, or your computed field is something that's reusable across different entity types, consider the Computed Field module as an alternative to the code examples above. Instead of a Field class, the computational code does in a plugin, which the module then makes available in an admin UI where you can create and edit computed fields alongside the usual stored fields.

And if your computed field is purely a render array rather than data, the Computed Field module also provides a computed_render_array field type for that, with an accompanying field formatter.

Do you need help with data structures, and their integration with Views or SearchAPI? I'm available for hire - contact me!

Tags

  • Field API
  • Drupal core

Speed up your PHPUnit Browser tests with this one trick

By joachim, Wed, 01/04/2026 - 08:14

It's true, no April fools. You can make your Browser tests run much quicker. How? By deleting them!

You will of course need to add a corresponding Kernel test - and that's the trick. Kernel tests run much faster than Browser tests.

But Browser tests make requests to the test site using an internal web browser, I hear you say, whereas Kernel tests make API calls directly. Kernel tests have their uses for testing APIs, but Browser tests are needed to test actual HTML output.

Aha! Kernel tests can now make HTTP requests.

This is subject to a number of caveats and limitations: there is no session, and forms can't be submitted. And functionality such as a current user, blocks on the page, and page caching will need additional setup.

And more generally, with Kernel tests, modules are enabled but not installed: you need to handle things like entity schemas, database tables, and install config yourself in the test. The benefit though is that you only set up the parts of the module that you need for your test.

So not all Browser tests are suitable for conversion. But a lot of them are. We're already working on converting tests in core, and as this feature has been backported to Drupal core 11.x, contrib modules can make use of it too.

The benefits to conversion are tests that run faster, so less time developing and less time waiting for CI pipelines to run, and a lower energy footprint and lower costs for drupal.org. And they're easier to debug too.

And if you haven't yet written any tests for your module, now is an excellent time to start!

Do you need help with writing PHPUnit tests, or getting started with test-driven development? I'm available for hire - contact me!

Tags

  • tests

New Module Builder documentation site

By joachim, Thu, 19/03/2026 - 12:45

Module Builder now has its own documentation site.

This covers the many options it offers developers for fine-tuning their module code, from dependency injection to plugin inheritance, entity base fields, form elements, permissions, library asset files, and more.

Meanwhile, the latest release of Module Builder adds a feature I've wanted to implement for a very long time: when a new form section is added to add a new component (such as a plugin, hook class, or entity type), the form scrolls up to the new section that's just been added with AJAX. This makes it much clearer to understand what's just been changed, and helps with navigating around Module Builder's forms.

Tags

  • module builder

Pagination

  • Current page 1
  • Page 2
  • Page 3
  • Page 4
  • Page 5
  • Page 6
  • Page 7
  • Page 8
  • Page 9
  • …
  • Next page
  • Last page
Blog

Frequent tags

  • Drupal Code Builder (9)
  • module builder (7)
  • git (7)
  • 6.x (5)
  • Drupal core (5)
  • contrib module (4)
  • drupal commerce (4)
  • Field API (4)
  • Composer (3)
  • contributing code (3)
  • Drush (3)
  • Entity API (3)
  • tests (3)
  • development (3)
  • patching (3)
  • Rector (3)
  • modules (2)
  • roadmap (2)
  • maintaining projects (2)
  • debugging (2)
  • drupal.org (2)
  • code style (2)
  • deprecation (2)
  • 7.x (2)
  • views (2)
  • issue queue (2)
  • multisite (2)
  • wtf (2)
  • developer tools (2)
Powered by Drupal