Drupal’s Shift Toward Object-Oriented Hooks
The concept of “hooks” was introduced in Drupal 3, allowing developers to modify or extend the functionality of core and contributed modules without hacking the original source code. Hooks made it possible to alter how Drupal behaves, such as modifying forms or manipulating data without changing core files, making updates and long-term maintenance significantly easier. At a time when PHP had very limited support for object-oriented programming (OOP), a function-based hook system provided a powerful architectural pattern that fit well within the technical constraints of the era. Think of hooks as points in the code where Drupal pauses and asks, “Does anyone want to alter this?”
As Drupal projects became larger and more complex, especially in enterprise environments, the traditional procedural hook system using global functions like mymodule_form_alter() gradually became difficult to maintain and scale. Hooks for a module were often grouped into large .module files that became increasingly hard to navigate across large teams and codebases.
To address these architectural limitations, Drupal introduced Object-Oriented Hooks in Drupal 11.1, a major shift from the procedural approach that had defined Drupal development for over two decades. Since then, the feature has evolved further with support for ordered hooks, preprocess hooks, and theme hooks, making OO Hooks far more practical for modern Drupal development.
Quick recap: What are Object-Oriented (OO) Hooks?
Before Drupal 11.1, writing a hook involved a procedural way in having a function with a certain naming convention that would reside in the .module file
/**
* Implements hook_form_alter().
*/
function my_custom_module_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
// Check if the form ID is 'contact_message_feedback_form'.
if ($form_id === 'contact_message_feedback_form') {
$form['copy']['#default_value'] = TRUE;
}
} With Drupal 11.1, hooks can now be implemented using classes inside the Drupal\modulename\Hook namespace. These classes are automatically registered as autowired services using the Drupal\Core\Hook\Attribute\Hook attribute.
<?php
declare(strict_types=1);
namespace Drupal\my_custom_module\Hook;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Hook\Attribute\Hook;
class MyCustomModuleFormHook {
#[Hook('form_alter')]
public function formAlter(&$form, FormStateInterface $form_state, $form_id) {
// Check if the form ID is 'contact_message_feedback_form'.
if ($form_id === 'contact_message_contact_form_form') {
$form['copy']['#default_value'] = TRUE;
}
}
}The object-oriented implementation follows the same method signature as its procedural counterpart:
- hook_form_alter() → formAlter()
- hook_node_insert() → nodeInsert()
- hook_cron() → cron()
Why Drupal introduced OO Hooks
The introduction of OO Hooks in Drupal represents one of the final major steps in Drupal’s transition toward modern PHP architecture. While the underlying hook system and concepts remain the same, the implementation now follows modern development patterns that address several long-standing issues in large enterprise codebases, including:
Dependency management
Hooks often need to interact with multiple services such as entityQuery(), currentUser(), or the messenger service. Procedural hooks cannot use Dependency Injection directly and instead rely on global service calls like \Drupal::service(), which is generally considered an anti-pattern in modern PHP architecture.
Bloated module files
Traditionally, hooks for a module were stored inside a single .module file. In large enterprise applications, these files could quickly become massive, difficult to navigate, and increasingly hard to maintain across development teams.
Performance overhead
Hooks work as interception points where Drupal effectively pauses and asks, “Does anyone want to alter this?”. To support this, Drupal has historically needed to scan and load module files to discover hook implementations. In systems with a large number of modules, this introduces additional overhead compared to modern approaches like PSR-4 autoloading, where code is only loaded when required.
Execution order conflicts
In large systems, multiple modules often implement the same hook. Managing execution order using module weights can become fragile and difficult to maintain as applications grow in complexity.
Testability issues
Procedural hooks exist as global functions, making them difficult to unit test in isolation. Developers often had to bootstrap large parts of Drupal or mock the global environment to test a single hook. Modern enterprise development standards instead favour testing isolated classes with mocked dependencies.
The move to modern patterns
Since Drupal 8 adopted Symfony components, Drupal has steadily moved toward modern development patterns such as Controllers, Event Subscribers, Plugins, and Services. OO Hooks follow the same direction by introducing a more structured approach that improves encapsulation, reusability, maintainability, and standardisation.
Modern PHP alignment
Procedural hooks are part of a legacy system that has existed in Drupal for over two decades. While they still work effectively, OO Hooks align Drupal more closely with modern PHP standards such as PSR-4 autoloading and Symfony-based architecture, making the framework more approachable for developers coming from modern PHP ecosystems.
Dependency Injection
As mentioned earlier, procedural hooks depend heavily on global \Drupal::service() calls. OO Hooks, however, are registered as services, which means dependencies can be injected directly into the constructor.
<?php
declare(strict_types=1);
namespace Drupal\my_custom_module\Hook;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountProxyInterface;
class MyCustomModuleFormHook {
public function __construct(
protected MessengerInterface $messenger,
protected AccountProxyInterface $currentUser,
) {}
#[Hook('form_alter')]
public function formAlter(
&$form,
FormStateInterface $form_state,
$form_id
) {
// Check if the form ID is 'contact_message_feedback_form'.
if ($form_id === 'contact_message_feedback_form') {
// Add a message for authenticated users.
if ($this->currentUser->isAuthenticated()) {
$form['copy']['#default_value'] = TRUE;
$this->messenger->addStatus(
'Thanks for contacting us. We pre-selected "Send yourself a copy".'
);
}
}
}
}Organised code
Instead of maintaining a massive .module file filled with unrelated procedural functions, OO Hooks allow developers to group related hooks into dedicated classes. This makes the codebase easier to navigate, debug, and maintain.
Structured execution order
Rather than relying on module weights stored in the database, developers can now explicitly control execution order using PHP attributes such as:
#[Hook(order: 10)]
#[Hook(before: 'other_module')]This provides a far more predictable and maintainable approach for large applications.
Testing without a full bootstrap
With procedural hooks, testing often required bootstrapping large parts of Drupal, including the kernel and database layer. Since OO Hooks are now implemented as standard PHP classes, they can be unit tested more easily by mocking only the required dependencies.
What changed after Drupal 11.1
Drupal 11.1 introduced Object-Oriented Hooks, but the feature has continued to evolve in subsequent releases. Drupal 11.2 added support for preprocess hooks and introduced the order parameter for better control over hook execution order. With Drupal 11.3, themes can now also implement hooks using the Object-Oriented approach, extending OO Hooks support beyond modules and further integrating modern development patterns across Drupal’s architecture.
Object-Oriented Preprocess Hooks
A meaningful improvement after the initial OO Hooks release was support for preprocess hooks using object-oriented classes, as preprocess logic is traditionally difficult to maintain in larger Drupal projects. Over time, global preprocess functions often turn into large procedural files with mixed responsibilities, service lookups, and tightly coupled rendering logic.
With OO preprocess hooks, Drupal allows preprocess logic to live inside dedicated classes with dependency injection support, making theme-layer code easier to organise, manage, debug, and test.
Instead of adding more logic to sprawling .module files, devs can now structure preprocess behavior in smaller, focused classes.
Before: Traditional procedural preprocess hook
function mytheme_preprocess_block(array &$variables): void {
$current_user = \Drupal::currentUser();
if ($current_user->isAuthenticated() && $current_user->hasPermission('view authenticated data')) {
$variables['show_authenticated_data'] = TRUE;
}
}After: Object-oriented preprocess hook
namespace Drupal\my_custom_module\Hook;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Hook\Attribute\Hook;
final class BlockPreprocessHooks {
public function __construct(
protected AccountProxyInterface $currentUser,
) {}
#[Hook('preprocess_block')]
public function preprocessBlock(array &$variables): void {
if ($this->currentUser->isAuthenticated() && $this->currentUser->hasPermission('view authenticated data')) {
$variables['show_authenticated_data'] = TRUE;
}
}
}The biggest improvement here is not the syntax; it is the ability to use Dependency Injection and encapsulate the preprocess logic in dedicated classes.
For teams working on larger Drupal codebases, this creates a cleaner separation between rendering, business logic & service injections.
Hook order parameter
Controlling hook execution order using hook_module_implements_alter() was powerful, but also unintuitive and difficult to maintain in large codebases. Drupal 11.2 introduced the order parameter in the #[Hook] attribute, allowing developers to define execution order directly within the hook implementation itself. Instead of writing a separate “manager” hook to rearrange other hook implementations, execution order can now be controlled in a far more explicit and maintainable way.
Before: using hook_module_implements_alter
/**
* This forces 'my_module_form_alter' to run after 'other_module'.
*/
function my_custom_module_module_implements_alter(&$implementations, $hook) {
if ($hook == 'form_alter') {
$group = $implementations['my_custom_module'];
unset($implementations['my_custom_module']);
// Adding it back to the end makes it run last
$implementations['my_custom_module'] = $group;
}
}After: using the order parameter in the #[Hook] attribute
<?php
declare(strict_types=1);
namespace Drupal\my_custom_module\Hook;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Hook\Order\Order;
use Drupal\Core\Hook\Attribute\Hook;
class MyCustomModuleFormHook {
// Order the hook first
#[Hook('form_alter', order: Order::First)]
public function formAlter(&$form, FormStateInterface $form_state, $form_id) {
// Logic goes here.
}
// Order the hook last
#[Hook('form_alter', order: Order::Last)]
public function formAlter(&$form, FormStateInterface $form_state, $form_id) {
// Logic goes here.
}
// Order the hook before another module's hook implementation
#[Hook('form_alter', order: Order::Before('other_module'))]
public function formAlter(&$form, FormStateInterface $form_state, $form_id) {
// Logic goes here.
}
// Order the hook after another module's hook implementation
#[Hook('form_alter', order: Order::After('other_module'))]
public function formAlter(&$form, FormStateInterface $form_state, $form_id) {
// Logic goes here.
}
}Ordered OO hooks simplify a previously awkward part of Drupal development by removing the need for a separate hook_module_implements_alter() implementation just to control execution order. Defining the order directly within the hook attribute makes the intent clearer and keeps related logic in a single place.
Theme hooks
Although Drupal 11.2 introduced #[Hook] attribute support for module preprocess functions, theme preprocess functions still remained procedural. That gap was addressed in Drupal 11.3 with the introduction of Object-Oriented preprocess hooks for themes.
Theme preprocess functions are commonly used to modify variables or pass additional data to Twig templates, and over time, these implementations can significantly bloat the .theme file. With Drupal 11.3, developers can now organise preprocess logic into dedicated classes, making theme code easier to manage, debug, and test.
There are, however, a few implementation differences when using OO Hooks within themes:
- The
#[Hook]attribute in themes does not support theorderormoduleparameters. - Themes do not support
ReorderHook. - Themes do not support
RemoveHook. - The execution order of theme hook implementations cannot be modified.
- Hooks in base themes always execute before hooks in the active theme.
- Hooks in non-active themes are not executed.
These constraints largely align with how Drupal’s theme layer traditionally works. Since theme hooks are primarily focused on presentation and template preparation, their execution model is intentionally more predictable and controlled compared to module hooks. Even with these limitations, OO Hooks still bring a significant improvement to theme code organisation and maintainability.
Hybrid architecture still exists
To maintain backward compatibility, procedural hooks have not been removed and continue to coexist alongside OO Hooks. This is particularly important for contributed modules that still support older Drupal versions, such as Drupal 10.1 through 11.0. In such cases, procedural hooks often act as lightweight shims that forward execution to the new OO Hook implementation, allowing modules to support both old and new Drupal versions during the transition.
This gradual migration strategy makes sense, considering hooks are one of Drupal’s foundational concepts and are deeply integrated across contributed modules and themes. However, it can also introduce some confusion. Depending on the subsystem or supported Drupal version, developers may encounter procedural hooks, OO Hooks, legacy shim implementations, or hook types that still remain procedural without an OO counterpart. As a result, Drupal’s hook system currently exists in a transitional state where multiple implementation patterns coexist within the same codebase.
Better structure does not guarantee better code
Object-oriented hooks do improve separation by moving implementations into isolated classes instead of large procedural files. However, good organization still requires architectural discipline.
Creating one class per hook can quickly become excessive and difficult to navigate. A better approach is grouping related hooks by feature or responsibility.
For example, hooks related to article presentation — such as preprocess_node, and page_attachments could reasonably live together inside a class like ArticleDisplayHooks. Similarly, form-related hooks for a specific workflow could be grouped into something like ArticleFormHooks.
The goal should not be maximizing the number of classes, but creating logical boundaries that improve maintainability and readability over time.
Should teams adopt OO Hooks today?
My personal take is that teams do not need to rush into converting every procedural hook to OO Hooks immediately. Procedural hooks will continue to work throughout Drupal 11 and are likely to remain supported through Drupal 12, although Drupal core has indicated they may be removed as early as Drupal 13.
For contributed module maintainers, the recommended approach is to use #[LegacyHook] for supporting Drupal versions prior to 11.1 while implementing the new OO Hook in parallel. This involves creating a hook class, registering it as a service, and keeping a lightweight procedural shim for backward compatibility.
That said, there are several areas where adopting OO Hooks already makes practical sense:
- When writing new hooks, prefer the Object-Oriented approach instead of creating new procedural hook functions.
- When building new modules from scratch, there is little reason to rely heavily on
.moduleor.themefiles for hook implementations. - During the Drupal 11 to Drupal 12 upgrade efforts, procedural hooks can be migrated to OO Hooks as part of broader code modernisation effort.
Rather than forcing a complete migration overnight, OO Hooks are best adopted incrementally as projects evolve naturally over time.
Object-Oriented Hooks do not fundamentally change how Drupal hooks work, but they do modernise how we structure and maintain Drupal applications moving forward. In many ways, this feels like Drupal finally bringing one of its oldest concepts into the modern PHP world.
That said, after writing procedural hook functions for nearly 14 years, seeing giant .module and .theme files slowly become less relevant does feel a little strange. Hooks like hook_form_alter() almost feel nostalgic at this point. They will probably be missed… but also not really missed once dependency injection, cleaner organisation, and better maintainability start becoming the default experience.
References:
- https://drupalize.me/blog/drupal-111-adds-hooks-classes-history-how-and-tutorials-weve-updated
- https://bonnici.co.nz/blog/introduction-to-drupal-10
- https://www.hashbangcode.com/article/drupal-11-object-oriented-hooks-and-hook-service-classes
- https://www.drupal.org/node/3442349
- https://www.drupal.org/node/3551652
- https://www.drupal.org/node/3493962