This article is also available in English
Drupal 11.1 a introduit, en décembre 2024, une nouvelle façon d'implémenter les hooks, de manière orientée objet ! C'est aujourd'hui la manière de faire recommandée par le core, et la quasi totalité des hooks est concernée.
Mise à jour du 6 août 2026 : article revu pour Drupal 11.4. Depuis sa première publication, les preprocess, les requirements et les hooks de thèmes sont passés en orienté objet, deux noms ont changé en 11.2, et la liste des hooks qui doivent rester procéduraux a bien fondu.
En pratique il suffit de créer un fichier MesHooks.php (un ou plusieurs fichiers, le nom est libre) dans le dossier src/Hook (cela peut être dans un sous dossier, mais le dossier de base doit être obligatoirement Hook pour être détecté par drupal) de votre module.
Ainsi pour implémenter le hook HOOK_form_alter, voici comment cela se passe :
Je créé un fichier mon_module/src/Hook/FormHooks.php avec le contenu suivant :
<?php
declare(strict_types=1);
namespace Drupal\mon_module\Hook;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Provides hook implementations for Form alterations.
*/
class FormHooks {
/**
* Alters a Drupal form before it is rendered.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* An object containing the current state of the form.
* @param string $form_id
* The unique identifier of the form being altered.
*/
#[Hook('form_alter')]
public function formAlter(array &$form, FormStateInterface $form_state, string $form_id): void {
...
}
/**
* Alters the programme bundle edit and add forms
*
* @param array $form
* The form structure to be altered.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param string $form_id
* The unique identifier of the form.
*/
#[Hook('form_node_programme_edit_form_alter')]
#[Hook('form_node_programme_form_alter')]
public function formProgrammeAlter(array &$form, FormStateInterface $form_state, string $form_id): void {
...
}
}c'est l'attribut PHP
#[Hook('form_alter')]qui indique à drupal quel est le hook implémenté par la méthode qui suit (dont le nom est libre)
On peut aussi utiliser une même méthode pour plusieurs hooks, ici j'implémente les hooks HOOK_form_FORM_ID_alter pour mes formulaire d'ajout et d'édition de noeud pour mon type de contenu « programme ».
#[Hook('form_node_programme_edit_form_alter')]
#[Hook('form_node_programme_form_alter')]
public function formProgrammeAlter(array &$form, FormStateInterface $form_state, string $form_id): void {
...
}L'injection de dépendances, cadeau bonus
C'est le principal intérêt de la manœuvre : vos classes de hooks sont automatiquement enregistrées comme services autowirés, avec le nom de la classe comme identifiant de service. Rien à déclarer dans votre mon_module.services.yml, vous pouvez injecter directement vos dépendances dans le constructeur plutôt que d'appeler \Drupal::service() :
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountProxyInterface;
class FormHooks {
public function __construct(
protected readonly AccountProxyInterface $currentUser,
protected readonly EntityTypeManagerInterface $entityTypeManager,
) {}
#[Hook('form_alter')]
public function formAlter(array &$form, FormStateInterface $form_state, string $form_id): void {
if ($this->currentUser->hasPermission('administer nodes')) {
...
}
}
}Des hooks testables, avec de vraies dépendances injectées. Et si l'autowiring ne suffit pas, rien n'empêche de déclarer le service à la main dans votre services.yml.
Les autres façons de poser l'attribut
L'attribut peut aussi être placé sur la classe, en précisant la méthode concernée :
#[Hook('user_cancel', method: 'userCancel')]
class Hooks {
public function userCancel(...) {}
}ou sur une classe invocable, dont la méthode __invoke() est alors l'implémentation :
#[Hook('user_cancel')]
class Hooks {
public function __invoke(...) {}
}Enfin, un module peut implémenter un hook pour le compte d'un autre module, via le paramètre module :
#[Hook('form_alter', module: 'autre_module')]Bonne nouvelle au passage : un même module peut implémenter plusieurs fois le même hook, dans des classes différentes. Pratique pour ajouter un form_alter sans toucher à l'existant. Seuls quatre hooks n'admettent qu'une implémentation par module : hook_library_info_build(), hook_mail(), hook_help() et hook_node_update_index().
Maîtriser l'ordre d'exécution (Drupal 11.2)
hook_module_implements_alter() n'est plus le seul moyen de jouer sur l'ordre d'exécution des hooks, l'attribut accepte maintenant un paramètre order :
use Drupal\Core\Hook\Order\Order;
use Drupal\Core\Hook\Order\OrderAfter;
use Drupal\Core\Hook\Order\OrderBefore;
#[Hook('entity_type_alter', order: Order::First)]
#[Hook('entity_type_alter', order: Order::Last)]
#[Hook('entity_type_alter', order: new OrderBefore(['foo']))]
#[Hook('entity_type_alter', order: new OrderAfter(['foo']))]Et deux attributs permettent d'agir sur les implémentations des autres modules sans y toucher : #[ReorderHook] pour les réordonner, #[RemoveHook] pour empêcher purement et simplement leur exécution :
use Drupal\Core\Hook\Attribute\RemoveHook;
use Drupal\Core\Hook\Attribute\ReorderHook;
// Exécuter le entity_presave de content_moderation avant celui de workspaces.
#[ReorderHook('entity_presave',
class: ContentModerationHooks::class,
method: 'entityPresave',
order: new OrderBefore(['workspaces'])
)]
// Ne pas exécuter le hook_help de layout_builder.
#[RemoveHook('help',
class: LayoutBuilderHooks::class,
method: 'help'
)]Le change record : https://www.drupal.org/node/3497308
La fin du fichier module ?
Presque, comme indiqué dans le change record, tous les hooks ne peuvent pas être implémentés de cette manière là. Certains doivent rester en procédural, mais la liste a bien fondu depuis la sortie de 11.1. Voici celle en vigueur avec Drupal 11.4 :
- hook_hook_info()
- hook_module_implements_alter()
- hook_install()
- hook_install_tasks()
- hook_install_tasks_alter()
- hook_post_update_NAME()
- hook_removed_post_updates()
- hook_schema()
- hook_uninstall()
- hook_update_dependencies()
- hook_update_last_removed()
- hook_update_N()
En clair : ce qui relève de l'installation et de la mise à jour (le contenu de votre fichier .install) reste procédural, plus deux hooks « méta ». Tout le reste peut passer en classe.
Les preprocess, depuis Drupal 11.2
Les hook_preprocess() et hook_preprocess_HOOK() des modules s'implémentent en orienté objet depuis Drupal 11.2 (change record) :
#[Hook('preprocess_node')]
public function preprocessNode(array &$variables): void {
...
}Cela règle le point que nous avions relevé fin 2024 avec Alan, lors de nos tests d'upgrade vers Drupal 11.1 : les HOOK_preprocess_ENTITY_TYPE devaient alors rester en procédural, et nous avions dû conserver la définition du service et le hook procédural pour faire l'appel à la classe. Ce n'est plus nécessaire. Seule limite : une seule implémentation par module pour un preprocess donné.
hook_requirements(), déprécié depuis Drupal 11.3
Il est remplacé par trois points d'entrée distincts (change record), tous compatibles avec l'orienté objet :
- hook_runtime_requirements() et hook_runtime_requirements_alter() pour le rapport d'état du site ;
- hook_update_requirements() et hook_update_requirements_alter() pour les contrôles avant mise à jour ;
- une classe Drupal\mon_module\Install\Requirements\MonModuleRequirements implémentant InstallRequirementsInterface pour les contrôles à l'installation.
namespace Drupal\mon_module\Hook;
use Drupal\Core\Extension\Requirement\RequirementSeverity;
use Drupal\Core\Hook\Attribute\Hook;
class RequirementsHooks {
#[Hook('runtime_requirements')]
public function runtime(): array {
$requirements = [];
// ...
return $requirements;
}
}Si vous devez conserver un hook_requirements() procédural pour les versions antérieures, marquez-le avec l'attribut #[LegacyRequirementsHook] : Drupal 11.3 et suivants ne l'exécuteront pas, et ne vous signaleront pas la dépréciation.
Les hooks des thèmes, depuis Drupal 11.3
C'était la dernière grosse exception : les thèmes aussi peuvent désormais implémenter leurs hooks en orienté objet (change record), dans mon_theme/src/Hook/MonThemeHooks.php, exactement comme pour un module :
namespace Drupal\mon_theme\Hook;
use Drupal\Core\Hook\Attribute\Hook;
class MonThemePreprocessHooks {
#[Hook('preprocess_html')]
public function preprocessHtml(array &$variables): void {
...
}
}Deux limites à connaître. D'abord les thèmes ne peuvent implémenter qu'un sous-ensemble de hooks : hook_theme, les preprocess, et une liste finie d'alter (form_alter, form_FORM_ID_alter, css_alter, js_alter, library_info_alter, element_info_alter, page_attachments_alter, theme_registry_alter, theme_suggestions_alter…). Ensuite l'attribut Hook n'y accepte ni order ni module, et #[ReorderHook] comme #[RemoveHook] n'y sont pas supportés : les hooks du thème de base s'exécutent avant ceux du thème actif, et c'est tout.
Compatibilité avec Drupal 10
La question se pose de moins en moins, Drupal 10 arrivant en fin de support en décembre 2026, mais si votre module doit encore tourner dessus, le principe n'a pas changé : on garde l'implémentation procédurale, qui se contente d'appeler la classe.
On commence par ajouter notre classe à notre fichier mon_module.services.yml (inutile sur Drupal 11 où l'enregistrement est automatique, mais nécessaire pour Drupal 10) :
services:
Drupal\mon_module\Hook\FormHooks:
class: Drupal\mon_module\Hook\FormHooks
autowire: trueEt on ajoute l'appel à la classe dans mon_module.module :
/**
* Implements hook_form_alter().
*/
#[LegacyHook]
function mon_module_form_alter(&$form, FormStateInterface $form_state, $form_id) {
\Drupal::service(FormHooks::class)->formAlter($form, $form_state, $form_id);
}
/**
* Implements hook_form_node_programme_edit_form_alter().
*/
#[LegacyHook]
function mon_module_form_node_programme_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {
\Drupal::service(FormHooks::class)->formProgrammeAlter($form, $form_state, $form_id);
}
/**
* Implements hook_form_node_programme_form_alter().
*/
#[LegacyHook]
function mon_module_form_node_programme_form_alter(&$form, FormStateInterface $form_state, $form_id) {
\Drupal::service(FormHooks::class)->formProgrammeAlter($form, $form_state, $form_id);
}C'est l'attribut #[LegacyHook] qui fait le travail : à partir de Drupal 11.1, il indique au core de ne pas exécuter la fonction procédurale, pour éviter le doublon avec l'implémentation orientée objet. Sur Drupal 10, qui l'ignore, seule la fonction procédurale s'exécute.
Dès que votre module exige Drupal 11.1 au minimum (core_version_requirement: ^11.1 || ^12), ces deux ajustements deviennent du code mort : vous pouvez les supprimer.
Note : le patch que je mentionnais dans la version initiale de cet article, pour que les attributs soient connus de Drupal 10 et que phpstan valide votre code, n'est plus d'actualité. Le backport des déclarations d'attributs (issue #3482464) n'a finalement survécu que dans la branche 10.3, aujourd'hui non maintenue : sur 10.4 et suivants, les classes Hook et LegacyHook sont tout simplement absentes du core. Il faut donc ajouter une exclusion phpstan pour les attributs inconnus, ou, plus raisonnable en 2026, migrer d'abord vers Drupal 11 puis convertir vos hooks.
Petite astuce pour les performance
Il existe un paramètre pour signaler à drupal qu'il n'est pas nécessaire de scanner les fichiers d'un module à la recherche d'implémentation de hooks (principalement les fichiers .module). On est sur de l'ordre de la micro-optimisation mais bon, ça peut servir.
Attention, les deux noms ont changé en Drupal 11.2, les exemples ci-dessous sont ceux à utiliser aujourd'hui.
Si vous n'utilisez aucun hook procédural (ou uniquement à visée de rétrocompatibilité pour les drupaux < 11.1), vous pouvez ajouter à votre fichier services.yml la clé suivante :
parameters:
mon_module.skip_procedural_hook_scan: true(en Drupal 11.1, ce paramètre s'appelait mon_module.hooks_converted)
Si certains de vos fichiers contiennent encore des hooks procéduraux, alors placez-les tous au début de votre fichier, ajoutez à vos use :
use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;Et placez l'attribut suivant sur la première fonction qui suit votre dernier hook procédural :
#[ProceduralHookScanStop]
function mon_module_une_fonction_qui_nest_pas_un_hook() {
...
}(en Drupal 11.1, cet attribut s'appelait #[StopProceduralHookScan])
Ainsi drupal saura qu'il n'a pas besoin de scanner le reste du fichier à la recherche de potentiels hooks.
Le change record : https://www.drupal.org/node/3490771
Convertir vos hooks automatiquement
Pour un gros module, inutile de tout déplacer à la main : drupal-rector embarque une règle HookConvertRector qui déplace chaque hook procédural dans une classe src/Hook/*Hooks.php. Elle s'utilise avec une configuration dédiée, en deux passes :
# 1. on corrige d'abord les dépréciations, avec votre rector.php habituel
vendor/bin/rector process web/modules/custom/mon_module
# 2. puis on convertit les hooks
vendor/bin/rector process web/modules/custom/mon_module \
--config=vendor/palantirnet/drupal-rector/rector-hook-convert.phpDeux passes, parce que Rector ne repasse pas sur les fichiers qu'il vient de créer : sans la première, les dépréciations seraient recopiées telles quelles dans les nouvelles classes. Un coup de PHPCS derrière ne fait pas de mal non plus, la conversion laisse quelques scories (des use devenus inutiles, notamment).
Et Drupal 12 ?
Drupal 12 est attendu pour la semaine du 7 décembre 2026. Les hooks procéduraux n'y sont pas encore officiellement dépréciés : l'issue qui devait s'en charger est en pause, le core s'orientant plutôt vers la dépréciation des fichiers .module eux-mêmes. Ils devraient donc continuer à fonctionner sur Drupal 12, la suppression n'étant pas envisagée avant Drupal 13. Rien ne presse, donc, mais pour du code neuf, il n'y a plus vraiment de raison d'écrire un hook en procédural.
Le change record de référence, tenu à jour : https://www.drupal.org/node/3442349
Commentaires (0)