Hook and Event Reuse
When to Use
When you need to respond to system events (entity operations, form alterations, request processing) without duplicating logic across similar hooks or event subscribers.
Hooks vs Events (Drupal 11+)
| Mechanism | Use When | Example |
|---|---|---|
| Hook classes (attributes) | Standard Drupal operations (form alter, entity presave, theme) | #[Hook('form_alter')], #[Hook('entity_presave')] |
| Event subscribers | Symfony events, custom events | KernelEvents::REQUEST, custom domain events |
| Legacy .module hooks | Legacy code (being phased out) | hook_form_alter(), hook_preprocess_HOOK() |
Decision
| If you need... | Use... | Why |
|---|---|---|
| Alter forms | Hook class with #[Hook('form_alter')] |
Modern, OOP, supports DI and autowiring |
| React to entity save | Hook class with #[Hook('entity_presave')] |
Standard Drupal pattern |
| Custom application event | Event dispatcher + subscribers | Decoupled, reusable across modules |
| Shared hook logic across forms | Base hook class or service method | Avoid duplicating logic in each hook |
Pattern
// Modern hook class (Drupal 11+)
namespace Drupal\mymodule\Hook;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Form\FormStateInterface;
class MyModuleHooks {
public function __construct(
protected MyService $myService,
) {}
#[Hook('form_alter')]
public function formAlter(array &$form, FormStateInterface $form_state, string $form_id): void {
// Shared validation logic via service
if (str_starts_with($form_id, 'node_')) {
$this->myService->addCustomValidation($form, $form_state);
}
}
#[Hook('entity_presave')]
public function entityPresave($entity): void {
if ($entity->getEntityTypeId() === 'node') {
// Shared presave logic
$this->myService->processNode($entity);
}
}
}
// Extract shared logic to service
class MyService {
public function addCustomValidation(array &$form, FormStateInterface $form_state): void {
// Logic used by multiple form_alter implementations
$form['#validate'][] = [$this, 'validateCustomField'];
}
public function processNode($node): void {
// Logic used by multiple entity hooks
if (!$node->get('field_custom')->isEmpty()) {
// Process custom field
}
}
}
Reference: core/lib/Drupal/Core/Hook/, core/modules/system/src/Hook/SystemHooks.php, core/lib/Drupal/Component/EventDispatcher/
Event Subscribers (When Needed)
namespace Drupal\mymodule\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
class MyEventSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents() {
return [
KernelEvents::REQUEST => ['onRequest', 100],
];
}
public function onRequest($event): void {
// Event logic
}
}
Common Mistakes
- Duplicating logic across similar hooks — Extract to service method, call from multiple hooks
- Using event subscribers for standard Drupal operations — Use hook classes (modern) for form_alter, entity hooks, theme hooks
- Complex logic directly in hook — Keep hooks thin, delegate to services
- Not checking entity type/bundle in generic hooks — Always filter in entity_presave, entity_insert, etc.
- Multiple event subscribers doing similar things — Consolidate into one subscriber or extract shared logic to service