logo

Extra Block Types (EBT) - Nueva experiencia con Layout Builder❗

Extra Block Types (EBT): tipos de bloques con estilo y personalizables: Presentaciones de diapositivas, Pestañas, Tarjetas, Acordeones y muchos más. Configuraciones integradas para fondo, DOM Box y plugins de JavaScript. Experimenta hoy el futuro de la construcción de diseños.

Módulos de demostración EBT Descargar módulos EBT

❗Extra Paragraph Types (EPT) - Nueva experiencia con Paragraphs

Extra Paragraph Types (EPT): conjunto de módulos basado en párrafos de forma análoga.

Módulos de demostración EPT Descargar módulos EPT

Scroll

Event Subscriber y Event Dispatcher. Sistema de gestión de eventos en Drupal.

20/06/2025, by Ivan

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:
    1. Drupal 7: los hooks se invocan mediante la función module_invoke_all().
    2. 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:
    1. Drupal 7: module_invoke_all('my_event_name', $some_arbitrary_parameter);
    2. 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".

events-1-installed

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".

events-2-uninstalled

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.

events-3-login

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.

events-4-installed-priorities

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.

events-5-uninstalled-priorities

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:

Drupal’s online documentation is © 2000-2020 by the individual contributors and can be used in accordance with the Creative Commons License, Attribution-ShareAlike 2.0. PHP code is distributed under the GNU General Public License.