Hook ed eventi permettono a componenti differenti di Drupal di comunicare senza dipendere direttamente gli uni dagli altri.

Entrambi seguono un principio simile:

  1. durante l'esecuzione si verifica un determinato avvenimento
  2. Drupal o un modulo segnala l'avvenimento
  3. altri moduli possono registrare del codice da eseguire
  4. 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