Drupal 8: Hooks,Events и Event Subscribers.

Команда ra-don.ru
Наш кумулятивный опыт и статьи от нескольких авторов

Введение в Event в Drupal 8

Сравнение Event и Hooks, как использовать Event и немного о том, чего ожидать в будущем.

Многие современные сложные системы построены с надежной системой Event. Если вы новичок в работе с архитектурой на основе Event, то вы должны знать, что система Event состоит из нескольких ключевых компонентов:

  • Подписчики (Event subscribers) - иногда называемые Listeners  - вызываемые методы или функции, которые реагируют на Event, распространяемое по всему регистру событий.
  • Реестр событий (Event registry) - где Event subscribers собираются и сортируются.
  • Диспетчер событий (Event dispatcher) - механизм, в котором и Event запускается, или «отправляется» по всей системе.
  • Контекст событий (Event context). Для многих Event требуется определенный набор данных, что важно для Event subscribers. Это может быть так же просто, как значение, переданное Event subscriber , или такое сложное, как специально созданный класс, который содержит соответствующие данные.
  • Большую часть жизни у Drupal была рудиментарная система Event по принципу «Hooks».

Давайте рассмотрим, как концепция «Hooks» разбивается на эти 4 элемента системы событий.

Хуки для Drupal

  • Event subscribers - хуки Drupal, регистрируются в системе, определяя функцию с определенным именем. Например, если вы хотите подписаться на созданное событие «hook_my_event_name», вы должны определить новую функцию с именем myprefix_my_event_name(), где «myprefix» - это имя вашего модуля или темы.
  • Реестр событий. Хуки Drupal хранятся в буфере «cache_boostrap» под идентификатором «module_implements». Это просто массив модулей, которые реализуют хук, на который ссылается имя самого хука.
  • Диспетчер событий. Хуки отправляются с использованием метода module_invoke_all () в Drupal 7- и метода службы \ Drupal :: moduleHandler () -> invokeAll () в Drupal 8.
  • Контекст события. Контекст передается в списки с помощью параметров для абонента. Например, эта отправка будет выполнять все реализации «hook_my_event_name» и передать параметр
  • $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]);

Эта простая система добралась до Drupal, но некоторые очевидные недостатки этого подхода заключаются в следующем:

  • Регистрирует события только во время восстановления кэша.
  • Вообще говоря, Drupal ищет новые Хуки при создании определенных кешей. Это означает, что если вы хотите внедрить новый Хук на своем сайте, вам придется перестраивать различные кеши в зависимости от используемого вами Хук.
  • Может реагировать только на каждое Event один раз на модуль.
  • Поскольку эти Event реализуются путем определения очень конкретных имен функций, может быть только одна реализация события на модуль или тему. Это произвольное ограничение по сравнению с другими системами Event.
  • Невозможно легко определить порядок Event.
  • Drupal определяет порядок подписчиков Event по модулям заказа, взвешиваются в большей системе. Модули и темы Drupal имеют «вес» в системе. Этот «вес» определяет загрузку модулей заказа, и поэтому Events заказа отправляются своим подписчикам. Работа над этой проблемой была добавлена ​​позднее в Drupal 7 с помощью «hook_module_implements_alter», второго события, которое ваш модуль должен подписаться, если вы хотите изменить порядок выполнения вашего хук, не изменяя вес вашего модуля.

Основываясь на Symfony в Drupal 8 введена еще одна система Event. Лучшая система Event в большинстве случаев. Хотя в Drupal 8 ядра не так много Events, многие модули начали использовать эту систему.

Drupal 8 Events

Drupal 8 Events - это очень важные Events Symfony. Давайте посмотрим, как это происходит в нашем списке компонентов системы Events.

  • Event Subscribers - класс, который реализует \Symfony\Component\EventDispatcher\EventSubscriberInterface.
  • Диспетчер событий - класс, который реализует \Symfony\Component\EventDispatcher\EventDispatcherInterface. Как правило, по меньшей мере один экземпляр диспетчера событий представляется как служба для системы.
  • Реестр событий - реестр для подписчиков хранится в объекте Event Dispatcher как массив, определяемый именем Event и приоритетом (порядком) события. Если вы регистрируете Event как услугу, то этот подписчик событий будет зарегистрирован в предоставляемых глобально службах: \Drupal::service('event_dispatcher');
  • Контекст событий - класс, расширяющий класс \Symfony\Component\EventDispatcher\Event. Как правило, каждое расширение, которое отправляет свое собственное Event, создаст новый тип класса Event, который содержит необходимые EventSubscriber данных.

Обучение использованию Drupal 8 Events поможет вам лучше разобраться с разработкой с помощью пользовательских модулей и подготовит вас к будущему, когда Events (надеюсь) заменят Хуки. Итак, давайте создадим настраиваемый модуль, который показывает, как использовать каждый из этих компонентов Events в Drupal 8.

Мой первый Drupal 8 Event Subscribers

Давайте создадим нашего первого Event Subscribers в Drupal 8, используя некоторые основные Events. Мне лично нравится делать что-то очень простое, поэтому мы собираемся создать Event Subscribers, который показывает пользователю сообщение, когда объект Config сохраняется или удаляется.

Первое, что нам нужно - это модуль, в котором мы будем делать свою работу. Я назвал мои custom_events.

name: Custom Events
type: module
description: Custom/Example event work.
core: 8.x
package: Custom

Следующий шаг, мы хотим зарегистрировать нового Event Subscribers с Drupal. Для этого нам нужно создать custom_events.services.yml. Если вы пришли из Drupal7 и более знакомы с системой hooks, вы можете подумать об этом шаге так же, как написать функцию «hook_my_event_name» в своем модуле или теме.

services:
  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' }

  # Subscriber to the event we dispatch in hook_user_login, with dependencies injected.
  custom_events_user_login_with_di:
    class: '\Drupal\custom_events\EventSubscriber\UserLoginSubscriberWithDI'
    arguments: ['@database', '@date.formatter']
    tags:
      - { name: 'event_subscriber' }

# todo - Requires Drupal 8.5+ for the messenger service
#   Subscriber to the config events, with dependencies injected.
#  my_config_events_subscriber_with_di:
#    class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriberWithDI'
#    arguments: ['@messenger']
#    tags:
#      - { name: 'event_subscriber' }

Это довольно просто, но давайте немного разобьем его.

Мы определяем новую службу с именем «my_config_events_subscriber»

Мы устанавливаем свойство class для глобального имени нового класса PHP, который мы создадим.

Мы определяем свойство «tags» и предоставляем тег с именем «event_subscriber». Таким образом, услуга регистрируется как подписчик событий с глобально доступным диспетчером.

Теперь нам нужно только написать класс Event Subscribers. Для этого класса есть несколько требований, которые мы хотим убедиться:

  • Должен расширить класс EventSubscriber.
  • Должен иметь метод getSubscribedEvents(), который возвращает массив.

Ключами массива будут имена Events, на которые вы хотите подписаться, а значения этих ключей - имя метода для этого объекта-объекта события.

Вот наш абонентский класс. Он подписывается на Events в классе ConfigEvents и выполняет локальный метод для каждого Events.

<?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_set_message('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_set_message('Deleted config: ' . $config->getName());
  }

}

Это оно! Это выглядит довольно просто, но давайте пройдем дальше и выделим важные заметки:

Мы расширяем класс EventSubscriber.

Мы реализуем метод getSubscribedEvents(). Этот метод возвращает массив пар имени / значения имени метода name => method name. Имена методов «configSave» и «configDelete» полностью составлены. Это может быть все, что вы хотите назвать методом.

В configSave() и configDelete() мы ожидаем объект типа ConfigCrudEvent. Этот объект имеет метод getConfig(), который возвращает объект Config для этого Event.

И несколько вопросов, которые могут возникнуть у проницательного наблюдателя:

  • Что такое ConfigEvents::SAVE и откуда оно взялось?

Это обычная практика при определении новых Events, которые вы создаете глобально доступной константой, значение которой является именем Event. В этом случае \Drupal\Core\Config\ConfigEvents имеет постоянное СОХРАНЕНИЕ, а его значение - «config.save».

  • Почему мы ожидали объект ConfigCrudEvent, и как мы это знаем?

Это также обычная практика при определении новых Events, которые вы создаете для нового типа объектов, которые являются особыми для вашего Event, содержит необходимые данные и имеет простой api для этих данных. На данный момент мы можем наилучшим образом определить объект Event, ожидаемый путем изучения базы кода и публичной документации api.

Я думаю, что мы готовы включить модуль и протестировать этот Event. Мы ожидаем, что всякий раз, когда объект конфигурации сохраняется или удаляется Drupal, мы должны увидеть сообщение, содержащее имя объекта конфигурации.

Поскольку объекты конфигурации настолько распространены в Drupal 8, это довольно простая задача. Большинство модулей управляют своими настройками с помощью объектов конфигурации, поэтому мы должны иметь возможность просто установить и удалить модуль и посмотреть, какие объекты конфигурации они сохраняют во время установки и удалять при удалении.

  1. Установите модуль «custom_events» самостоятельно.
  2. Установите модуль «статистика».                        

друпал

 

 

 

 

 

 

Похоже, были сохранены два конфигурационных объекта! Первым является объект конфигурации core.extension, который управляет установленными модулями и темами. Следующий объект config.settings config.

3.Удалите модуль «статистика».

друпал
 

 

 

 

 

 

На этот раз мы увидели оба Events SAVE и DELETE. Мы видим, что объект конфигурации statistics.settings удален, а объект конфигурации core.extension сохранен.

Мы успешно подписались на два основных Event Drupal.

Теперь давайте посмотрим, как создавать собственные Events и отправлять их для использования другими модулями.

Мой первый Drupal 8 Event и Диспетчер событий 
Прежде всего нам нужно решить, какой тип Event мы собираемся отправить, и когда мы его отправим. Мы собираемся создать Event для хука Drupal, который еще не имеет события в ядре «hook_user_login».

Начнем с создания нового класса, который расширяет Event, мы будем называть новый класс UserLoginEvent. Давайте также удостовериться, что мы предоставляем глобально доступное имя EventSubscribers.
 

<?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». Это имя нашего нового пользовательского события.
  • Конструктор для этого Event ожидает объект UserInterface и сохраняет его как свойство Event. Это сделает объект $ account доступным для EventSubscribers.

Вот и все!

Теперь нам просто нужно отправить наш новый Event. Мы сделаем это во время «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 server and dispatch the event.
  $event_dispatcher = \Drupal::service('event_dispatcher');
  $event_dispatcher->dispatch(UserLoginEvent::EVENT_NAME, $event);
}

Внутри нашей реализации «hook_user_login» нам только нужно сделать несколько вещей, для отправки нашего нового Event:

  1. Создайте новый пользовательский объект с именем UserLoginEvent и предоставьте его конструктору объект $account, доступный в пределах hook.
  2. Получите услугу event_dispatcher.
  3. Выполните метод dispatch() службы event_dispatcher. Укажите имя Event, которое мы отправляем (UserLoginEvent::EVENT_NAME), и объект Event, который мы только что создали ($event).

Мы сделали это!

Теперь отправим наш привычный Event, когда пользователь вошел в Drupal.

Далее, давайте закончим наш пример, создав EventSubscribers для нашего нового Event. Сначала нам нужно обновить наш файл services.yml, чтобы включить EventSubscribers, который мы будем писать.

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' }

  # Subscriber to the event we dispatch in hook_user_login, with dependencies injected.
  custom_events_user_login_with_di:
    class: '\Drupal\custom_events\EventSubscriber\UserLoginSubscriberWithDI'
    arguments: ['@database', '@date.formatter']
    tags:
      - { name: 'event_subscriber' }

# todo - Requires Drupal 8.5+ for the messenger service
#   Subscriber to the config events, with dependencies injected.
#  my_config_events_subscriber_with_di:
#    class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriberWithDI'
#    arguments: ['@messenger']
#    tags:
#      - { name: 'event_subscriber' }

Как и раньше. Мы определяем новую службу и помещаем ее как event_subscriber. Теперь нам нужно написать класс EventSubscriber.

<?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',
    ];
  }

  /**
   * React 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_set_message(t('Welcome, your account was created on %created_date.', [
      '%created_date' => $dateFormatter->format($account_created, 'short'),
    ]));
  }
}

Продолжаем дальше: 

  • Мы подписываемся на Event с именем UserLoginEvent::EVENT_NAME с помощью метода onUserLogin() (имя метода, которое мы составили).
  • Во время onUserLogin мы получаем доступ к свойству $account (пользователю, который только что вошел в систему) объекта $event, и делаем с ним кое-что.
  • Когда пользователь входит в систему, они должны увидеть сообщение с указанием даты и времени, когда они присоединились к сайту.

друпал
 

 

 

Вуаля! Мы сделали обе отправки нового настраиваемого Event и подписались на это Event. Мы молодцы!  

Приоритеты Event Subscriber

Еще одной замечательной особенностью системы Events является способность абонента устанавливать свой собственный приоритет внутри самого абонента, вместо того, чтобы изменять весь объем выполнения всего модуля или использовать другой Хук для изменения приоритета (как с помощью перехватчиков).

Это очень просто, но для того, чтобы лучше всего это показать, нам нужно написать еще одного абонента на Event, в котором у нас уже есть подписчик. Давайте напишем «AnotherConfigEventSubscriber» и зададим приоритеты для listeners.

Во-первых, мы зарегистрируем EventSubscriber в нашем файле 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' }

  # Subscriber to the event we dispatch in hook_user_login, with dependencies injected.
  custom_events_user_login_with_di:
    class: '\Drupal\custom_events\EventSubscriber\UserLoginSubscriberWithDI'
    arguments: ['@database', '@date.formatter']
    tags:
      - { name: 'event_subscriber' }

# todo - Requires Drupal 8.5+ for the messenger service
#   Subscriber to the config events, with dependencies injected.
#  my_config_events_subscriber_with_di:
#    class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriberWithDI'
#    arguments: ['@messenger']
#    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_set_message('(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_set_message('(Another) Deleted config: ' . $config->getName());
  }

}

В значительной степени единственное важное отличие здесь заключается в том, что мы изменили возвращаемый массив в методе getSubscribedEvents(). Вместо значения для данного события, являющегося строкой с именем локального метода, теперь это массив, в котором первым элементом в массиве является имя локального метода, а второй элемент является приоритетом этого слушателя.

Поэтому мы изменили это:

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().

Давайте посмотрим Event SAVE в действии, снова включив модуль статистики.

друпал
 

 

 

 

 

 

 

Здорово! Наш новый listener Event в ConfigEvents::SAVE произошел раньше, чем мы написали. Теперь давайте удалим модуль статистики и посмотрим, что происходит в событии DELETE.

друпал
 

 

 

 

 

 

Также здорово! Наш новый listener  Event  в ConfigEvents::DELETE был выполнен после того, как мы написали, потому что он имеет очень низкий приоритет.

Примечание. Когда вы регистрируете EventSubscriber без указания приоритета, оно по умолчанию равно 0.

Заметный прогресс в событиях в Drupal 8
В очереди возникает проблема, связанная с заменой основы системы крюков на систему событий:

  • Добавьте HookEvent. Этот подход обеспечит общий HookEvent, который ожидает, что пользовательские события будут расширены, и метод для возврата значений из события.
  • Более старый вопрос о замене крючков на события был отложен до Drupal 9. Это обсуждение закончилось более 5 лет назад.
  • Постоянная проблема, связанная с добавлением событий для сопоставления сущностей.
  • Более готовый к использованию, есть отличный модуль с именем hook_event_dispatcher, который предоставляет события для наиболее часто используемых крючков Drupal. Если вы готовы начать использовать меньше крючков и больше событий, этот модуль будет отличной зависимостью для вашего пользовательского кода.
  • Хотя я вряд ли являюсь экспертом в отношении того, что мы ожидаем от Drupal в будущем относительно событий, я надеюсь, что мы увидим много других из них и что они в конечном итоге полностью заменяют крючки.

 

В статье использованы материалы Джонатана Даггерхарта, drupal-разработчика, с публикаций в DrupalPlanet.