Для чего нам Event Subscriber
В Drupal 8 расширить стандартное поведение скриптов можно разными способами:
- хуки;
- переопределение сервисов через ServiceProviderBase;
- переопределение классов плагинов используя хуки;
- события и подписчики на события.
В Drupal 8 события пришли из компонента Symfony EventDispatcher, который реализует архитектурный шаблон Mediator (Посредник). Идея этого шаблона в том, что связать разные классы друг с другом не напрямую, а используя посредник. Классы в этом шаблоне не знают друг о друге, но в тоже время они могут взаимодействовать между собой. Данный подход позволяет делать приложение гибче, а реализацию классов проще, ведь не нужно описывать все возможные связи. Как мы можем видеть, хуки в Drupal 7 это тоже реализация данного шаблона в парадигме процедурного программировании. В Drupal 8 хуки и события сосуществуют вместе, но в Drupal 9 от хуков будут отказываться в пользу событий, реализованных через объектно-ориентированный подход Уже сейчас можно использовать модуль Hook Event Dispatcher, который реализует события для хук из ядра.
Для некоторых событий есть аналоги в виде хуков, как например EntityTypeEvents::CREATE
и hook_ENTITY_TYPE_create()
(например hook_node_type_create
или hook_comment_type_create
), но это скорее исключение.
Виды событий
Количество событий в ядре неуклонно растет. Список событий вы можете найти здесь:
Как вы видите мы должны использовать события:
- при работе с запросами к сайту (KernelEvents)
- при возвращении ответа от сайта (KernelEvents)
- при обработке конфигураций (ConfigEvents)
- при работе с типами сущностей (EntityTypeEvents)
- при создании, обновлении и удалении хранилищ для полей (FieldStorageDefinitionEvents)
- при работе с миграциями (MigrateEvents)
- при обрабатывании путей (RoutingEvents)
- и т.д.
Система событий
Система событий в Drupal 8 состоят из 3 частей:
- Event Dispatcher - Реестр событий. Хранит информацию о всех событиях в системе, отсортированных по приоритетам. Используется для запуска событий. Вызывается через
\Drupal::service('event_dispatcher')
(смотрите реализацию в core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php). - Event Subscribers – Подписчики на события. В каждом подписчике определяются слушатели, которые будут запущены при возникновении события.
- Event - событие. Для каждого события определяется отдельный класс расширяющий
\Symfony\Component\EventDispatcher\Event
, в который помещаются данные для дальнейшей обработки.
Подписчики на события
Давайте рассмотрим на примере, как работают подписчики на события.
В начале определяется подписчик на событие в *.services.yml файле. Пример из core/modules/user/user.services.yml:
# Название подписчика
user_maintenance_mode_subscriber:
# Класс подписчика, где будет реализовываться логика.
class: Drupal\user\EventSubscriber\MaintenanceModeSubscriber
# Сервисы, которые будут использоваться в подписчике.
arguments: ['@maintenance_mode', '@current_user']
# Используя тег “event_subscriber” мы обозначаем этот сервис как подписчик.
# Он добавится в Event Dispatcher сервис.
tags:
- { name: event_subscriber }
Реализация подписчика на примере core/modules/user/src/EventSubscriber/MaintenanceModeSubscriber.php
namespace Drupal\user\EventSubscriber;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\MaintenanceModeInterface;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Maintenance mode subscriber to log out users.
*/
class MaintenanceModeSubscriber implements EventSubscriberInterface {
/**
* The maintenance mode.
*/
protected $maintenanceMode;
/**
* The current account.
*/
protected $account;
/**
* Constructs a new MaintenanceModeSubscriber.
*/
public function __construct(MaintenanceModeInterface $maintenance_mode, AccountInterface $account) {
// Подключаем сервисы, которые мы определили в описании сервиса в user.services.yml
$this->maintenanceMode = $maintenance_mode;
$this->account = $account;
}
/**
* Метод getSubscribedEvents() является обязательным и служит
* для описания слушателей.
*/
public static function getSubscribedEvents() {
// Подписываемся на событие KernelEvents::REQUEST.
// Когда событие произойдет, то запустится метод-слушатель onKernelRequestMaintenance в этом же классе.
// 31 - приоритет слушателя.
$events[KernelEvents::REQUEST][] = ['onKernelRequestMaintenance', 31];
return $events;
}
/**
* Logout users if site is in maintenance mode.
*/
public function onKernelRequestMaintenance(GetResponseEvent $event) {
// Непосредственно реализация слушателя.
$request = $event->getRequest();
$route_match = RouteMatch::createFromRequest($request);
if ($this->maintenanceMode->applies($route_match)) {
// If the site is offline, log out unprivileged users.
if ($this->account->isAuthenticated() && !$this->maintenanceMode->exempt($this->account)) {
user_logout();
// Redirect to homepage.
$event->setResponse(
new RedirectResponse(Url::fromRoute('<front>')->toString())
);
}
}
}
}
Прерывание работы события.
Слушатели вызываются один за одним. Порядок определяется их приоритетом. Чем больше приоритет, тем раньше этот слушатель вызовется. Тут нужно быть внимательным и не перепутать с понятием “вес”, который работает наоборот.
Можно сделать так, чтобы прекратить вызов слушателей. Для этого есть метод Symfony\Component\EventDispatcher\Event::stopPropagation()
. Если этот метод был вызван в каком-либо из слушателей, то следующие слушатели не будут вызваны.
Пример из core/modules/system/src/SystemConfigSubscriber.php:
/**
* Инициализация слушателей.
*/
public static function getSubscribedEvents() {
$events[ConfigEvents::SAVE][] = ['onConfigSave', 0];
// Проверка на пустое значение имеет высокий приоритет,
// чтобы остановить дальнейшую обработку событий если конфигурация пустая.
$events[ConfigEvents::IMPORT_VALIDATE][] = ['onConfigImporterValidateNotEmpty', 512];
return $events;
}
/**
* Если конфигурация пуста, то не нужно её импортировать, т.к. это удалит имеющуюся конфигурацию.
* Останавливаем процесс импорта на данном этапе.
*/
public function onConfigImporterValidateNotEmpty(ConfigImporterEvent $event) {
$importList = $event->getConfigImporter()->getStorageComparer()->getSourceStorage()->listAll();
if (empty($importList)) {
$event->getConfigImporter()->logError($this->t('This import is empty and if applied would delete all of your configuration, so has been rejected.'));
$event->stopPropagation();
}
}
Определение слушателей динамически.
Слушатели могут быть определены и “на лету”. Давайте рассмотрим пример из core/lib/Drupal/Core/Action/Plugin/Action/GotoAction.php:
$response = new RedirectResponse($url);
// Слушатель события определенный динамически.
$listener = function ($event) use ($response) {
$event->setResponse($response);
};
// Добавляем слушатель события в реестр событий используя сервис “event_dispatcher”.
$this->dispatcher->addListener(KernelEvents::RESPONSE, $listener);
В данном примере при возникновении события KernelEvents::RESPONSE
(ответ от сайта был сформирован, но еще не отправлен) происходит замена ответа на редирект на другую страницу. Как вы видите слушатель был создан “на лету” и он будет выполнен только во время данного запроса.
Определение событий
В процессе разработки модулей нам нужно предоставлять интерфейс для изменения данных другим разработчикам и уведомлять другие части системы о произошедших действиях. Как было уже сказано, хуки постепенно отживают свое. Будем переходить на события. Давайте разберемся как это делать на примере Route Events в ядре Drupal.
В core/lib/Drupal/Core/Routing/RoutingEvents.php определяются названия событий. Обычно для этих целей используются константы. Да, можно было определить названия событий в классе события, но тогда константы можно было бы переопределить при расширении класса события. Чтобы защитить вашу систему от вас самих, названия событий определили в отдельном финальном классе, который вы не сможете расширить.
namespace Drupal\Core\Routing;
/**
* Contains all events thrown in the core routing component.
*/
final class RoutingEvents {
/**
* Name of the event fired during route collection to allow new routes.
*/
const DYNAMIC = 'routing.route_dynamic';
/**
* Name of the event fired during route collection to allow changes to routes.
*/
const ALTER = 'routing.route_alter';
/**
* Name of the event fired to indicate route building has ended.
*/
const FINISHED = 'routing.route_finished';
}
Непосредственно класс события реализован в core/lib/Drupal/Core/Routing/RouteBuildEvent.php. Как мы видим здесь реализован конструктор, который принимает переменную и функция getRouteCollection(), которая отдает эту переменную. Это все. Обычно класс события не содержит в себе логику, являясь лишь хранилищем для данных.
namespace Drupal\Core\Routing;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\Routing\RouteCollection;
/**
* Represents route building information as event.
*/
class RouteBuildEvent extends Event {
/**
* The route collection.
*/
protected $routeCollection;
/**
* Constructs a RouteBuildEvent object.
*/
public function __construct(RouteCollection $route_collection) {
$this->routeCollection = $route_collection;
}
/**
* Gets the route collection.
*/
public function getRouteCollection() {
return $this->routeCollection;
}
}
Как это вызывается? Для того, чтобы вызвать событие нам нужен объект сервиса event_dispatcher
и метод dispatch
: \Drupal::service(“event_dispatcher”)->dispatch(Event::NAME, $event)
Рассмотрим пример core/lib/Drupal/Core/Routing/RouteBuilder.php
namespace Drupal\Core\Routing;
/**
* Managing class for rebuilding the router table.
*/
class RouteBuilder implements RouteBuilderInterface, DestructableInterface {
/**
* The event dispatcher to notify of routes.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $dispatcher;
...
public function rebuild() {
...
// DYNAMIC is supposed to be used to add new routes based upon all the
// static defined ones.
$this->dispatcher->dispatch(RoutingEvents::DYNAMIC, new RouteBuildEvent($collection));
// ALTER is the final step to alter all the existing routes. We cannot stop
// people from adding new routes here, but we define two separate steps to
// make it clear.
$this->dispatcher->dispatch(RoutingEvents::ALTER, new RouteBuildEvent($collection));
$this->checkProvider->setChecks($collection);
...
}
}
Ссылки: