With the release of Drupal 11.1, there’s a cool new feature for developers: Hooks can now be implemented as class methods using PHP attributes instead of functions. This change is a major step forward in modernizing Drupal’s codebase. While procedural function-based hooks are still supported (and will be for some time), developers writing new code should strongly consider using the object-oriented (OOP) approach introduced in Drupal 11.1.
A brief history of hooks in Drupal
The concept of hooks -- functions that follow a defined naming convention and are invoked at specific points during Drupal’s runtime -- has been a part of Drupal for a very, very, long time. I think it’s safe to say that they are one of Drupal’s most defining features. And virtually un-changed for 24 years. Until now.
The idea of PHP functions that follow a defined naming convention as a way to allow modular code was first added to Drupal this commit [#8d5b4e7b] on Dec. 23rd, 2000.
The code at that time looked like this:
function module_execute($module, $hook, $argument = "") {
global $repository;
return ($repository[$module][$hook]) ? $repository[$module][$hook]($argument) : "";
}
Function names were cached in a $repository
at runtime, and then called (if they existed) whenever module_execute()
was invoked.
On May 5th, 2001, the module system was re-written in [#be8e898d]. This updates includes the addition of the module_invoke
function below. And the introduction of this line of code $function = $name ."_". $hook;
which has basically been with us ever since.
It was more or less this for a while:
// invoke hook $hook of module $name with optional arguments:
function module_invoke($name, $hook, $a1 = NULL, $a2 = NULL, $a3 = NULL, $a4 = NULL) {
$function = $name ."_". $hook;
if (function_exists($function)) {
return $function($a1, $a2, $a3, $a4);
}
}
Then this:
// invoke hook $hook of module $name with optional arguments:
function module_invoke(){
$args = func_get_args();
$function = array_shift($args);
$function .= "_". array_shift($args);
if (function_exists($function)) {
return $function(implode(",", $args));
}
}
Then this:
/**
* Invoke a hook in a particular module.
*
* @param $module
* The name of the module (without the .module extension).
* @param $hook
* The name of the hook to invoke.
* @param ...
* Arguments to pass to the hook implementation.
* @return
* The return value of the hook implementation.
*/
function module_invoke($module, $hook, $a1 = NULL, $a2 = NULL, $a3 = NULL, $a4 = NULL) {
$function = $module .'_'. $hook;
if (function_exists($function)) {
return $function($a1, $a2, $a3, $a4);
}
}
On Mar. 1st, 2005, the Drupal 4.6 code was updated in [#456fd7cc] to use call_user_func_array()
which amongst other things allowed hooks at have any number of arguments. Note the use of $a1 ... $4
which prior to this limited the number of arguments a hook could receive.
/**
* Invoke a hook in a particular module.
*
* @param $module
* The name of the module (without the .module extension).
* @param $hook
* The name of the hook to invoke.
* @param ...
* Arguments to pass to the hook implementation.
* @return
* The return value of the hook implementation.
*/
function module_invoke() {
$args = func_get_args();
$module = array_shift($args);
$hook = array_shift($args);
$function = $module .'_'. $hook;
if (module_hook($module, $hook)) {
return call_user_func_array($function, $args);
}
}
In Drupal 8 ([#c80c3e18]), this code was moved into the ModuleHandler::invoke()
method, and updated to make use of some newer PHP language features, but the logic remained the same. And the code stayed this way until Drupal 11.1:
public function invoke($module, $hook, array $args = []) {
if (!$this->hasImplementations($hook, $module)) {
return;
}
$hookInvoker = \Closure::fromCallable($module . '_' . $hook);
return call_user_func_array($hookInvoker, $args);
}
In Drupal 11.1, the logic for calling a function based hook is now in ModuleHandler::legacyInvoke()
, but is still basically the same:
protected function legacyInvoke($module, $hook, array $args = []) {
$this->load($module);
$function = $module . '_' . $hook;
if (function_exists($function) && !(new \ReflectionFunction($function))->getAttributes(LegacyHook::class)) {
return $function(...$args);
}
return NULL;
}
Kind of amazing how well this system has served Drupal for 24 years. Drupal 8 introduced new patterns for altering, extending, and enhancing Drupal core like plugins, services, and events. But hooks are still an important part of how the system works.
Object-oriented hook implementations
The idea of switching to object-oriented hook implementations has been around for a long time. And numerous attempts have been made since the introduction of Drupal 8. But for various reasons, some technical and some DX related, the solution was elusive until recently.
Before Drupal 11.1, implementing a hook meant writing a procedural function following a specific naming convention in your .module file, like this:
/**
* Implements hook_form_alter().
*/
function mymodule_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
// Custom code and comments go here ...
}
Now, with Drupal 11.1+, we can implement hooks inside classes using the #[Hook]
attribute. Example code lives in src/Hook/MyModuleFormHooks.php:
<?php
declare(strict_types=1);
namespace Drupal\mymodule\Hook;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Hook\Attribute\Hook;
class MyModuleFormHooks {
/**
* Implements hook_form_alter().
*/
#[Hook('form_alter')]
public function formAlter(&$form, FormStateInterface $form_state, $form_id): void {
// Custom code and comments go here ...
}
}
Why use OOP hooks instead of the traditional procedural function-based hooks?
Improved code organization and readability
- Hooks are now encapsulated within classes, reducing clutter in .module files.
- Instead of having multiple hook functions scattered across a .module file, related hooks can be grouped logically inside a single class.
Enables autowiring and dependency injection
Class-based hooks support dependency injection, allowing services to be injected directly into the class. This eliminates the need for \Drupal::service()
calls, making code more testable and modular.
Example:
namespace Drupal\mymodule\Hook;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Hook\Attribute\Hook;
class MyModuleHooks {
protected LoggerChannelFactoryInterface $loggerFactory;
public function __construct(LoggerChannelFactoryInterface $loggerFactory) {
$this->loggerFactory = $loggerFactory;
}
#[Hook('cron')]
public function cron() {
$this->loggerFactory->get('mymodule')->info('Cron job executed.');
}
}
Better testability
Unit tests become easier since hooks are now inside classes that can be instantiated with mocked dependencies. The procedural approach required global function mocking, which was cumbersome.
PSR-4 Autoloading and performance
Since class-based hooks follow PSR-4 autoloading, they aren’t loaded unless needed. This contrasts with .module files, which are always loaded, even if they contain only hook functions that might not be used. Reducing unnecessary file loading can lead to minor performance improvements.
TL;DR:
✅ Better code organization
✅ Supports dependency injection (no more \Drupal::service())
✅ Easier to test
✅ Performance benefits
✅ Future-proof (aligns with modern PHP practices)
If you’re writing new Drupal 11.1+ code, the OOP approach is the way to go! 🚀
Updating our tutorials with the new approach
One of our core commitments at Drupalize.Me is ensuring that our tutorials remain accurate and relevant as Drupal evolves. So, we’re working on updating all of our tutorials to take into account the new OOP approach to adding hooks in a module. Procedural hooks have been around for 24 years. We know they aren’t going to disappear overnight. You’ll see them in example code and existing documentation for a long time to come. For now, we’ll be including both approaches in our content whenever doing so makes sense.
You should plan on learning both approaches, and then using the one that makes the most sense given your specific case.
Notable tutorial updates
- What Are Hooks?: This tutorial now covers the concept of hooks, their purpose, and the new OOP implementation method introduced in Drupal 11.1.
- Implement Any Hook: Updated to guide developers through the process of implementing hooks using both traditional procedural functions and the new class-based approach with attributes.
The Drupal Module Developer Guide has been thoroughly updated to ensure compatibility with Drupal 11.1, incorporating the new OOP methodologies for hook implementations.
Add new comment