Starting from Drupal 11.1, Drupal introduces a new way to implement hooks using PHP attributes.
Although procedural hooks remain fully supported, Drupal now allows hook implementations to be defined inside classes under a module Hook namespace and registered using the #[Hook()] attribute.
The new approach allows hook implementations to be written inside a class:
namespace Drupal\my_module\Hook;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Hook\Attribute\Hook;
final class EntityHooks {
/**
* Implements hook_entity_insert().
*/
#[Hook('entity_insert')]
public function entityInsert(EntityInterface $entity): void {
// Custom logic here.
}
}Hook classes are placed in the src\Hook directory, giving them the namespace Drupal\my_module\Hook. A class containing hook implementations is automatically registered as an autowired service and you don't need a *.services.yml entry for Drupal to discover your hooks.
The first parameter to the #[Hook()] attribute is the short hook name, that is, with the 'hook_' prefix removed.
The three ways to place the attribute
On a method (the recommended approach).
namespace Drupal\my_module\Hook; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Hook\Attribute\Hook; class EntityHooks { /** * Implements hook_entity_insert(). */ #[Hook('entity_insert')] public function entityInsert(EntityInterface $entity): void { // Custom logic here. } }On a class, naming the method.
namespace Drupal\my_module\Hook; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Hook\Attribute\Hook; /** * Implements hook_entity_insert(). */ #[Hook('entity_insert', method: 'entityInsert')] class EntityHooks { /** * Implements hook_entity_insert(). */ public function entityInsert(EntityInterface $entity): void { // Custom logic here. } }On a class with an __invoke() method.
If you omit method, Drupal calls __invoke().
namespace Drupal\my_module\Hook; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Hook\Attribute\Hook; /** * Implements hook_entity_insert(). */ #[Hook('entity_insert')] class EntityHooks { /** * Implements hook_entity_insert(). */ public function __invoke(EntityInterface $entity): void { // Custom logic here. } }
It is recommended to use method attributes and avoid the class-level forms, which can be confusing in larger classes. It's good practice to group related hooks by responsibility. For example, a single class CronHooks.php to implement hook_cron(), and a class TokenHooks.php for both hook_token_info() and hook_tokens().
This helps keep hook implementations organised and easier to maintain.
Stacking attributes
A single method can implement several hooks by stacking attributes. This can solve the problem where insert and update hooks was identical and had to be duplicated.
namespace Drupal\my_module\Hook;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Hook\Attribute\Hook;
class EntityHooks {
/**
* Implements hook_entity_insert().
*/
#[Hook('entity_insert')]
#[Hook('entity_update')]
public function entityInsertOrUpdate(EntityInterface $entity): void {
// Custom logic here.
}
}Hooks execution order
By default, hooks execute by module weight and then alphabetically by module name.
And the only way to influence the order of hook execution was module weight or the hook_module_implements_alter().
Starting from Drupal 11.2 there are new options to override this: the order argument on #[Hook], the #[ReorderHook] and #[RemoveHook] attributes.
The order argument on #[Hook]
1. `Order`
Order::First and Order::Last are operations that move the hook implementation to the first or last position of hooks at the time the order directive is executed.
use Drupal\Core\Hook\Order\Order;
#[Hook('custom_hook', order: Order::First)]
public function customHookFirst(): void {}
#[Hook('custom_hook', order: Order::Last)]
public function customHookLast(): void {}
2. `OrderBefore` and `OrderAfter`
These two classes provide relative ordering against named modules or against specific classes and methods:
- modules: an array of module names ['module_1', 'module_2'] to order before or after.
- classesAndMethods: an array of [ClassName::class, 'methodName'] pairs for finer-grained targeting.
use Drupal\Core\Hook\Order\OrderBefore;
use Drupal\Core\Hook\Order\OrderAfter;
// Run before the views and field modules.
#[Hook('entity_type_alter', order: new OrderBefore(['views', 'field']))]
public function beforeSpecificModules(array &$entity_types): void {}
// Run after node and user modules.
#[Hook('entity_type_alter', order: new OrderAfter(['node', 'user']))]
public function afterSpecificModules(array &$entity_types): void {}
// Run 'entity_type_alter' before two methods.
#[Hook('entity_type_alter', order: new OrderBefore(
classesAndMethods: [
[Class1::class, 'someMethod1'],
[Class2::class, 'someMethod2'],
]
))]
public function beforeSpecificMethods(array &$entity_types): void {}
Acting on other modules: #[ReorderHook] and #[RemoveHook]
These attributes replace the hook_module_implements_alter(), letting one module influence the hooks of another.
1. #[ReorderHook]
It reorders an hooks implementation that already exists in another module.
The reordering is applied after any ordering defined on the #[Hook] attribute itself!
use Drupal\Core\Hook\Attribute\ReorderHook;
use Drupal\Core\Hook\Order\Order;
use Drupal\Core\Hook\Order\OrderBefore;
use Drupal\content_moderation\Hook\ContentModerationHooks;
// Re-order the execution of the 'entity_presave' hook so that
// Content Moderation module hook executes before the Workspaces module hook.
#[ReorderHook('entity_presave',
class: ContentModerationHooks::class,
method: 'entityPresave',
order: new OrderBefore(['workspaces'])
)]
public function reorder(): void {
// The method body itself can be empty. The attribute does the work.
}
/**
* Implements hook_entity_presave().
*/
#[Hook('entity_presave', order: Order::First)]
#[ReorderHook('entity_presave',
class: ContentModerationHooks::class,
method: 'entityPresave',
order: new OrderBefore(['workspaces'])
)]
public function entityPresave(EntityInterface $entity): void {}2. #[RemoveHook]
It allows to skip the execution of a hook implemented by another module.
use Drupal\Core\Hook\Attribute\RemoveHook;
use Drupal\layout_builder\Hook\LayoutBuilderHooks;
// Remove the 'help' hook of the Layout Builder module.
#[RemoveHook('help',
class: LayoutBuilderHooks::class,
method: 'help',
)]
public function removeLayoutBuilderHelp(): void {}
Themes: hooks restrictions (Drupal 11.3)
- The #[Hook] attribute in themes does not support the order or module parameters.
- Themes do not support #[ReorderHook] or #[RemoveHook].
- The order of hook implementations in themes cannot be modified.
- Base theme hooks run before the active theme.
Hooks that must remain procedural
Please note that not every hook can be converted. The following must remain procedural, and attempting to implement them via #[Hook] will not work.
Legacy meta hooks:
- hook_hook_info()
Install and update hooks:
- hook_install()
- hook_install_tasks()
- hook_install_tasks_alter()
- hook_post_update_NAME()
- hook_removed_post_updates()
- hook_schema()
- hook_uninstall()
- hook_update_dependencies()
- hook_update_last_removed()
- hook_update_N()
Full documentation of the Drupal 11 hook system, including supported attribute-based hooks and procedural-only hooks, is available here:
You can find more examples in the /core/core.api.php.
Backward compatibility for Drupal 10.1 and Drupal 11.0
For modules that must support both Drupal 10.1+ and Drupal 11, the recommended approach is to keep the procedural hook and delegate execution to a service class. A procedural hook should be marked with #[LegacyHook]:
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Hook\Attribute\LegacyHook;
use Drupal\my_module\Hook\EntityHooks;
/**
* Implements hook_entity_insert().
*
* @phpstan-ignore-next-line
*/
#[LegacyHook]
function my_module_entity_insert(EntityInterface $entity) {
\Drupal::service(EntityHooks::class)->entityInsert($entity);
}
The delegated class must be registered as a service:
services:
Drupal\my_module\Hook\EntityHooks:
class: Drupal\my_module\Hook\EntityHooks
autowire: true
With this approach, Drupal 10.1+ and 11.0 still executes the procedural function my_module_entity_insert(), which forwards execution to the service class. Drupal 11.1+ detects the attribute-based hook implementation directly and ignores procedural functions marked with #[LegacyHook]. This compatibility approach only works from Drupal 10.1+ because core introduced service aliases required for autowiring in that version. Since the attribute does not exist in all Drupal versions, it is recommended to add a local PHPStan ignore rule for each #[LegacyHook] attribute:
// @phpstan-ignore-next-lineThis allows one codebase to support Drupal 10.1+ and 11+ safely.