New Way of Writing Hooks in Drupal 11 Explained

Write hooks with PHP attributes—faster, testable, and ready for dependency injection
New Way of Writing Hooks in Drupal 11 Explained

With the release of Drupal 11.1, a new way of writing hooks has been introduced. Until now, hooks were always written procedurally, but they can now be implemented using attributes in PHP classes. This article explains what exactly is changing, the benefits it brings, the different ways it can be applied, and how to use the new approach while maintaining backward compatibility.

Benefits

Writing hooks in an object-oriented way offers several advantages. Below are the main benefits with a brief description:

  • Autoloading: The .module file is no longer scanned, and methods are automatically detected via attributes, improving bootstrap time.
  • Dependency Injection: Since hooks are now object-oriented, it is possible to inject services.
  • Testability: Hooks are now in classes, making them unit-testable without bootstrapping Drupal.
  • Organization: You can place the hooks you add in different classes, allowing for better organization. For example, you can group all node-related hooks in a single class.

One of the major advantages of the new OOP hooks in Drupal 11 is how autoloading works. Previously, all .module files were loaded by default, even if they contained only a few hooks that weren’t actually needed. This wasted unnecessary time. Now, it works smarter: thanks to PSR-4 autoloading, classes are only loaded when they are actually used. Fewer files for your system to open means a faster site startup. For a single module, the gain might seem marginal, but those running many modules will quickly see the benefits add up.

Implementing the Hook Attribute

Example 1: Adding a library to all forms.

<?php
declare(strict_types=1);
namespace Drupal\custom_module\Hook;
use Drupal\Core\Hook\Attribute\Hook;
/**
 * Module hooks for altering forms.
 */
class CustomModuleFormHooks {
  public function __construct() {}
  #[Hook('form_alter')]
  public function formAlter(&$form, &$form_state, $form_id): void {
    $form['#attached']['library'][] = 'custom_module/form';
  }
}

Example 2: Adding a library to one specific form.

<?php
declare(strict_types=1);
namespace Drupal\custom_module\Hook;
use Drupal\Core\Hook\Attribute\Hook;
/**
 * Module hooks for altering forms.
 */
class CustomModuleFormHooks {
  public function __construct() {}
  #[Hook('form_node_article_form_alter')]
  public function formAlter(&$form, &$form_state, $form_id): void {
    $form['#attached']['library'][] = 'custom_module/form';
  }
}

Example 3: Adding a library to two forms.

<?php
declare(strict_types=1);
namespace Drupal\custom_module\Hook;
use Drupal\Core\Hook\Attribute\Hook;
/**
 * Module hooks for altering forms.
 */
class CustomModuleFormHooks {
  public function __construct() {}
  #[Hook('form_node_article_form_alter')]
  #[Hook('form_node_article_edit_form_alter')]
  public function formAlter(&$form, &$form_state, $form_id): void {
    $form['#attached']['library'][] = 'custom_module/form';
  }
}

Different Approaches

It is also possible to place the hook attribute on the class itself, specifying the method in the hook attribute:

<?php
declare(strict_types=1);
namespace Drupal\custom_module\Hook;
use Drupal\Core\Hook\Attribute\Hook;
/**
 * Module hooks for altering forms.
 */
#[Hook('form_alter', method: 'formAlter')]
class CustomModuleFormHooks {
  public function __construct() {}
  public function formAlter(&$form, &$form_state, $form_id): void {
    $form['#attached']['library'][] = 'custom_module/form';
  }
}

If you use __invoke, you can omit the method:

<?php
declare(strict_types=1);
namespace Drupal\custom_module\Hook;
use Drupal\Core\Hook\Attribute\Hook;
/**
 * Module hooks for altering forms.
 */
#[Hook('form_alter')]
class CustomModuleFormHooks {
  public function __construct() {}
  public function __invoke(&$form, &$form_state, $form_id): void {
    $form['#attached']['library'][] = 'custom_module/form';
  }
}

Ordering Hooks

Since Drupal 11.2, hooks also have an order argument. This can be used to order hook implementations:

// Execute hook first.
#[Hook('some_hook', order: Order::First)]
// Execute hook last.
#[Hook('some_hook', order: Order::Last)]
// Execute hook before another module’s implementation.
#[Hook('some_hook', order: new OrderBefore(['other_module']))]
// Execute hook after another module’s implementation.
#[Hook('some_hook', order: new OrderAfter(['other_module']))]

Backward Compatibility

If you want to apply this new way of writing hooks and support Drupal 10.1 and 11.0, it is recommended to implement the new hooks class and manually register it as a service. Then, it can be added as a procedural shim implementation.

Suppose you have written a hook in the new way:

<?php
namespace Drupal\custom_module\Hook;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Entity\EntityInterface;
class CustomModuleHooks {
  #[Hook('node_create')]
  public function nodeCreate(EntityInterface $entity) {
    // Your custom code.
  }
}

Register it as a service:

services:
  Drupal\custom_module\Hook\CustomModuleHooks:
    class: Drupal\custom_module\Hook\CustomModuleHooks
    autowire: true

Then create a procedural shim:

<?php
use Drupal\Core\Hook\Attribute\LegacyHook;
use Drupal\custom_module\Hook\NodeHooks;
use Drupal\Core\Entity\EntityInterface;
#[LegacyHook]
function custom_module_node_create(EntityInterface $entity) {
  return \Drupal::service(NodeHooks::class)->nodeCreate($entity);
}

Preprocess Hooks

In Drupal 11.2 (or at least 11.1.8), preprocess hooks are also supported in the new OOP way. However, this can only be used in modules; preprocess hooks in themes remain procedural.

Hooks That Remain Procedural

  • hook_hook_info()
  • hook_module_implements_alter()
  • hook_install()
  • hook_install_tasks()
  • hook_install_tasks_alter()
  • hook_post_update_NAME()
  • hook_requirements()
  • hook_schema()
  • hook_uninstall()
  • hook_update_last_removed()
  • hook_update_N()

Good to Know

The old procedural hooks still work. Drupal 11.1 and higher support both methods side by side. For new code, we recommend using the OOP approach, but existing modules do not need to be rewritten immediately.

Drupal has added a Hook Service Collector in core. During container build, it scans all services for the Hook attribute. As a developer, you only need to place your class in the correct directory and namespace and use autowire: true. Drupal takes care of the rest.

It is even possible to have multiple classes within one module implement the same hook, or to influence execution order using the new order parameter. Preprocess hooks can also be used in this new way starting from Drupal 11.2, but only in modules.

What Does This Say About the Future?

This step aligns perfectly with Drupal’s direction: more modern, object-oriented, and developer-friendly. Attributes and services make the code cleaner, testable, and more enjoyable to work with.

Practical Links


Editor’s Note: This article is an authorized English translation of the original Dutch publication by Emble, authored by Teade Geertsma. The original Dutch version is available at Emble.nl. The content and code samples are reproduced with permission to ensure authenticity for the global Drupal community.

Reference: Nieuwe manier van hooks schrijven in Drupal 11 uitgelegd, Emble (10 September 2025)

Note: The vision of this web portal is to help promote news and stories around the Drupal community and promote and celebrate the people and organizations in the community. We strive to create and distribute our content based on these content policy. If you see any omission/variation on this please reach out to us at #thedroptimes channel on Drupal Slack and we will try to address the issue as best we can.

Related Organizations

Upcoming Events

Latest Opportunities