Drupal Object-Oriented Hooks – Finally, a Coherent Architecture

Auteur(s) de l'article

Ever since Drupal embraced the object-oriented approach with Symfony, something has been bothering me deeply about the architecture. On one side, we had Controllers, injectable services, structured plugins… And on the other, our good old hooks that continued to live in .module files with their module-name prefix.
Drupal🪝can realy mess your codebase
This dichotomy really gave me the feeling of having spaghetti code at the heart of an otherwise modern architecture. Fortunately, the Drupal team heard our frustrations, and with version 11.1.0, they’ve almost solved this architectural consistency problem.
Almost? Yes, because there are still some hooks that require the traditional procedural approach. But it’s already a huge step forward!

A quiet revolution

Drupal version 11.1.0, introduces a feature I’ve been waiting for years: the #[Hook] attribute for creating object-oriented hooks.
function my_module_entity_insert(EntityInterface $entity) {
  // ...
}
#[Hook('entity_insert')]
class EntityInsertHook {
  public function __invoke(EntityInterface $entity): void {
    // ...
  }
}
This new approach finally allows us to move our business logic out of .module files and organize it into dedicated classes, with all the advantages that entails.

Why this approach changes everything

1. Dependency Injection and Separation of Concerns

Gone are the days when we had to use \Drupal::service() or \Drupal::entityTypeManager() directly in our hooks. We can now inject our dependencies properly via the constructor of our hook classes.

2. Code Clarity

Having well-named classes becomes crucial. It’s essential to avoid creating generic classes that group different hooks together — this would simply transfer the problems of the *.module file to a class.

3. Improved Testability

With dedicated classes and dependency injection, our hooks become much easier to unit test.

How to use the Hook attribute

Implementation is surprisingly simple:
1. Create the Class in the Right Namespace
Your class must be in the Drupal\modulename\Hook namespace (or subdirectory). It will be automatically registered as an autowired service.
2. Use the #[Hook] Attribute
You can apply the Drupal\Core\Hook\Attribute\Hook attribute either on methods or on the class. If it's on the class and the class doesn't have an __invoke method, then the method argument is required.
#[Hook('entity_insert')]
class EntityInsertHook {
  public function __invoke(EntityInterface $entity): void {
    // ...
  }
}

Takeaway

1. Create a Dedicated Hook Folder

Just like we already do for Plugins or Controllers, I recommend creating a Hook folder in your module to organize your hook classes.

2. Use the “Hook” Suffix (Not “Hooks”)

Keep consistency with naming conventions: NodeManipulationHook rather than NodeManipulationHooks.

3. Attribute on Method vs on Class

You can add the #[Hook] attribute on the class and specify the method as the second parameter. I don’t recommend this approach — it’s cleaner to add the attribute directly on the method.

4. One Class per Hook… Or Almost

I suggest having one class per #[Hook], but it’s also conceivable to have multiple hooks in the same class. For example, if you want to listen to hook_node_insert, hook_node_update and hook_node_delete, having a single NodeManipulationHook class that listens to node_insert, node_update and node_delete is perfectly acceptable.
<?php

namespace Drupal\module\Hook;

use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Site\Settings;
use Drupal\node\NodeInterface;

/**
 * Node manipulation hook for ....
 */
class NodeManipulationHook {

  /**
   * Upon creation ...
   */
  #[Hook('node_insert')]
  public function upsert(NodeInterface $node): void {
    // ...
  }

  /**
   * Upon update ...
   */
  #[Hook('node_update')]
  public function update(NodeInterface $node): void {
    // ...
  }

  /**
   * Upon deletion ...
   */
  #[Hook('node_delete')]
  public function delete(NodeInterface $node): void {
    //..
  }
}

5. Multiple hook for a single method

A method can have multiple #[Hook] attributes if it implements multiple hooks. For example, node_comment_insert and node_comment_update have the exact same implementation and so they could become.
#[Hook('comment_insert')]
#[Hook('comment_update')]
public function commentInsertOrUpdate(CommentInterface $comment)

6. Pro Tip: Using __invoke

If your hook class contains only one method, you can use the magic __invoke method. In this case, you can apply the #[Hook] attribute directly on the class:
<?php

namespace Drupal\my_module\Hook;

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Hook\Attribute\Hook;

/**
 * Alter the Article form in order to ...
 */
#[Hook('form_alter')]
class ArticleFormAlterHook {
  public function __invoke(array &$form, FormStateInterface $form_state, $form_id) {
    // ..
  }
}

A Note for Open Source Maintenance

For everyone contributing to the Drupal ecosystem or maintaining existing modules, know that there’s also the #[LegacyHook] attribute that allows managing the transition between the old and new approaches. This can be particularly useful for maintaining compatibility during a migration period supporting multiple Drupal version in parallels.
// module.module
#[LegacyHook]
function module_comment_insert(CommentInterface $comment) {
   new CommentHooks()->commentInsertOrUpdate($comment);
}

#[LegacyHook]
function module_comment_update(CommentInterface $comment) {
   new CommentHooks()->commentInsertOrUpdate($comment);
}

Conclusion

Object-oriented hooks in Drupal 11.1 aren’t just a simple technical novelty — they represent the logical culmination of the architectural transformation begun with Drupal 8.
Even though some hooks still remain procedural, this approach finally allows us to write cleaner, more testable, and more maintainable code. Dependency injection, separation of concerns, and organization into dedicated classes radically transform how we can structure our modules.
I highly recommend adopting this approach right now in your new Drupal projects. Your future self (and your colleagues) will thank you!
❤️ Love on your keyboards.

Sources

Gábor Hojtsy (2024) Drupal 11.1.0 release note.
https://www.drupal.org/blog/drupal-11-1-0
David Duymelinck (2024) New Drupal Hook attribute.
https://dev.to/xwero/new-drupal-hook-attribute-34n0
Iheb Attia (2024) Hooks Implementation in Drupal 11.1: A New OOP Approach
https://attia-it.com/blog/hooks-implementation-drupal-11-new-oop-approach
Drupal Changes Record (2024)
https://www.drupal.org/node/3442349

Resources

Claude AI https://claude.ai
Assisted with writing and refining my English.
All images copyright of their respective owners.