Skip to main content
Stéphane Quantin

Main navigation

  • Portfolio
  • About
  • Freelance Drupal
    • Audit de site Drupal
    • Drupal maintenance contract
  • Blog
  • CV
  • Contact
  • Developer tools
Languages
  • French
  • English

Breadcrumb

  1. Home
  2. Blog

Drupal 11 and Object-Oriented Hooks: Toward Cleaner Modules

By StephaneQ , Tue, Dec 17, 2024 - 10:58

Hooks have long been part of Drupal’s identity. They allow a module to intervene at specific points during execution, alter a form, react to an action, modify data, or add behavior without directly changing Drupal Core or contributed module code.

With Drupal 11.1, this historical mechanism evolves: hooks can now be declared in classes using the #[Hook] attribute.

This change does not remove Drupal’s hook system. It simply brings it closer to the modern PHP practices already used elsewhere in Drupal.

The Historical Role of Hooks in Drupal

In Drupal, a hook is an extension point. When a specific event or step occurs, Drupal looks for the corresponding implementations in enabled modules.

Historically, these implementations are written as procedural functions in a .module file.

For example, a module named example can implement hook_help() with a function named example_help().

/**
   * Implements hook_help().
   */
  function example_help($route_name, \Drupal\Core\Routing\RouteMatchInterface $route_match): ?string {
    if ($route_name === 'help.page.example') {
      return 'Help text for the Example module.';
    }

    return NULL;
  }
  

This model is simple, direct, and very effective. It has contributed a lot to Drupal’s flexibility.

But over time, some .module files become difficult to maintain.

The Limits of Large .module Files

In a small module, a .module file with a few hooks remains perfectly readable.

In an older or more feature-rich module, the situation can quickly become harder to manage: several hooks, alters, preprocess functions, callbacks, sometimes business logic, and a lot of code in a single file.

The problem is not the hook itself. The problem is mostly organization.

  • the file can become very long;
  • responsibilities are sometimes mixed together;
  • typing and autocompletion are less comfortable;
  • dependency injection is not natural;
  • the logic is harder to test cleanly.

Modern Drupal already relies heavily on classes, services, plugins, events, controllers, and object-oriented forms. Procedural hooks were still one of the older parts of the development model.

The Principle of Hooks in Classes

With Drupal 11.1, a module can place its hook implementations in a class located in the module’s Hook namespace.

In practice, this gives a file like this:

modules/custom/example/src/Hook/ExampleHooks.php
  

The class then uses the #[Hook] attribute to declare which method corresponds to which hook.

<?php

  namespace Drupal\example\Hook;

  use Drupal\Core\Hook\Attribute\Hook;
  use Drupal\Core\Routing\RouteMatchInterface;

  /**
   * Hook implementations for the Example module.
   */
  final class ExampleHooks {

    /**
     * Implements hook_help().
     */
    #[Hook('help')]
    public function help(string $route_name, RouteMatchInterface $route_match): ?string {
      if ($route_name === 'help.page.example') {
        return 'Help text for the Example module.';
      }

      return NULL;
    }

  }
  

The method name is free. What matters is the #[Hook('help')] attribute, which tells Drupal that this method implements hook_help().

What the #[Hook] Attribute Brings

The #[Hook] attribute makes the method’s intent explicit.

Instead of relying only on a naming convention such as example_help(), Drupal can discover a class method marked as a hook implementation.

This evolution is useful for several reasons.

First, the code can be better organized. Hooks can be grouped in a dedicated class, or split across several classes according to the module’s responsibilities.

Second, method signatures become clearer for IDEs and static analysis tools. Typing feels more natural, and the code fits better with the rest of Drupal’s object-oriented architecture.

Finally, this approach opens the door to better testability. Not all logic should live directly inside the hook method, but being in a class makes it easier to extract behavior into services or dedicated methods.

An Example with hook_form_alter

A common case in Drupal is hook_form_alter().

In procedural form, you would usually write:

/**
   * Implements hook_form_alter().
   */
  function example_form_alter(array &$form, \Drupal\Core\Form\FormStateInterface $form_state, string $form_id): void {
    if ($form_id === 'node_article_form') {
      $form['advanced']['#open'] = TRUE;
    }
  }
  

In an object-oriented version with Drupal 11.1, the same idea becomes:

<?php

  namespace Drupal\example\Hook;

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

  /**
   * Form hook implementations for the Example module.
   */
  final class ExampleFormHooks {

    /**
     * Implements hook_form_alter().
     */
    #[Hook('form_alter')]
    public function formAlter(array &$form, FormStateInterface $form_state, string $form_id): void {
      if ($form_id === 'node_article_form') {
        $form['advanced']['#open'] = TRUE;
      }
    }

  }
  

The behavior remains the same. What changes is how the implementation is organized.

A Migration to Approach Gradually

This evolution should not be understood as an obligation to convert everything immediately.

In a simple module with one or two very short hooks, keeping a .module file can still be acceptable, especially if the module must remain compatible with older Drupal versions.

For a module targeting only Drupal 11.1 or later, class-based hooks become a very interesting option.

The right approach is pragmatic: start by converting hooks that really make the .module file harder to maintain, or hooks that would benefit from being grouped by responsibility.

Compatibility also matters. A contributed module that must support Drupal 10 or Drupal 11.0 cannot switch without an appropriate strategy. For project-specific code under your control, the decision is simpler if the target version is clearly Drupal 11.1 or newer.

What This Does Not Change

Hooks remain hooks.

They do not automatically become Symfony events, plugins, or business services. The extension point mechanism remains Drupal’s hook system.

The same principles still apply:

  • do not put too much business logic directly in a hook;
  • avoid overly long hook implementations;
  • preserve cacheability metadata when modifying render arrays;
  • respect the expected signatures;
  • extract complex logic into dedicated services.

The #[Hook] attribute improves code organization. It does not replace clean architecture.

Conclusion

With object-oriented hooks, Drupal keeps one of its most important mechanisms while integrating it better into a modern PHP approach.

For Drupal developers, this is a discreet but meaningful evolution. It helps reduce the weight of .module files, structure responsibilities more clearly, and bring hooks closer to the rest of Drupal’s object-oriented code.

The point is not to convert all existing code on principle. But for new Drupal 11.1+ modules, or for custom modules that are starting to accumulate too many procedural hooks, this approach is clearly worth adopting.

Drupal keeps its hook system, but gives it a cleaner, more readable form that better fits current PHP development practices.

Tags

  • Drupal
  • Drupal 11

Social networks

  • Malt
  • codeur.com
  • 404Works
  • LinkedIn
  • Twitter
  • DoYouBuzz

Twitter

Tweets by @StephaneQ
RSS feed

Pied de page

  • Contact
  • Mentions légales
Powered by Drupal