Drupal Hooks (Drupal 11)
OOP Hooks (Preferred)
| Location | src/Hook/MyModuleHooks.php |
| Namespace | Drupal\my_module\Hook |
| Auto-registered | Drupal 11.1+ (no services.yml needed) |
| DI support | Constructor injection |
| Testable | Yes — instantiate directly, inject mocks |
Example (Best Practice)
<?php
namespace Drupal\my_module\Hook;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\node\NodeInterface;
final class MyModuleHooks {
use StringTranslationTrait; // Provides $this->t().
public function __construct(
private readonly AccountProxyInterface $currentUser,
) {}
/**
* Implements hook_form_node_article_form_alter().
*
* Prefer targeted form hooks over generic form_alter — they only fire for
* the specific form ID and avoid unnecessary processing.
*/
#[Hook('form_node_article_form_alter')]
public function formNodeArticleFormAlter(array &$form, FormStateInterface $form_state): void {
$form['title']['#title'] = $this->t('Article title');
}
/**
* Implements hook_form_alter().
*
* Use only when acting on multiple forms or the form ID is unknown at
* development time. Check $form_id explicitly.
*/
#[Hook('form_alter')]
public function formAlter(array &$form, FormStateInterface $form_state, string $form_id): void {
if ($form_id === 'node_page_form') {
$form['title']['#description'] = $this->t('Enter a descriptive page title.');
}
}
#[Hook('node_presave')]
public function nodePresave(NodeInterface $node): void {
if ($node->getType() === 'article') {
// Example: stamp the current user's ID on save.
$node->set('uid', $this->currentUser->id());
}
}
#[Hook('theme')]
public function theme(): array {
return [
'my_template' => [
'variables' => ['content' => NULL],
],
];
}
}
Note: If a hook is not executed, verify namespace and location. OOP hooks outside
src/Hook/require manual service registration.
services.yml
| Scenario | Required? |
|---|---|
Class in src/Hook/, namespace Drupal\my_module\Hook |
No — auto-registered (Drupal 11.1+) |
Class outside src/Hook/ (e.g. a custom service) |
Yes — register manually |
When registration is needed — autowire resolves constructor dependencies by type hint:
namespace Drupal\my_module\Service;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Session\AccountProxyInterface;
final class MyCustomService {
public function __construct(
private readonly AccountProxyInterface $currentUser,
) {}
#[Hook('form_alter')]
public function formAlter(...): void {
// ...
}
}
services:
Drupal\my_module\Service\MyCustomService:
autowire: true
Procedural Hooks (.module)
Auto-discovered via my_module_hook_name() naming. Still valid; prefer OOP hooks for new code.
Hooks vs Event Subscribers
| Use Hooks When | Use Event Subscribers When |
|---|---|
form_alter, entity hooks, theme hooks |
reacting to dispatched Symfony events |
| extending Drupal core/contrib behavior | PSR-14 / decoupled systems |
| working with existing Drupal APIs | building loosely coupled logic |
OOP hooks in
src/Hook/are fully testable — instantiate the class directly and inject mocked dependencies.
RULES (IMPORTANT)
- ALWAYS prefer OOP hooks in
src/Hook/for new implementations - NEVER add new hooks in
.moduleunless explicitly requested - If procedural hook exists, suggest refactoring to OOP