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
.modulefile 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.
