Hook ed eventi permettono a componenti differenti di Drupal di comunicare senza dipendere direttamente gli uni dagli altri.
Entrambi seguono un principio simile:
- durante l'esecuzione si verifica un determinato avvenimento
- Drupal o un modulo segnala l'avvenimento
- altri moduli possono registrare del codice da eseguire
- ogni implementazione reagisce senza modificare direttamente il componente originale
I due sistemi non sono però intercambiabili: un hook è un extension point specifico di Drupal, il componente che lo invoca chiede ai moduli installati di contribuire a un'operazione o di alterare determinati dati.
Un evento invece utilizza il componente Event Dispatcher di Symfony che crea un event object e lo invia all'Event Dispatcher, mentre uno o più event subscriber possono reagire.
Gli hook
Gli hook possono essere utilizzati, per esempio, per:
- reagire alla creazione, modifica o eliminazione di un'entità
- modificare un form
- alterare definizioni prodotte da altri moduli
- aggiungere informazioni a un array
- eseguire operazioni durante cron
- dichiarare informazioni richieste da un'API
- reagire all'installazione o alla disinstallazione di un modulo
Hook di reazione
Gli hook di reazione segnalano che qualcosa è avvenuto e permettono di eseguire un'operazione conseguente, ad esempio un'implementazione di hook_entity_insert() può reagire alla creazione di una nuova entità.
Alter hook
Gli alter hook permettono di modificare dati o definizioni già costruiti da Drupal o da un altro modulo, ad esempio hook_form_alter(array &$form) permettere di modificare i form.
- hook_form_alter()
- hook_form_BASE_FORM_ID_alter() ad esempio hook_form_node_form_alter()
- hook_form_FORM_ID_alter()
Hook informativi
Gli hook informativi permettono a un modulo di fornire definizioni o informazioni richieste da un sistema.
- hook_theme()
- hook_mail()
- hook_requirements()
Hook che rimangono procedurali
- hook_install()
- hook_uninstall()
- hook_schema()
- hook_requirements()
- hook_update_N()
- hook_post_update_NAME()
Nel file nome_modulo.install:
<?php
declare(strict_types=1);
use Drupal\nome_modulo\ForecastStatusStorage;
/**
* Implements hook_uninstall().
*/
function nome_modulo_uninstall(): void {
$forecastStatusStorage = \Drupal::service( // qui non esiste la dependency injection quindi usiamo il service container
'nome_modulo.forecast_status_storage',
);
assert(
$forecastStatusStorage
instanceof ForecastStatusStorage,
);
$forecastStatusStorage->deleteAll();
}
Hook OOP
Da Drupal 11.1 la maggior parte degli hook può essere implementata anche attraverso classi e attributi PHP. La classe viene collocata nel namespace Hook, mentre il metodo viene contrassegnato con l'attributo:
#[Hook('nome_hook')]
Il nome passato all'attributo non contiene il prefisso hook_.
Esempio di hook_form_alter() con metodo procedurale nel file nome_modulo.module:
<?php
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_form_alter().
*/
function nome_modulo_form_alter(
array &$form,
FormStateInterface $form_state,
string $form_id,
): void {
// codice che modifica il form
}
Esempio di hook_form_alter() come OOP nel file src/Hook/FormHooks.php:
<?php
declare(strict_types=1);
namespace Drupal\nome_modulo\Hook;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Implements hooks related to forms.
*/
final class FormHooks // il nome della classe è libero //solo form_alter, senza hook prima
{
/**
* Implements hook_form_alter().
*/
public function formAlter( // il nome del metodo è libero
array &$form,
FormStateInterface $form_state,
string $form_id
): void {
// codice che modifica il form
}
}
?>
Esempio completo di hook_form_alter():
<?php
declare(strict_types=1);
namespace Drupal\nome_modulo\Hook;
use Drupal\Core\Entity\EntityFormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\node\NodeInterface;
/**
* Implements hooks related to forms.
*/
final class FormHooks {
/**
* Implements hook_form_BASE_FORM_ID_alter() for node forms.
* funziona sia per il form di aggiunta che di modifica
*/
#[Hook("form_node_form_alter")]
public function formNodeFormAlter(
array &$form,
FormStateInterface $form_state,
string $form_id
): void {
$formObject = $form_state->getFormObject();
if (!$formObject instanceof EntityFormInterface) { //garantisce che il form object permetta di recuperare un'entità
return;
}
$entity = $formObject->getEntity();
if (
!$entity instanceof NodeInterface ||
$entity->bundle() !== "weather_alert" // bundle è il Content Type
) {
return;
}
if (isset($form["title"]["widget"][0]["value"])) {
$form['title']['widget'][0]['value']['#description'] =
new TranslatableMarkup(
'Enter a short title that identifies the weather alert.'
);
}
}
}
Esempio completo di hook_entity_insert() nel file src/Hook/EntityHooks.php:
<?php
declare(strict_types=1);
namespace Drupal\nome_modulo\Hook;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\nome_modulo\Event\NomeModuloEvents;
use Drupal\nome_modulo\Event\WeatherAlertCreatedEvent;
use Drupal\node\NodeInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Implements hooks related to entities.
*/
final class EntityHooks {
/**
* Constructs an EntityHooks object.
*/
public function __construct(
private readonly EventDispatcherInterface $eventDispatcher
) {}
/**
* Implements hook_entity_insert().
*/
#[Hook("entity_insert")]
public function entityInsert(EntityInterface $entity): void {
if (
!$entity instanceof NodeInterface ||
$entity->bundle() !== "weather_alert"
) {
return;
}
$event = new WeatherAlertCreatedEvent($entity); //creazione dell'evento
$this->eventDispatcher->dispatch( // invio dell'evento
$event,
NomeModuloEvents::WEATHER_ALERT_CREATED
);
}
/**
* Implements hook_entity_bundle_field_info_alter().
*/
#[Hook('entity_bundle_field_info_alter')]
public function entityBundleFieldInfoAlter(
array &$fields,
EntityTypeInterface $entityType,
string $bundle,
): void {
if (
$entityType->id() !== 'node' ||
$bundle !== 'weather_alert' ||
!isset($fields['field_alert_end'])
) {
return;
}
$fields['field_alert_end']
->addConstraint(
'WeatherAlertDateRange',
);
}
}
Gli eventi
Event object
Nel file src/Event/NomeModuloEvents.php definiamo gli eventi che vengono lanciati dal modulo:
<?php
declare(strict_types=1);
namespace Drupal\nome_modulo\Event;
/**
* Defines events dispatched by Nome Modulo.
*/
final class NomeModuloEvents {
/**
* Dispatched after a weather alert has been created.
*/
public const WEATHER_ALERT_CREATED = "nome_modulo.weather_alert_created";
}
Nel file src/Event/WeatherAlertCreatedEvent.php creiamo l'event object:
<?php
declare(strict_types=1);
namespace Drupal\nome_modulo\Event;
use Drupal\Component\EventDispatcher\Event;
use Drupal\node\NodeInterface;
/**
* Event dispatched after a weather alert has been created.
*/
final class WeatherAlertCreatedEvent extends Event {
/**
* Constructs a WeatherAlertCreatedEvent object.
*/
public function __construct(
private readonly NodeInterface $alert,
) {}
/**
* Returns the created weather alert.
*/
public function getAlert(): NodeInterface {
return $this->alert;
}
}
Event Dispatcher
L'Event Dispatcher esegue i seguenti compiti:
- individua i subscriber registrati per quel nome
- li ordina in base alla priorità
- richiama i relativi metodi
- passa a ogni metodo lo stesso event object
Per inviare un evento si utilizza:
$event = new WeatherAlertCreatedEvent($entity);
$this->eventDispatcher->dispatch(
$event, //event object
NomeModuloEvents::WEATHER_ALERT_CREATED,
);
Event Subscriber
Un event subscriber è una classe che:
- implementa EventSubscriberInterface
- dichiara gli eventi ascoltati in getSubscribedEvents()
- contiene i metodi richiamati quando gli eventi vengono inviati
- viene registrata come servizio con il tag event_subscriber
Nel file src/EventSubscriber/WeatherEventSubscriber.php:
<?php
declare(strict_types=1);
namespace Drupal\nome_modulo\EventSubscriber;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\nome_modulo\Event\NomeModuloEvents;
use Drupal\nome_modulo\Event\WeatherAlertCreatedEvent;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Reacts to weather and configuration events.
*/
final class WeatherEventSubscriber implements EventSubscriberInterface { // e quindi deve definire un metodo getSubscribedEvents()
/**
* Constructs a WeatherEventSubscriber object.
*/
public function __construct(
private readonly CacheTagsInvalidatorInterface $cacheTagsInvalidator,
private readonly LoggerInterface $logger
) {
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
ConfigEvents::SAVE => 'onConfigSave',
NomeModuloEvents::WEATHER_ALERT_CREATED =>
'onWeatherAlertCreated',
100, // opzionale, per stabilire un ordine quando più subscriber ascoltano lo stesso evento
];
}
/**
* Invalidates forecast caches when module settings are saved.
*/
public function onConfigSave(ConfigCrudEvent $event): void {
$config = $event->getConfig();
if ($config->getName() !== "nome_modulo.settings") {
return;
}
$this->cacheTagsInvalidator->invalidateTags([
'nome_modulo:forecast'
]);
}
/**
* Logs the creation of a weather alert.
*/
public function onWeatherAlertCreated(
WeatherAlertCreatedEvent $event
): void {
$alert = $event->getAlert(); // recupera il nodo
$this->logger->notice(
'Weather alert @title was created with ID @id.',
[
"@title" => $alert->label(),
"@id" => $alert->id(),
]);
}
}
Nel file .services.yml:
services:
# ... servizi definiti in precedenza
nome_modulo.weather_event_subscriber:
class: Drupal\nome_modulo\EventSubscriber\WeatherEventSubscriber
arguments:
- '@cache_tags.invalidator'
- '@logger.channel.nome_modulo'
tags:
- { name: event_subscriber } # senza il tag la classe esisterebbe come servizio, ma non verrebbe richiamata quando gli eventi vengono inviati
Hooks vs eventi
Bisogna implementare un hook quando:
- il core o un modulo espone quello specifico hook
- bisogna alterare un array per riferimento
- si vuole modificare un form tramite Form API
- si deve reagire a un punto del ciclo di vita per il quale esiste un hook
- si deve integrare con un'API Drupal basata su hook
Bisogna utilizzare un event subscriber quando:
- il core o un modulo invia già l'evento necessario
- si vuole reagire a un evento Symfony come request, response o exception
- si vuole ricevere un event object tipizzato
- l'ordine dei subscriber deve essere controllato tramite priorità
- il proprio modulo deve notificare un avvenimento applicativo ad altri componenti