Event Subscriber и Event Dispatcher. Система работы с событиями в Drupal.
Обзор систем событий
Системы событий используются во многих сложных приложениях как способ позволить расширениям изменять работу системы. Система событий может быть реализована различными способами, но в целом концепции и компоненты, составляющие систему, одинаковы.
- Подписчики на события (Event Subscribers) - иногда называемые «слушателями», являются вызываемыми методами или функциями, которые реагируют на событие, распространяемое по всему реестру событий.
- Реестр событий (Event Registry) - где собираются и сортируются подписчики событий.
- Диспетчер событий (Event Dispatcher) - механизм, при котором событие инициируется или «отправляется» по всей системе.
- Контекст события (Event Context) - для многих событий требуется определенный набор данных, который важен для подписчиков события. Это может быть просто значение, передаваемое подписчику на событие, или сложное, например, специально созданный класс, содержащий соответствующие данные.
Drupal Хуки (Hooks)
На протяжении большей части своего существования у Drupal была рудиментарная система событий посредством «хуков». Давайте посмотрим, как понятие «хуков» разбивается на эти 4 элемента системы событий.
- Подписчики на события - хуки Drupal регистрируются в системе путем определения функции с определенным именем. Например, если вы хотите подписаться на подготовленное событие «hook_my_event_name», вы должны определить новую функцию с именем myprefix_my_event_name (), где «myprefix» - это имя вашего модуля или темы.
- Реестр событий - хуки Drupal хранятся в корзине «cache_boostrap» под идентификатором «module_implements». Это просто массив модулей, которые реализуют ловушку, обозначаемую именем самой ловушки.
- Диспетчер событий - хуки отправляются по-разному в Drupal 7 - против Drupal8:
1) Drupal 7-: хуки рассылаются с помощью функции module_invoke_all()
2) Drupal 8: хуки отправляются через сервисный метод \Drupal::moduleHandler()->invokeAll().
- Контекст события - контекст передается подписчику через параметры абоненту. Например, эта диспетчеризация будет выполнять все реализации "hook_my_event_name" и передавать параметр $ 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]);
Некоторые недостатки подхода «хуков» к событиям:
- Регистрирует события только во время перестройки кэша.
Вообще говоря, Drupal ищет новые хуки только при построении определенных кешей. Это означает, что если вы хотите реализовать новый хук на своем сайте, вам придется перестраивать различные кэши в зависимости от хука, который вы реализуете.
- Может реагировать на каждое событие только один раз для каждого модуля.
Поскольку эти события реализуются путем определения очень специфических имен функций, в каждом модуле или теме может быть только одна реализация события. Это произвольное ограничение по сравнению с другими системами событий.
- Невозможно легко определить порядок событий.
Drupal определяет порядок подписчиков событий с помощью модулей заказов, взвешенных в большей системе. Модули и темы Drupal имеют «вес» в системе. Этот «вес» определяет порядок загрузки модулей, и, следовательно, события заказа отправляются своим подписчикам. Обход этой проблемы был добавлен позднее в Drupal 7 посредством "hook_module_implements_alter", второго события, на которое должен подписаться ваш модуль, если вы хотите изменить порядок выполнения ловушек без изменения веса вашего модуля.
С основанием Symfony в Drupal 8 теперь существует другая система событий. Лучшая система событий в большинстве случаев. Хотя в ядре Drupal 8 не отправляется много событий, многие модули начали использовать эту систему.
Drupal 8 События
События Drupal 8 очень похожи на события Symfony. Давайте посмотрим, как это разбивается на наш список компонентов системы событий.
- Подписчики на события - класс, который реализует \Symfony\Component\EventDispatcher\EventSubscriberInterface.
- Диспетчер событий - класс, который реализует \Symfony\Component\EventDispatcher\EventDispatcherInterface. Обычно, по крайней мере, один экземпляр диспетчера событий предоставляется в качестве службы для системы, но при желании могут быть созданы другие диспетчеры.
- Реестр событий. Реестр для подписчиков хранится в объекте «Диспетчер событий» в виде массива, содержащего имя события и приоритет события (порядок). При регистрации события в качестве службы (см. Примеры) это событие регистрируется в глобально доступном диспетчере.
- Контекст события - класс, который расширяет класс \Symfony\Component\EventDispatcher\Event. Обычно каждое расширение, которое отправляет свое собственное событие, создает новый тип класса Event, который содержит соответствующие данные, необходимые подписчикам.
Обучение использованию событий Drupal 8 поможет вам лучше понять разработку с помощью пользовательских модулей и подготовит вас к будущему, в котором события (будем надеяться) заменят хуки. Итак, давайте создадим пользовательский модуль, который показывает, как использовать каждый из этих компонентов событий в Drupal 8.
Мой первый подписчик на событие Drupal 8
Давайте создадим нашего первого подписчика на события в Drupal 8, используя некоторые основные события. Мне лично нравится делать что-то очень простое для начала, поэтому мы собираемся создать подписчика на событие, который показывает пользователю сообщение, когда объект Config сохраняется или удаляется.
Первое, что нам нужно, это модуль, в котором мы собираемся выполнять свою работу. Я назвал мои custom_events.
name: Custom Events type: module description: Custom/Example event work. core: 8.x package: Custom
Следующим шагом мы хотим зарегистрировать нового подписчика на события в Drupal. Для этого нам нужно создать custom_events.services.yml. Если вы пришли из Drupal7 и более знакомы с системой хуков, то вы можете думать об этом шаге так же, как о написании функции "hook_my_event_name" в своем модуле или теме.
services: # Name of this service. my_config_events_subscriber: # Event subscriber class that will listen for the events. class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber' # Tagged as an event_subscriber to register this subscriber with the event_dispatch service. tags: - { name: 'event_subscriber' }
Это довольно просто, но давайте немного разберемся с этим.
1) Мы определяем новый сервис с именем "my_config_events_subscriber"
2) Мы устанавливаем его свойство "class" для глобального имени нового класса PHP, который мы создадим.
3) Мы определяем свойство «tags» и предоставляем тег с именем «event_subscriber». Вот как сервис регистрируется в системе как подписчик событий.
В качестве альтернативы вы можете использовать PHP-класс подписчика на событие (без обратной косой черты) в качестве имени службы и пропустить свойство «class», например:
services: # Name of this service, using the event subscriber class that will listen for the events. Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber: tags: - { name: 'event_subscriber' }
Теперь нам нужно только написать класс подписчика на событие. Для этого класса есть несколько требований, которые мы хотим сделать:
1. Должен реализовывать класс EventSubscriberInterface.
2. Должен иметь метод getSubscribeedEvents(), который возвращает массив. Ключами массива будут имена событий, на которые вы хотите подписаться, а значения этих ключей - имя метода для этого объекта подписчика на событие.
Вот наш класс подписчиков событий. Он подписывается на события в классе ConfigEvents и выполняет локальный метод для каждого события.
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; /** * Class EntityTypeSubscriber. * * @package Drupal\custom_events\EventSubscriber */ class ConfigEventsSubscriber implements EventSubscriberInterface { /** * {@inheritdoc} * * @return array * The event names to listen for, and the methods that should be executed. */ public static function getSubscribedEvents() { return [ ConfigEvents::SAVE => 'configSave', ConfigEvents::DELETE => 'configDelete', ]; } /** * React to a config object being saved. * * @param \Drupal\Core\Config\ConfigCrudEvent $event * Config crud event. */ public function configSave(ConfigCrudEvent $event) { $config = $event->getConfig(); \Drupal::messenger()->addStatus('Saved config: ' . $config->getName()); } /** * React to a config object being deleted. * * @param \Drupal\Core\Config\ConfigCrudEvent $event * Config crud event. */ public function configDelete(ConfigCrudEvent $event) { $config = $event->getConfig(); \Drupal::messenger()->addStatus('Deleted config: ' . $config->getName()); } }
Это оно! Это выглядит довольно просто, но давайте пройдемся по этому вопросу и обратим внимание на важные замечания:
- Мы реализуем класс EventSubscriberInterface.
- Мы реализуем метод getSubscribeedEvents(). Этот метод возвращает массив пар имя/значение события => имя метода.
- В обоих configSave() и configDelete() мы ожидаем объект типа ConfigCrudEvent. Этот объект имеет метод getConfig(), который возвращает объект Config для этого события.
И несколько вопросов, которые могут возникнуть у проницательного наблюдателя:
- Что такое ConfigEvents::SAVE и откуда он взялся?
Обычно при определении новых событий вы создаете глобально доступную константу, значением которой является имя события. В этом случае \Drupal\Core\Config\ConfigEvents имеет постоянный SAVE, и его значение равно «config.save».
- Почему мы ожидали объект ConfigCrudEvent и как мы узнали это?
При определении новых событий также принято, что вы создаете новый тип объекта, который является особенным для вашего события, содержит необходимые данные и имеет простой API для этих данных. На данный момент мы лучше всего можем определить ожидаемый объект события, изучив базу кода и общедоступную документацию API.
Я думаю, что мы готовы включить модуль и протестировать это событие. То, что мы ожидаем, это то, что всякий раз, когда объект конфигурации сохраняется или удаляется Drupal, мы должны видеть сообщение, которое содержит имя объекта конфигурации.
Поскольку объекты конфигурации настолько распространены в Drupal 8, это довольно легко попробовать. Большинство модулей управляют своими настройками с помощью объектов конфигурации, поэтому мы должны иметь возможность просто установить и удалить модуль и посмотреть, какие объекты конфигурации они сохраняют во время установки и удаляют во время удаления.
1. Установите модуль "custom_events" самостоятельно.
2. Установить модуль "статистика"
Сообщение после установки модуля статистики.
Похоже, два объекта конфигурации были сохранены! Первым является объект конфигурации core.extension, который управляет установленными модулями и темами. Далее идет объект конфигурации statistics.settings.
3. Удалить модуль "статистика"
Сообщение после удаления модуля статистики.
На этот раз мы видим оба события SAVE и DELETE. Мы видим, что объект конфигурации statistics.settings был удален, а объект конфигурации core.extension был сохранен.
Я бы назвал это успехом! Мы успешно подписались на два основных события Drupal.
Теперь давайте посмотрим, как создавать свои собственные события и отправлять их для использования другими модулями.
Мое первое мероприятие Drupal 8 и отправка событий
Первое, что нам нужно решить, это то, какой тип события мы собираемся отправлять и когда мы собираемся его отправлять. Мы собираемся создать событие для ловушки Drupal, у которой еще нет события в ядре "hook_user_login".
Давайте начнем с создания нового класса, который расширяет Event, мы назовем новый класс UserLoginEvent. Давайте также удостоверимся, что мы предоставляем глобально доступное имя события для подписчиков.
src/Event/UserLoginEvent.php
<?php namespace Drupal\custom_events\Event; use Drupal\user\UserInterface; use Symfony\Component\EventDispatcher\Event; /** * Event that is fired when a user logs in. */ class UserLoginEvent extends Event { const EVENT_NAME = 'custom_events_user_login'; /** * The user account. * * @var \Drupal\user\UserInterface */ public $account; /** * Constructs the object. * * @param \Drupal\user\UserInterface $account * The account of the user logged in. */ public function __construct(UserInterface $account) { $this->account = $account; } }
- UserLoginEvent::EVENT_NAME является константой со значением «custom_events_user_login». Это название нашего нового пользовательского события.
- Конструктор для этого события ожидает объект UserInterface и сохраняет его как свойство события. Это сделает объект $ account доступным для подписчиков этого события.
И это все!
Теперь нам просто нужно отправить наше новое событие. Мы собираемся сделать это во время "hook_user_login". Начните с создания custom_events.module.
<?php /** * @file * Contains custom_events.module. */ use Drupal\custom_events\Event\UserLoginEvent; /** * Implements hook_user_login(). */ function custom_events_user_login($account) { // Instantiate our event. $event = new UserLoginEvent($account); // Get the event_dispatcher service and dispatch the event. $event_dispatcher = \Drupal::service('event_dispatcher'); $event_dispatcher->dispatch(UserLoginEvent::EVENT_NAME, $event); }
Внутри нашей реализации "hook_user_login" нам нужно всего лишь сделать несколько вещей для отправки нашего нового события:
1. Создайте новый пользовательский объект с именем UserLoginEvent и предоставьте его конструктору объект $account, доступный в ловушке.
2. Получить сервис event_dispatcher.
3. Выполните метод dispatch() в службе event_dispatcher. Укажите имя отправляемого события (UserLoginEvent::EVENT_NAME) и только что созданный объект события ($event).
Ну вот и все! Теперь мы отправляем наше пользовательское событие, когда пользователь входит в Drupal.
Далее, давайте завершим наш пример, создав подписчика на событие для нашего нового события. Сначала нам нужно обновить наш файл services.yml, включив подписчика на событие, которое мы напишем.
services: # Name of this service. my_config_events_subscriber: # Event subscriber class that will listen for the events. class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber' # Tagged as an event_subscriber to register this subscriber with the event_dispatch service. tags: - { name: 'event_subscriber' } # Subscriber to the event we dispatch in hook_user_login. custom_events_user_login: class: '\Drupal\custom_events\EventSubscriber\UserLoginSubscriber' tags: - { name: 'event_subscriber' }
Так же, как и раньше. Мы определяем новый сервис и помечаем его как event_subscriber. Теперь нам нужно написать этот класс EventSubscriber.
src/EventSubscriber/UserLoginSubscriber.php
<?php namespace Drupal\custom_events\EventSubscriber; use Drupal\custom_events\Event\UserLoginEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Class UserLoginSubscriber. * * @package Drupal\custom_events\EventSubscriber */ class UserLoginSubscriber implements EventSubscriberInterface { /** * Database connection. * * @var \Drupal\Core\Database\Connection */ protected $database; /** * Date formatter. * * @var \Drupal\Core\Datetime\DateFormatterInterface */ protected $dateFormatter; /** * {@inheritdoc} */ public static function getSubscribedEvents() { return [ // Static class constant => method on this class. UserLoginEvent::EVENT_NAME => 'onUserLogin', ]; } /** * Subscribe to the user login event dispatched. * * @param \Drupal\custom_events\Event\UserLoginEvent $event * Dat event object yo. */ 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('Welcome, your account was created on %created_date.', [ '%created_date' => $dateFormatter->format($account_created, 'short'), ])); } }
Сломано:
1. Мы подписываемся на событие с именем UserLoginEvent::EVENT_NAME с помощью метода onUserLogin() (имя метода, которое мы создали).
2. Во время onUserLogin мы обращаемся к свойству $account (пользователь, который только что вошел в систему) объекта $event и делаем с ним некоторые вещи.
3. Когда пользователь входит в систему, он должен увидеть сообщение с указанием даты и времени, когда он присоединился к сайту.
Сообщение после входа в систему.
Вуаля! Мы оба отправили новое пользовательское событие и подписались на это событие. Мы потрясающие в этом!
Приоритеты подписчиков событий
Еще одна замечательная особенность системы «События» - это возможность подписчика устанавливать собственный приоритет внутри самого подписчика, вместо того, чтобы изменять вес исполнения всего модуля или использовать другой хук для изменения приоритета (как в случае хуков).
Сделать это очень просто, но чтобы лучше всего это продемонстрировать, нам нужно записать другого подписчика на событие, где у нас уже есть подписчик. Давайте напишем «AnotherConfigEventSubscriber» и установим приоритеты для его слушателей.
Сначала мы зарегистрируем нашего нового подписчика на событие в нашем файле services.yml:
services: # Name of this service. my_config_events_subscriber: # Event subscriber class that will listen for the events. class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber' # Tagged as an event_subscriber to register this subscriber with the event_dispatch service. tags: - { name: 'event_subscriber' } # Subscriber to the event we dispatch in hook_user_login. 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' }
Затем мы напишем AnotherConfigEventSubscriber.php:
<?php namespace Drupal\custom_events\EventSubscriber; use Drupal\Core\Config\ConfigCrudEvent; use Drupal\Core\Config\ConfigEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Class EntityTypeSubscriber. * * @package Drupal\custom_events\EventSubscriber */ class AnotherConfigEventsSubscriber implements EventSubscriberInterface { /** * {@inheritdoc} * * @return array * The event names to listen for, and the methods that should be executed. */ public static function getSubscribedEvents() { return [ ConfigEvents::SAVE => ['configSave', 100], ConfigEvents::DELETE => ['configDelete', -100], ]; } /** * React to a config object being saved. * * @param \Drupal\Core\Config\ConfigCrudEvent $event * Config crud event. */ public function configSave(ConfigCrudEvent $event) { $config = $event->getConfig(); \Drupal::messenger()->addStatus('(Another) Saved config: ' . $config->getName()); } /** * React to a config object being deleted. * * @param \Drupal\Core\Config\ConfigCrudEvent $event * Config crud event. */ public function configDelete(ConfigCrudEvent $event) { $config = $event->getConfig(); \Drupal::messenger()->addStatus('(Another) Deleted config: ' . $config->getName()); } }
Практически единственным важным отличием является то, что мы изменили возвращаемый массив в методе getSubscribeedEvents(). Вместо значения для данного события, являющегося строкой с именем локального метода, теперь это массив, где первый элемент в массиве - это имя локального метода, а второй элемент - приоритет этого слушателя.
Итак, мы изменили это:
public static function getSubscribedEvents() { return [ ConfigEvents::SAVE => 'configSave', ConfigEvents::DELETE => 'configDelete', ]; }
На это:
public static function getSubscribedEvents() { return [ ConfigEvents::SAVE => ['configSave', 100], ConfigEvents::DELETE => ['configDelete', -100], ]; }
Результаты, которые мы ожидаем:
- AnotherConfigEventSubscriber::configSave() имеет очень высокий приоритет, поэтому его следует выполнить перед ConfigEventSubscriber::configSave().
- AnotherConfigEventSubscriber::configDelete() имеет очень низкий приоритет, поэтому его следует выполнять после ConfigEventSubscriber::configDelete().
Давайте посмотрим на событие SAVE в действии, снова включив модуль статистики.
Установка модуля Статистика и просмотр сообщений.
Отлично! Наш новый слушатель событий на ConfigEvents::SAVE произошел раньше, чем тот, который мы написали. Теперь давайте удалим модуль статистики и посмотрим, что происходит с событием DELETE.
Удаление модуля статистики и просмотр сообщений.
Тоже отлично! Наш новый прослушиватель событий в ConfigEvents::DELETE был выполнен после того, как мы написали другой, потому что он имеет очень низкий приоритет.
Примечание. При регистрации подписчика на событие без указания приоритета по умолчанию устанавливается значение 0.
Ссылки:
- GitHub repo - содержит весь рабочий код, представленный в этом руководстве.
- Документация Symfony: прослушиватели событий и подписчики - обратите внимание, Drupal 8 не использует «прослушиватели событий» в смысле Symfony. Фокус на подписчиках событий.
- Документация Symfony: Диспетчер событий
- Оригинальное сообщение в блоге - очень похоже на эту страницу руководства. Содержит дополнительную информацию о будущем Events в Drupal.
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.