Nieuwe manier van hooks schrijven in Drupal 11 uitgelegd
Met Drupal 11.1 is de nieuwe manier van hooks schrijven geïntroduceerd. Tot nu toe werden hooks altijd procedureel geschreven, maar nu kunnen hooks door middel van attributen in PHP-classes worden geschreven. In dit artikel leg ik uit wat er precies verandert, welke voordelen het meebrengt, de verschillende manieren waarop het kan worden toegepast en hoe je de nieuwe manier gebruikt en backward compatible blijft.
Voordelen
Het schrijven van OOP-hooks brengt veel voordelen met zich mee. Hieronder zijn de grootste voordelen uitgewerkt met een korte beschrijving.
Autoloading
Het `.module`-bestand wordt niet meer gescand en methods worden automatisch gedetecteerd via attributen, wat voor een verbeterde bootstraptijd zorgt.
Dependency injection
Omdat de hooks nu OOP zijn, is het mogelijk om services te injecteren.
Testbaarheid
Hooks zitten nu in classes, wat betekent dat ze unit-testbaar zijn zonder Drupal te bootstrappen.
Organisatie
De hooks die je toevoegt, kun je in verschillende classes zetten, wat voor meer mogelijkheden in organisatie zorgt. Je kunt bijvoorbeeld alle hooks die met nodes te maken hebben groeperen in een class.
Een van de grote voordelen van de nieuwe OOP hooks in Drupal 11 is de manier waarop autoloading werkt. Vroeger werden alle .module bestanden standaard ingeladen, ook als daar maar een paar hooks in stonden die je eigenlijk niet nodig had. Dat kostte onnodig tijd. Nu werkt het slimmer: dankzij PSR 4 autoloading worden classes alleen opgehaald als ze echt gebruikt worden. Minder files die je systeem hoeft te openen betekent een snellere start van je site. Voor één module lijkt dat misschien marginaal, maar wie veel modules draait ziet al snel dat de winst optelt.
Implementatie Hook-attribuut
Voorbeeld 1:
library toevoegen op alle formulieren.
Als eerste moet je een class aanmaken in de Drupal\{module_naam}\Hook namespace. Dit kan ook een subdirectory zijn. Binnen je module kan het bestand onder src/Hook worden aangemaakt. Drupal zorgt ervoor dat de class geregistreerd wordt als een autowired service.
In je class kun je een functie aanmaken voor je hook. Deze kan elke naam krijgen, maar in dit geval heb ik hem formAlter genoemd. Boven deze functie kun je de Hook-attribute toevoegen (Drupal\Core\Hook\Attribute\Hook). En hier kun je de hook die je wil implementeren zonder hook_ invullen. Voor dit geval wil ik hook_form_alter gebruiken, dus ziet mijn hook-attribuut er als volgt uit: # [Hook('form_alter')].
<?php
declare(strict_types=1);
namespace Drupal\custom_module\Hook;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Module hooks for altering forms.
*/
class CustomModuleFormHooks {
public function __construct() {}
#[Hook('form_alter')]
public function formAlter(&$form, &$form_state, $form_id): void {
$form['#attached']['library'][] = 'custom_module/form';
}
}
Voorbeeld 2:
Library toevoegen op 1 specifiek formulier. Stel, je wilt de library toevoegen op alleen maar 1 specifiek formulier. Hiervoor zou je de hook_form_FORM_ID_alter gebruiken. In je hook-attribute kun je de hook toevoegen zonder hook_ en met FORM_ID vervangen met je form id van formulier.
<?php
declare(strict_types=1);
namespace Drupal\custom_module\Hook;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Module hooks for altering forms.
*/
class CustomModuleFormHooks {
public function __construct() {}
#[Hook('form_node_article_form_alter')]
public function formAlter(&$form, &$form_state, $form_id): void {
$form['#attached']['library'][] = 'custom_module/form';
}
}
Voorbeeld 3:
Library toevoegen op 2 formulieren. Ook kun je twee of meer hook-attributes onder elkaar zetten, mits deze dezelfde argumenten hebben.
<?php
declare(strict_types=1);
namespace Drupal\custom_module\Hook;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Module hooks for altering forms.
*/
class CustomModuleFormHooks {
public function __construct() {}
#[Hook('form_node_article_form_alter')]
#[Hook('form_node_article_edit_form_alter')]
public function formAlter(&$form, &$form_state, $form_id): void {
$form['#attached']['library'][] = 'custom_module/form';
}
}
Verschillende manieren
Het is ook mogelijk om de hook-attribute op de class te zetten, waarbij je dan de method aan de hook-attribute meegeeft.
<?php
declare(strict_types=1);
namespace Drupal\custom_module\Hook;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Module hooks for altering forms.
*/
#[Hook('form_alter', method: 'formAlter')]
class CustomModuleFormHooks {
public function __construct() {}
public function formAlter(&$form, &$form_state, $form_id): void {
$form['#attached']['library'][] = 'custom_module/form';
}
}
En als je een invoke gebruikt, kun je ook de method weglaten:
<?php
declare(strict_types=1);
namespace Drupal\custom_module\Hook;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Module hooks for altering forms.
*/
#[Hook('form_alter')]
class CustomModuleFormHooks {
public function __construct() {}
public function __invoke(&$form, &$form_state, $form_id): void {
$form['#attached']['library'][] = 'custom_module/form';
}
}
Order hooks
Sinds Drupal 11.2 hebben hooks ook een orderargument. Deze kan gebruikt worden om de hook-implementaties te ordenen.
Hiervoor kun je de volgende functies en class constants gebruiken:
\Drupal\Core\Hook\Order::First
\Drupal\Core\Hook\Order::Last
\Drupal\Core\Hook\OrderBefore
\Drupal\Core\Hook\OrderAfter
Hier zijn enkele voorbeelden:
#[Hook('some_hook', order: Order::First)] // Hook als eerste uitvoeren
#[Hook('some_hook', order: Order::Last)] // Hook als laatste uitvoeren
#[Hook('some_hook', order: new OrderBefore(['other_module']))] // Hook voor een andere modules implementatie uitvoeren
#[Hook('some_hook', order: new OrderAfter(['other_module']))] // Hook na ee n andere modules implementatie uitvoeren
// Hook voor meerdere verschillede methods en classes uitvoeren
#[Hook('some_hook',
order: new OrderBefore(
classesAndMethods: [
[Foo::class, 'someMethod'],
[Bar::class, 'someOtherMethod'],
]
)
)]
// Hook voor meerdere verschillede methods en classes uitvoeren
Nieuwe manier van hooks schrijven in Drupal 11 uitgelegd 6
#[Hook('some_hook',
order: new OrderAfter(
classesAndMethods: [
[Foo::class, 'someMethod'],
[Bar::class, 'someOtherMethod'],
]
)
)]
Backward compatibility
Als je deze nieuwe manier van hooks wil toepassen en Drupal 10.1 en 11.0 wil ondersteunen. Wordt er geadviseerd de nieuwe hooks class te implementeren en deze manueel te registreren als een service. Daarna kan deze als een procedurele shim-implementatie toegevoegd worden.
Stel, je hebt een hook geschreven op de nieuwe manier. Die zal er vergelijkbaar uitzien als dit:
src/Hook/CustomModuleHooks.php
namespace Drupal\custom_module\Hook;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Entity\EntityInterface;
class CustomModuleHooks {
#[Hook('node_create')]
public function nodeCreate(EntityInterface $entity) {
// Your custom code here...
}
}
De volgende stap is een service aanmaken voor je hook class.
custom_module.services.yml
services:
Drupal\custom_module\Hook\CustomModuleHooks:
class: Drupal\custom_module\Hook\CustomModuleHooks
autowire: true
En in je modulebestand kun je de zojuist aangemaakte service aanroepen.
custom_module.module
use Drupal\Core\Hook\Attribute\LegacyHook;
use Drupal\custom_module\Hook\NodeHooks;
use Drupal\Core\Entity\EntityInterface;
#[LegacyHook]
function custom_module_node_create(EntityInterface $entity) {
// Als de hook geen return heeft kan return weggelaten worden.
return \Drupal::service(NodeHooks::class)->nodeCreate($entity);
}
Nu is je hook ook beschikbaar voor al je sites die Drupal 10.1 of 11.0 zijn.
Preprocess hooks
In Drupal 11.2 of minimaal 11.1.8 worden ook preprocess hooks op de nieuwe OOP manier ondersteund. Dit kan echter alleen in modules gebruikt worden; de preprocess-hooks in themes blijven procedureel.
Hooks die nog procedureel blijven
Bij de meeste hooks is het nu mogelijk om deze op een OOP-manier te schrijven. Er zijn echter nog wel wat uitzonderingen. Hieronder staat een lijstje met de hooks die procedural blijven.
- hook_hook_info()
- hook_module_implements_alter()
- hook_install()
- hook_install_tasks()
- hook_install_tasks_alter()
- hook_post_update_NAME()
- hook_requirements()
- hook_schema()
- hook_uninstall()
- hook_update_last_removed()
- hook_update_N()
Handig om te weten
De oude procedurele hooks werken nog steeds. Drupal 11.1 en hoger ondersteunen beide manieren naast elkaar. Voor nieuwe code raden we aan de OOP aanpak te gebruiken, maar bestaande modules hoeven niet meteen herschreven te worden. Dat geeft rust. Je kunt stap voor stap overstappen, terwijl je bestaande code gewoon blijft draaien.
Voor de developers die graag onder de motorkap kijken: Drupal heeft in de core een Hook Service Collector toegevoegd. Die scant tijdens de container build alle services op het Hook attribuut. Als ontwikkelaar hoef je dus alleen maar je class in de juiste map en namespace te zetten en autowire: true te gebruiken. De rest regelt Drupal voor je. Dat is meteen ook de reden dat hooks nu als volwaardige services behandeld kunnen worden.
Het is zelfs mogelijk om meerdere classes binnen één module dezelfde hook te laten implementeren. Net zoals meerdere modules dezelfde hook kunnen gebruiken, kun je binnen je eigen module verschillende service classes aanhaken. Dat kan handig zijn als je meerdere soorten gedrag wilt koppelen aan dezelfde hook. De volgorde van uitvoeren kun je, net als vroeger, beïnvloeden. In Drupal 11.2 is daar de nieuwe order parameter voor gekomen. Daarmee kun je bijvoorbeeld aangeven dat jouw hook altijd als eerste moet draaien, of juist pas nadat een andere module zijn werk heeft gedaan. Voor wie nog op oudere versies werkt is er zelfs een LegacyModuleImplementsAlter attribuut zodat je backwards compatible kunt blijven.
Ook preprocess hooks zijn vanaf Drupal 11.2 via deze nieuwe manier te gebruiken, maar alleen in modules. Denk bijvoorbeeld aan hook_preprocess_page, die je nu in een class met #[Hook('preprocess_page')] kunt zetten. In themes blijven preprocess functies gewoon in .theme bestanden staan. Goed om te weten dus dat dit echt een stap is voor modules en dat themers voorlopig nog de bekende procedurele manier blijven gebruiken.
En wat zegt dit over de toekomst?
Deze stap past helemaal in de koers die Drupal volgt: moderner, object georiënteerd en klaar voor de volgende generatie developers. Het gebruik van attributen en services maakt de code niet alleen schoner en beter testbaar, maar ook fijner om mee te werken. Heb je vragen naar aanleiding van dit artikel? Ik lees en beantwoord ze graag hieronder in het reactie gedeelte.
Praktische links
Support for object oriented hook implementations using autowired services
Preprocess functions in modules now support object-oriented implementations