Writing Hooks in Drupal 11

Drupal 10.1
Drupal 11

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

}

A class placed in the Drupal\my_module\Hook namespace is automatically discovered by Drupal and registered as an autowired service. The first parameter to the #[Hook()] attribute is the short hook name, that is, with the 'hook_' prefix removed.
It is recommended to either keep one class per hook when the logic is substantial, or 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.

 


Hooks that must remain procedural

Please note that some hooks, such as hook_hook_info(), hook_module_implements_alter(), hook_install(), hook_schema(), hook_uninstall(), hook_update_N(), etc, as well as hooks implemented by themes, must be implemented as procedural.


 

Full documentation of the Drupal 11 hook system, including supported attribute-based hooks and procedural-only hooks, is available here:

 

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-line

This allows one codebase to support Drupal 10.1+ and 11+ safely.

References