Drupal 7 - Views Handlers... Once Upon a Time! (But Now in Drupal 11)

kenneth

Authored on

Image

I recall a moment when I was working on migrating an old Drupal 7 custom Views filter into a Drupal 11 module. The filter was called superadmin_handler_filter_users_access_name.inc — yes, that long — and the goal was simple: bring it back to life inside a custom module called kbc_tweaks.

Simple, right?


The Idea Behind the Filter

The use case is actually pretty neat. Imagine you have an internal directory — something like a /team page — outside the admin area. You only know the email domain (e.g. @keboca.com) and you want to list only the users who have the Administrator role and whose email matches that domain.

That's what this "Access Name" filter does. It searches by email for administrators, and by username for everyone else. So if you drop @keboca.com in the filter, only admins with that domain in their email show up. Non-admins would only appear if their username literally contained that string — which, let's be honest, is pretty rare.


Setting Up the Module

First things first. The kbc_tweaks module lives under modules/custom/kbc_tweaks/ — just an info.yml and the plugin file. No .module file needed.

name: 'KBC Tweaks'
type: module
description: 'Site-specific tweaks and customizations.'
core_version_requirement: ^10 || ^11
package: keboca

A fun fact here: I initially assumed I needed a .module file to register kbc_tweaks.views.inc via hook_views_data_alter(). Turns out, Drupal core loads MODULE.views.inc automatically through the Views hook group (HookCollectorPass + hook_hook_info()). So I removed the .module file completely. Less files, less noise. Happy days.

The UsersAccessName plugin itself extends StringFilter, uses a single "Contains any word" operator, and is registered through that views.inc alter. Also worth noting: in Drupal 11 we use the PHP 8 attribute style for the plugin declaration — instead of the old annotation approach. Small but important detail.

#[ViewsFilter("users_access_name")]
class UsersAccessName extends StringFilter {
  // ...
}

How the Query Works

The heart of the plugin is the opContains() method. Instead of doing a straightforward field search, it builds a SQL CASE expression that decides what to match depending on whether the user has the Administrator role or not:

protected function opContains($field) {
  /** @var Sql $query */
  $query = $this->query;
  $admin_placeholder = $this->placeholder();
  $like_placeholder  = $this->placeholder();
  $expression = $this->buildExpression($admin_placeholder, $query);

  $query->addWhereExpression(
    $this->options['group'],
    "({$expression}) LIKE {$like_placeholder}",
    [
      $admin_placeholder => $this->getAdministratorRoleId(),
      $like_placeholder  => '%' . $this->connection->escapeLike($this->value) . '%',
    ]
  );
}

And the CASE expression itself — the piece that does the heavy lifting — looks like this:

protected function buildExpression(string $admin_placeholder, Sql $query): string {
  $table_alias  = $this->tableAlias;
  $roles_table  = $query->getConnection()->prefixTables('{user__roles}');

  return "CASE WHEN {$table_alias}.uid IN (
    SELECT entity_id FROM {$roles_table}
    WHERE roles_target_id = {$admin_placeholder}
  ) THEN {$table_alias}.mail
    ELSE {$table_alias}.name
  END";
}

If the user's uid is found in the user__roles table with the administrator role, it matches against mail. Otherwise it matches against name. Clean and contained in a single expression.

For reading the Administrator role ID, rather than hardcoding anything, the plugin iterates over all user.role.* configs and retrieves the one with is_admin: true:

protected function getAdministratorRoleId(): string {
  $prefix = 'user.role.';
  foreach ($this->configFactory->listAll($prefix) as $config_name) {
    $role_id = substr($config_name, strlen($prefix));
    $config  = $this->configFactory->get($config_name);
    if ($config->get('is_admin') === TRUE) {
      return $role_id;
    }
  }
  return '';
}

This reads from active configuration only — so the role settings need to be saved or imported for this to pick them up correctly.


Putting It All Together

Once everything was in place, the setup in a View is pretty straightforward:

  1. Create or use a View with User as the base table.
  2. Add the Access name filter (from KBC Tweaks, User group) and expose it. Set a default like @keboca.com or leave it open.
  3. Drop the view on a non-admin path like /team and set permissions accordingly.

Result: only administrators whose email matches the domain show up. Exactly what we needed.


It was a fun one to work through. There was just enough friction to keep things interesting, and the end result is a tidy little plugin that handles a real-world scenario you won't find a ready-made solution for in contrib.

I hope this saves you some time if you ever end up in a similar situation!

It was a fun one to work through — and as always, the best documentation is the code you had to debug yourself. 
Happy coding!