New way of implementing hooks in Drupal 10

Drupal 10

As you know hooks are one of the ways to interact with Drupal core and contributed modules, when some things happen in the system, e.g. a user logs in/out, a node is created, updated, or deleted, etc.

Starting from v.8 the procedural approach in Drupal has been replaced by an object-oriented approach. It's built on top of the Symfony framework and already has used the EventDispatcher Component which is the Symfony way of allowing components to interact with each other. This serves the same purpose as the hook system. So in the Drupal core, there are two parallel systems that provide the ability for components to communicate with each other - hooks and events.

Hooks

The hooks inherited from older versions are still present in Drupal 10. Drupal core does not provide events that we can use to replace hooks. 

Actually the hooks system uses procedural approach and it's a rudiment in OOP. And in order to use powerful Symfony tool as services and dependency injection in procedural code we need to use the \Drupal class, which is a wrapper for a static service container.

Fortunately there are several ways to reduce size of the .module file (or even get rid of it) and eliminating many calls to \Drupal with dependency injection in hooks.

Events

Please see description of this approach in the article "Events system in Drupal" and how it can be used to replace hooks here.

Advantages of this approach:

  • Easier to determine the sequence of events.
  • Events can prevent the execution of subsequent events.
  • Ability to define listeners dynamically.

Also there is a contrib module Hook Event Dispatcher which implements the events approach and provides events for several Drupal core and module hooks.

Please use it with care and check list of known issue before the decision.

Drupal::classResolver

Another way is using the ClassResolver hook pattern similarly to Drupal core does in .module files in Content Moderation, Layout Builder, and Workspaces module in order for core hooks to be overridable and partially embrace Dependency Injection.

This example code will implement a hook bridge for hook_views_query_alter(). 

Into example_module.module file:

/**
 * Implements hook_views_query_alter().
 */
function example_module_views_query_alter(ViewExecutable $view, QueryPluginBase $query) {
  /** @var \Drupal\example_module\ExampleViewsHooks $views_hooks */
  $views_hooks = \Drupal::classResolver(ExampleViewsHooks::class);
  $views_hooks->viewsQueryAlter($view, $query);
}

A new file "example_module/src/ExampleViewsHooks.php":

<?php

namespace Drupal\example_module;

use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Defines a class for views hooks.
 *
 * @internal
 */
class ExampleViewsHooks implements ContainerInjectionInterface {

  /**
   * The entity type manager service.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;
  
  /**
   * The HTTP request stack.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;
  
  /**
   * Constructs a new ExampleViewsHooks instance.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager service.
   * @param \Symfony\Component\HttpFoundation\RequestStack $stack
   *   The current HTTP request stack.
   */
  final public function __construct(
    EntityTypeManagerInterface $entity_type_manager,
    RequestStack $stack
  ) {
    $this->entityTypeManager = $entity_type_manager;
    $this->requestStack = $stack;
  }
  
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('request_stack')
    );
  }
  
  /**
   * Implements a hook bridge for hook_views_query_alter().
   *
   * @see hook_views_query_alter()
   */
  public function viewsQueryAlter(ViewExecutable $view, QueryPluginBase $query) {
    // Altering views query.
  }
  
}

The functions "__construct" and "create" were added as an example of using dependency injection.

Hux

Also there is a contrib module Hux which provides hook implementations without needing to define a .module file. It takes advantage of improving of the Drupal core where almost all hook invocations are dispatched via the ModuleHandler service. This now allows third party projects to supplement ModuleHandler via the service decorator pattern.

An example of using:

namespace Drupal\example_module\Hooks;

use Drupal\hux\Attribute\Alter;

/**
 * Example hooks.
 */
final class ExampleHooks {
  /**
   * Implements hook_entity_access().
   */
  #[Hook('entity_access')]
  public function myEntityAccess(EntityInterface $entity, string $operation, AccountInterface $account): AccessResult {
    return AccessResult::neutral();
  }

}

A new file should be placed in the example_module/src/Hooks folder and it'll be found automatically. A PHP annotation is used to define a hook. 

Also if you want to use dependency injection this class should implement ContainerInjectionInterface.

Unfortunately Hux can not work with templates and the preprocess functions have to be defined as usual.

Hux has been rarely used at the time of writing this article, only 302 sites reported using it. Please use it with care and check list of known issue before the decision.

Conclusion

Essentially, all these ways are an attempt to fix the Drupal architecture. But if we want to improve interaction with Drupal core and contributed modules and cut off hooks, it has to be done in the Drupal core. But for now described above approaches are admissible ways for the improvement.

The main problem with using Drupal::classResolver and Hux is they still have to call hooks. If hooks are dropped tomorrow, they will be useless. Also I prefer to not install extra contrib modules Hook Event Dispatcher or Hux with their overhead and added complexity. For myself, I decided to implement events in developing of new modules and using Drupal::classResolver for the current hooks.

References