Event Subscriber y Event Dispatcher. Sistema de gesti贸n de eventos en Drupal.
Resumen de sistemas de eventos
Los sistemas de eventos se utilizan en muchas aplicaciones complejas como una forma de permitir que las extensiones modifiquen el comportamiento del sistema. Un sistema de eventos puede implementarse de diversas maneras, pero en general, los conceptos y componentes que lo conforman son los mismos.
- Suscriptores de eventos (Event Subscribers) - a veces llamados "escuchas", son m茅todos o funciones invocables que reaccionan a un evento que se propaga a trav茅s del registro de eventos.
- Registro de eventos (Event Registry) - donde se recolectan y organizan los suscriptores de eventos.
- Despachador de eventos (Event Dispatcher) - el mecanismo por el cual un evento es iniciado o "despachado" a lo largo del sistema.
- Contexto del evento (Event Context) - muchos eventos requieren un conjunto espec铆fico de datos importantes para los suscriptores. Esto puede ser un simple valor pasado al suscriptor o algo complejo, como una clase especialmente creada que contiene los datos relevantes.
Hooks en Drupal
A lo largo de gran parte de su historia, Drupal ha tenido un sistema rudimentario de eventos mediante "hooks". Veamos c贸mo el concepto de "hooks" se descompone en estos 4 elementos del sistema de eventos.
- Suscriptores de eventos - los hooks de Drupal se registran definiendo una funci贸n con un nombre espec铆fico. Por ejemplo, para suscribirse a un evento llamado "hook_my_event_name", debes definir una funci贸n llamada myprefix_my_event_name(), donde "myprefix" es el nombre de tu m贸dulo o tema.
- Registro de eventos - los hooks de Drupal se almacenan en la cach茅 "cache_bootstrap" bajo el identificador "module_implements". Esto es simplemente un arreglo de m贸dulos que implementan el hook nombrado.
- Despachador de eventos - los hooks se disparan de forma diferente en Drupal 7 y Drupal 8:
- Drupal 7: los hooks se invocan mediante la funci贸n module_invoke_all().
- Drupal 8: los hooks se invocan mediante el m茅todo de servicio \Drupal::moduleHandler()->invokeAll().
- Contexto del evento - el contexto se pasa al suscriptor a trav茅s de par谩metros. Por ejemplo, esta invocaci贸n llamar谩 a todas las implementaciones de "hook_my_event_name" y pasar谩 el par谩metro $some_arbitrary_parameter:
- Drupal 7: module_invoke_all('my_event_name', $some_arbitrary_parameter);
- Drupal 8: \Drupal::moduleHandler()->invokeAll('my_event_name', [$some_arbitrary_parameter]);
Algunas desventajas del enfoque basado en "hooks":
- Los eventos solo se registran durante la reconstrucci贸n de la cach茅.
En general, Drupal busca nuevos hooks solo cuando se construyen ciertas cach茅s. Esto significa que si quieres implementar un nuevo hook en tu sitio, deber谩s reconstruir diferentes cach茅s seg煤n el hook que implementes.
- Solo puede responderse a cada evento una vez por m贸dulo.
Debido a que estos eventos se implementan definiendo funciones con nombres espec铆ficos, solo puede haber una implementaci贸n del evento por m贸dulo o tema. Esto es una limitaci贸n arbitraria en comparaci贸n con otros sistemas de eventos.
- No es f谩cil definir el orden de los eventos.
Drupal determina el orden de los suscriptores a eventos usando el peso de los m贸dulos dentro del sistema. Los m贸dulos y temas tienen un "peso" que determina el orden en que se cargan, y por tanto el orden en que se ejecutan sus eventos. Para evitar este problema, en Drupal 7 se a帽adi贸 "hook_module_implements_alter", un segundo hook al que se debe suscribir tu m贸dulo si quieres cambiar el orden de ejecuci贸n sin cambiar el peso del m贸dulo.
Con la base Symfony en Drupal 8 existe ahora un sistema diferente de eventos. Es un mejor sistema de eventos en la mayor铆a de los casos. Aunque Drupal 8 core no despacha muchos eventos, muchos m贸dulos han comenzado a usar este sistema.
Eventos en Drupal 8
Los eventos en Drupal 8 son muy similares a los de Symfony. Veamos c贸mo se desglosan en los componentes del sistema de eventos:
- Suscriptores de eventos - clases que implementan \Symfony\Component\EventDispatcher\EventSubscriberInterface.
- Despachador de eventos - clases que implementan \Symfony\Component\EventDispatcher\EventDispatcherInterface. Usualmente hay al menos una instancia del despachador proporcionada como servicio, aunque se pueden crear otros despachadores si se desea.
- Registro de eventos - el registro de suscriptores se mantiene dentro del objeto "Event Dispatcher" como un arreglo que contiene el nombre del evento y la prioridad (orden). Al registrar un evento como servicio (ver ejemplos), se registra en el despachador global.
- Contexto del evento - clases que extienden \Symfony\Component\EventDispatcher\Event. Generalmente cada extensi贸n que despacha su propio evento crea una clase Event que contiene los datos relevantes para los suscriptores.
Aprender a usar los eventos en Drupal 8 te ayudar谩 a entender mejor el desarrollo con m贸dulos personalizados y te preparar谩 para un futuro en el que los eventos (esperamos) reemplacen a los hooks. As铆 que vamos a crear un m贸dulo personalizado que muestre c贸mo usar cada uno de estos componentes de eventos en Drupal 8.
Mi primer suscriptor de eventos en Drupal 8
Vamos a crear nuestro primer suscriptor de eventos en Drupal 8 usando algunos eventos b谩sicos. Personalmente me gusta empezar con algo muy simple, as铆 que crearemos un suscriptor que muestra un mensaje al usuario cuando un objeto Config es guardado o eliminado.
Lo primero que necesitamos es un m贸dulo donde ejecutar nuestro c贸digo. Lo he llamado custom_events.
name: Custom Events
type: module
description: Custom/Example event work.
core: 8.x
package: Custom
El siguiente paso es registrar un nuevo suscriptor de eventos en Drupal. Para ello creamos custom_events.services.yml. Si vienes de Drupal 7 y est谩s m谩s familiarizado con los hooks, puedes pensar en este paso como si estuvieras escribiendo la funci贸n "hook_my_event_name" en tu m贸dulo o tema.
services:
# Nombre de este servicio.
my_config_events_subscriber:
# Clase del suscriptor de eventos que escuchar谩 los eventos.
class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber'
# Etiquetado como event_subscriber para registrar este suscriptor con el servicio event_dispatcher.
tags:
- { name: 'event_subscriber' }
Esto es bastante simple, pero analic茅moslo:
1) Definimos un nuevo servicio llamado "my_config_events_subscriber".
2) Establecemos la propiedad "class" con el nombre completo del nuevo clase PHP que crearemos.
3) Definimos la propiedad "tags" y damos la etiqueta "event_subscriber". As铆 se registra el servicio como suscriptor de eventos.
Alternativamente, puedes usar el nombre de clase PHP del suscriptor (sin barra invertida inicial) como el nombre del servicio y omitir la propiedad "class", por ejemplo:
services:
Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber:
tags:
- { name: 'event_subscriber' }
Ahora solo necesitamos escribir la clase suscriptora. Esta clase debe cumplir algunos requisitos:
1. Implementar la interfaz EventSubscriberInterface.
2. Tener un m茅todo getSubscribedEvents() que devuelva un arreglo donde las claves son los nombres de los eventos a los que quieres suscribirte y los valores son los m茅todos de esa clase que se ejecutar谩n.
Aqu铆 est谩 nuestra clase suscriptora, que se suscribe a los eventos de la clase ConfigEvents y ejecuta un m茅todo local para cada uno.
src/EventSubscriber/ConfigEventsSubscriber.php
<?php
namespace Drupal\custom_events\EventSubscriber;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Clase ConfigEventsSubscriber.
*
* @package Drupal\custom_events\EventSubscriber
*/
class ConfigEventsSubscriber implements EventSubscriberInterface {
/**
* {@inheritdoc}
*
* @return array
* Los nombres de eventos a los que escuchar, y los m茅todos a ejecutar.
*/
public static function getSubscribedEvents() {
return [
ConfigEvents::SAVE => 'configSave',
ConfigEvents::DELETE => 'configDelete',
];
}
/**
* Reacciona cuando se guarda un objeto config.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* Evento de CRUD de config.
*/
public function configSave(ConfigCrudEvent $event) {
$config = $event->getConfig();
\Drupal::messenger()->addStatus('Configuraci贸n guardada: ' . $config->getName());
}
/**
* Reacciona cuando se elimina un objeto config.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* Evento de CRUD de config.
*/
public function configDelete(ConfigCrudEvent $event) {
$config = $event->getConfig();
\Drupal::messenger()->addStatus('Configuraci贸n eliminada: ' . $config->getName());
}
}
隆Eso es todo! Parece bastante sencillo, pero revisemos los puntos clave:
- Implementamos la interfaz EventSubscriberInterface.
- Implementamos el m茅todo getSubscribedEvents(), que devuelve un arreglo con pares evento => m茅todo.
- En configSave() y configDelete() esperamos un objeto de tipo ConfigCrudEvent, que tiene el m茅todo getConfig() para obtener el objeto config asociado.
Algunas preguntas que podr铆an surgir:
- 驴Qu茅 es ConfigEvents::SAVE y de d贸nde viene?
Generalmente, cuando defines nuevos eventos creas una constante global con el nombre del evento. En este caso, \Drupal\Core\Config\ConfigEvents define la constante SAVE con valor "config.save".
- 驴Por qu茅 esperamos un objeto ConfigCrudEvent y c贸mo lo sabemos?
Al definir nuevos eventos tambi茅n es com煤n crear una clase espec铆fica para el evento que contenga los datos relevantes y tenga una API sencilla. En este caso, lo sabemos explorando el c贸digo base y la documentaci贸n p煤blica.
Ahora estamos listos para habilitar el m贸dulo y probar el evento. Esperamos que cada vez que se guarde o elimine un objeto de configuraci贸n, veamos un mensaje con el nombre del objeto.
Dado que los objetos de configuraci贸n son muy comunes en Drupal 8, es f谩cil probarlo. La mayor铆a de m贸dulos gestionan su configuraci贸n con objetos config, por lo que podemos instalar y desinstalar m贸dulos para ver qu茅 objetos config se guardan y eliminan.
1. Instala el m贸dulo "custom_events".
2. Instala el m贸dulo "statistics".

Mensaje despu茅s de instalar el m贸dulo statistics.
Parece que se guardaron dos objetos de configuraci贸n: primero core.extension, que gestiona los m贸dulos y temas instalados, y luego statistics.settings.
3. Desinstala el m贸dulo "statistics".

Mensaje despu茅s de desinstalar el m贸dulo statistics.
Esta vez vemos ambos eventos SAVE y DELETE. Se elimin贸 statistics.settings y se guard贸 core.extension.
隆Lo llamar铆a un 茅xito! Nos suscribimos exitosamente a dos eventos clave de Drupal.
Ahora veamos c贸mo crear nuestros propios eventos y despacharlos para que otros m贸dulos los usen.
Mi primer evento Drupal 8 y despachar eventos
Lo primero que debemos decidir es qu茅 tipo de evento vamos a despachar y cu谩ndo. Crearemos un evento para un hook de Drupal que a煤n no tiene evento en core: "hook_user_login".
Empecemos creando una clase que extienda Event, llamaremos a la nueva clase UserLoginEvent. Tambi茅n asegur茅monos de proveer un nombre global para suscribirse al evento.
src/Event/UserLoginEvent.php
<?php
namespace Drupal\custom_events\Event;
use Drupal\user\UserInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Evento que se dispara cuando un usuario inicia sesi贸n.
*/
class UserLoginEvent extends Event {
const EVENT_NAME = 'custom_events_user_login';
/**
* La cuenta de usuario.
*
* @var \Drupal\user\UserInterface
*/
public $account;
/**
* Constructor.
*
* @param \Drupal\user\UserInterface $account
* La cuenta del usuario que inici贸 sesi贸n.
*/
public function __construct(UserInterface $account) {
$this->account = $account;
}
}
- UserLoginEvent::EVENT_NAME es una constante con el valor "custom_events_user_login". Este es el nombre de nuestro nuevo evento personalizado.
- El constructor espera un objeto UserInterface y lo guarda como propiedad del evento, haciendo disponible $account para los suscriptores.
隆Eso es todo!
Ahora solo necesitamos despachar nuestro nuevo evento. Lo haremos durante "hook_user_login". Empecemos creando custom_events.module.
<?php
/**
* @file
* Contiene custom_events.module.
*/
use Drupal\custom_events\Event\UserLoginEvent;
/**
* Implementa hook_user_login().
*/
function custom_events_user_login($account) {
// Instancia nuestro evento.
$event = new UserLoginEvent($account);
// Obt茅n el servicio event_dispatcher y despacha el evento.
$event_dispatcher = \Drupal::service('event_dispatcher');
$event_dispatcher->dispatch(UserLoginEvent::EVENT_NAME, $event);
}
En nuestra implementaci贸n de hook_user_login solo necesitamos:
1. Crear un nuevo objeto de evento UserLoginEvent y pasarle el $account.
2. Obtener el servicio event_dispatcher.
3. Ejecutar dispatch() en event_dispatcher con el nombre del evento y el objeto evento.
隆Eso es todo! Ahora despachamos nuestro evento personalizado cuando un usuario inicia sesi贸n en Drupal.
Ahora terminemos el ejemplo creando un suscriptor para nuestro nuevo evento. Primero actualizamos nuestro archivo services.yml para incluir el nuevo suscriptor.
services:
my_config_events_subscriber:
class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber'
tags:
- { name: 'event_subscriber' }
custom_events_user_login:
class: '\Drupal\custom_events\EventSubscriber\UserLoginSubscriber'
tags:
- { name: 'event_subscriber' }
Como antes, definimos un nuevo servicio y lo marcamos como event_subscriber. Ahora escribimos la clase suscriptora.
src/EventSubscriber/UserLoginSubscriber.php
<?php
namespace Drupal\custom_events\EventSubscriber;
use Drupal\custom_events\Event\UserLoginEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Clase UserLoginSubscriber.
*
* @package Drupal\custom_events\EventSubscriber
*/
class UserLoginSubscriber implements EventSubscriberInterface {
/**
* Conexi贸n a la base de datos.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Formateador de fechas.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
// Constante de clase est谩tica => m茅todo de esta clase.
UserLoginEvent::EVENT_NAME => 'onUserLogin',
];
}
/**
* Suscriptor al evento de inicio de sesi贸n.
*
* @param \Drupal\custom_events\Event\UserLoginEvent $event
* Objeto del evento.
*/
public function onUserLogin(UserLoginEvent $event) {
$database = \Drupal::database();
$dateFormatter = \Drupal::service('date.formatter');
$account_created = $database->select('users_field_data', 'ud')
->fields('ud', ['created'])
->condition('ud.uid', $event->account->id())
->execute()
->fetchField();
\Drupal::messenger()->addStatus(t('Bienvenido, su cuenta fue creada el %created_date.', [
'%created_date' => $dateFormatter->format($account_created, 'short'),
]));
}
}
Resumen:
1. Nos suscribimos al evento UserLoginEvent::EVENT_NAME usando el m茅todo onUserLogin().
2. En onUserLogin accedemos a la propiedad $account (el usuario que acaba de iniciar sesi贸n) del objeto $event y hacemos algunas operaciones.
3. Cuando el usuario inicia sesi贸n, ver谩 un mensaje con la fecha y hora en que se cre贸 su cuenta.

Mensaje tras iniciar sesi贸n.
隆Voil脿! Hemos despachado un nuevo evento personalizado y nos hemos suscrito a 茅l. 隆Somos unos genios!
Prioridades de los suscriptores de eventos
Otra gran caracter铆stica del sistema de eventos es que los suscriptores pueden establecer su propia prioridad dentro del suscriptor, sin modificar el peso del m贸dulo o usar otro hook para cambiar el orden (como ocurr铆a con los hooks).
Esto es muy sencillo de hacer, pero para demostrarlo mejor, escribiremos otro suscriptor de eventos para cuando ya tenemos uno. Vamos a crear un "AnotherConfigEventSubscriber" y establecer prioridades para sus escuchas.
Primero registramos el nuevo suscriptor en services.yml:
services:
my_config_events_subscriber:
class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber'
tags:
- { name: 'event_subscriber' }
custom_events_user_login:
class: '\Drupal\custom_events\EventSubscriber\UserLoginSubscriber'
tags:
- { name: 'event_subscriber' }
another_config_events_subscriber:
class: '\Drupal\custom_events\EventSubscriber\AnotherConfigEventsSubscriber'
tags:
- { name: 'event_subscriber' }
Luego escribimos AnotherConfigEventsSubscriber.php:
<?php
namespace Drupal\custom_events\EventSubscriber;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Clase AnotherConfigEventsSubscriber.
*
* @package Drupal\custom_events\EventSubscriber
*/
class AnotherConfigEventsSubscriber implements EventSubscriberInterface {
/**
* {@inheritdoc}
*
* @return array
* Los nombres de eventos a los que escuchar, y los m茅todos a ejecutar.
*/
public static function getSubscribedEvents() {
return [
ConfigEvents::SAVE => ['configSave', 100],
ConfigEvents::DELETE => ['configDelete', -100],
];
}
/**
* Reacciona cuando se guarda un objeto config.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* Evento de CRUD de config.
*/
public function configSave(ConfigCrudEvent $event) {
$config = $event->getConfig();
\Drupal::messenger()->addStatus('(Otro) Configuraci贸n guardada: ' . $config->getName());
}
/**
* Reacciona cuando se elimina un objeto config.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* Evento de CRUD de config.
*/
public function configDelete(ConfigCrudEvent $event) {
$config = $event->getConfig();
\Drupal::messenger()->addStatus('(Otro) Configuraci贸n eliminada: ' . $config->getName());
}
}
La 煤nica diferencia importante es que en getSubscribedEvents() ahora devolvemos un arreglo donde el primer elemento es el m茅todo local y el segundo la prioridad del suscriptor.
Cambiamos esto:
public static function getSubscribedEvents() {
return [
ConfigEvents::SAVE => 'configSave',
ConfigEvents::DELETE => 'configDelete',
];
}
Por esto:
public static function getSubscribedEvents() {
return [
ConfigEvents::SAVE => ['configSave', 100],
ConfigEvents::DELETE => ['configDelete', -100],
];
}
Los resultados esperados:
- AnotherConfigEventSubscriber::configSave() tiene una prioridad muy alta, por lo que se ejecutar谩 antes que ConfigEventSubscriber::configSave().
- AnotherConfigEventSubscriber::configDelete() tiene una prioridad muy baja, por lo que se ejecutar谩 despu茅s que ConfigEventSubscriber::configDelete().
Veamos el evento SAVE en acci贸n, instalando nuevamente el m贸dulo statistics.

Instalaci贸n del m贸dulo Statistics y vista de los mensajes.
隆Perfecto! Nuestro nuevo suscriptor al evento ConfigEvents::SAVE se ejecut贸 antes que el otro que escribimos. Ahora desinstalemos statistics para ver qu茅 pasa con el evento DELETE.

Desinstalaci贸n del m贸dulo Statistics y vista de los mensajes.
Tambi茅n perfecto! Nuestro nuevo suscriptor para ConfigEvents::DELETE se ejecut贸 despu茅s del anterior debido a su prioridad baja.
Nota: Si un suscriptor se registra sin prioridad, se asigna 0 por defecto.
Referencias:
- Repositorio GitHub - contiene todo el c贸digo funcional presentado en esta gu铆a.
- Documentaci贸n Symfony: Event Listeners y Subscribers - nota que Drupal 8 no usa "listeners" en el sentido Symfony, sino que se enfoca en subscribers.
- Documentaci贸n Symfony: Event Dispatcher
- Art铆culo original del blog - similar a esta gu铆a y con informaci贸n adicional sobre el futuro de Events en Drupal.