Symfony. Компонент EventDispatcher (диспетчер событий).

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

Предисловие

Объектно-ориентированный подход проделал большой путь, ради удобства расширения кода. Создавая классы с четко определенными «обязанностями», ваш код становится более гибким. Другие разработчики могут легко расширять его, через подклассы (наследование), чтобы модифицировать поведение под свои нужды. Но если возникает потребность поделиться своими изменениями с другими разработчиками, которые тоже внесли изменения, — наследование не самый лучший инструмент.

Представьте себе вполне жизненный пример: нужно разработать систему плагинов для проекта. Плагин должен иметь возможность добавлять методы, что-то делать до или после определенного действия, при этом без воздействия на другие плагины. Решить подобную задачу через одно лишь наследование не так-то просто. Даже если бы PHP поддерживал мультинаследование, все равно остались серьезные недостатки.

Диспетчер событий от Symfony, реализует паттерн медиатор простым и эффективным способом, чтобы ваш проект был по-настоящему расширяемым.

Взгляните на простой пример работы компонента HttpKernel. Как только объект Response был создан, может понадобиться, чтобы другие элементы системы могли его модифицировать (например, добавить заголовки для управления кэшем) до того, как тот будет отправлен в браузер пользователя. Для этого, ядро Symfony генерирует событие – kernel.response. Вот как все работает:

  • Обработчик (или слушатель – listener — PHP объект) – «сообщает» объекту диспетчера, что он хочет слушать/обрабатывать событие kernel.response.
  • В какой-то момент, ядро Symfony «говорит» диспетчеру начать обработку события kernel.response, передав объект Event, который, в свою очередь, имеет доступ к объекту Response.
  • Диспетчер информирует (то есть вызывает метод) по очереди каждого слушателя события kernel.response, позволяя им внести свои изменения в Response.

Установка

Вы можете установить компонент двумя разными способами:

  • Через Composer (symfony/event-dispatcher – проект packagist.org)
  • Использовав официальный Git репозиторий (https://github.com/symfony/event-dispatcher)

Затем, подключите автолоадер (vendor/autoload.php), который поставляется вместе с composer, иначе ваше приложение не сможет найти необходимые для работы компонента классы.

События

Когда нужно добавить событие, ему присваивают уникальное имя (например, kernel.response), чтобы по нему подключить слушателей. Создается экземпляр класса Event, и передается каждому слушателю. Как вы позже увидите, объект Event часто содержит информацию, касаемо обрабатываемого события.

Соглашение об именовании (naming convention)

Любая строка может быть уникальным именем события, но принято пользоваться некоторыми правилами:

  • Используйте только строчные латинские буквы, цифры, точки и нижнее подчеркивание (_).
  • Ставьте префикс вначале имени события, затем точку (например order., user.* )
  • В конце пишите подходящий по смыслу глагол в прошедшем времени (например order.placed – заказ.размещен).

Имена событий и объекты событий

Когда диспетчер информирует (вызывает) слушателей, то передает им текущий объект Event с нужной информацией. В некоторых случаях передается подходящий подкласс, в котором содержатся дополнительные методы для извлечения и обновления этой информации во время обработки. Например, событие kernel.response использует FilterResponseEvent, в котором есть методы для извлечения и даже замены текущего объекта Response.

Диспетчер

Диспетчер – ключевой объект системы событий. Обычно создается в единственном экземпляре, управляет регистрацией слушателей (обработчиков). Когда событие передается диспетчеру, вызываются все зарегистрированные (подписанные) на него слушатели (обработчики):

  1. use Symfony\Component\EventDispatcher\EventDispatcher;
  2. $dispatcher = new EventDispatcher();

Подключение (или регистрация) слушателей

В полной мере воспользоваться определенным событием можно, только если к нему подключен хотя бы один слушатель, тогда диспетчер сможет его вызвать в определенный момент. Через метод addListener() добавляется любой код PHP, который можно вызвать (PHP callable) для события.

  1. $listener = new AcmeListener();
  2. $dispatcher>addListener(‘acme.foo.action’, array($listener, ‘onFooAction’));

Метод addListener() принимает до трех аргументов:

  1. Имя события (строка), на которое регистрируется обработчик.
  2. Функция или метод (PHP callable), — будет вызвано при наступлении события.
  3. Приоритет в виде числа. Чем оно выше, тем более высокий приоритет, и тем раньше будет вызван слушатель. Таким образом, можно конфигурировать четко заданную очередь выполнения. По умолчанию приоритет выставляется в значение 0. Если несколько обработчиков с одним и тем же приоритетом, они запускаются в том порядке, в каком были зарегистрированы.
PHP callable – переменная, которая может быть запущена с помощью call_user_func(), и возвращает true, если проверять её через функцию is_callable(). Обе встроены в PHP. Это может быть экземпляр \Closure, объект реализующий метод __invoke() (на самом деле он и является замыканием), строка описывающая функцию; массив в котором имя статической функции либо метода.Пока что вы видели как регистрировать объекты в качестве слушателей. Точно так же можно зарегистрировать замыкание:

  1. use Symfony\Component\EventDispatcher\Event;
  2. $dispatcher>addListener(‘acme.foo.action’, function (Event $event) {
  3. // будет выполнена, когда событие acme.foo.action начнет обрабатываться
  4. });

После того, как слушатель зарегистрирован в диспетчере, он ждет момента, когда будет вызван. В предыдущем примере, когда событие acme.foo.action «срабатывает», запускается метод AcmeListener::onFooAction(), в который передается объект Event в качестве единственного аргумента:

  1. use Symfony\Component\EventDispatcher\Event;
  2. class AcmeListener
  3. {
  4. // …
  5. public function onFooAction(Event $event)
  6. {
  7. // … do something
  8. }
  9. }

Аргумент $event как раз и есть тот самый экземпляр класса-события. В большинстве случаев последний является подклассом, в нем содержится дополнительная информация. (Можете ознакомиться, какой именно подкласс определен, для каждого из событий компонента HttpKernel по умолчанию).

Регистрация слушателей и подписчиков в сервис контейнере (Service Container)

Зарегистрировать сервис с тэгом kernel.event_listener и kernel.event_subscriber не достаточно, чтобы активировать слушателей и подписчиков события. Необходимо так же зарегистрировать «компилятор» под названием RegisterListenersPass().

  1. use Symfony\Component\DependencyInjection\ContainerBuilder;
  2. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
  3. use Symfony\Component\DependencyInjection\Reference;
  4. use Symfony\Component\EventDispatcher\EventDispatcher;
  5. use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
  6. $containerBuilder = new ContainerBuilder(new ParameterBag());
  7. // register the compiler pass that handles the ‘kernel.event_listener’
  8. // and ‘kernel.event_subscriber’ service tags
  9. $containerBuilder>addCompilerPass(new RegisterListenersPass());
  10. $containerBuilder>register(‘event_dispatcher’, EventDispatcher::class);
  11. // регистрация слушателя
  12. $containerBuilder>register(‘listener_service_id’, \AcmeListener::class)
  13. >addTag(‘kernel.event_listener’, array(
  14. ‘event’ => ‘acme.foo.action’,
  15. ‘method’ => ‘onFooAction’,
  16. ));
  17. // регистрация подписчика
  18. $containerBuilder>register(‘subscriber_service_id’, \AcmeSubscriber::class)
  19. >addTag(‘kernel.event_subscriber’);

По умолчанию «компилятор» предполагает id диспетчера – event_dispatcher; что тэг слушателя – kernel.event_listener и тэг подписчика – kernel.event_subscriber. Вы можете поменять перечисленные умолчания, передав свои значения в конструктор RegisterListenerPass.

Создание и обработка события

В дополнение к регистрации слушателей на существующие события, вы можете создать свои. Такое бывает полезно для распространяемых библиотек или когда нужно обойтись без жесткого связывания компонентов вашей системы, дабы та оставалась гибкой.

Создание подкласса Event

Представьте, вы хотите создать новое событие – order.placed – которое обрабатывалось бы каждый раз, когда покупатель заказывает товар в вашем приложении (сайте). Во время обработки будет передан экземпляр подкласса Event, содержащий информацию о заказе. Начнем с написания данного подкласса:

  1. namespace Acme\Store\Event;
  2. use Symfony\Component\EventDispatcher\Event;
  3. use Acme\Store\Order;
  4. /**
  5. * Событие order.placed обрабатывается каждый раз,
  6. * когда заказ создается в системе
  7. */
  8. class OrderPlacedEvent extends Event
  9. {
  10. const NAME = ‘order.placed’;
  11. protected $order;
  12. public function __construct(Order $order)
  13. {
  14. $this>order = $order;
  15. }
  16. public function getOrder()
  17. {
  18. return $this>order;
  19. }
  20. }

Теперь каждый слушатель имеет доступ к заказу через метод getOrder()

Если дополнительные данные не нужны слушателям события, можете использовать экземпляр класса Event, не расширяя его. В таком случае, опишите событие и его имя в основном классе StoreEvents, подобно KernelEvents.

Обработка события

Метод dispatch() информирует (запускает) всех слушателей. Принимает два аргумента: имя события и объект Event, который будет передан каждому из них.

  1. use Acme\Store\Order;
  2. use Acme\Store\Event\OrderPlacedEvent;
  3. // заказ каким-то образом создается или извлекается из БД
  4. $order = new Order();
  5. // …
  6. // создаем OrderPlacedEvent and обрабатываем его
  7. $event = new OrderPlacedEvent($order);
  8. $dispatcher>dispatch(OrderPlacedEvent::NAME, $event);

Обратите внимание, специальный объект OrderPlacedEvent был создан и передан в dispatch(). Теперь каждый слушатель события order.placed получит его.

Использование подписчиков

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

Есть другой способ «слушать» — через подписчиков (subscribers). Это PHP класс, который в состоянии «сообщить» диспетчеру, на какие именно события его следует подписать. Подписчик реализует интерфейс EventSubscriberInterface, в нем единственный статически метод – getSubscribedEvents(). Рассмотрим пример подписчика на события kernel.response и order.placed:

  1. namespace Acme\Store\Event;
  2. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  3. use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
  4. use Symfony\Component\HttpKernel\KernelEvents;
  5. use Acme\Store\Event\OrderPlacedEvent;
  6. class StoreSubscriber implements EventSubscriberInterface
  7. {
  8. public static function getSubscribedEvents()
  9. {
  10. return array(
  11. KernelEvents::RESPONSE => array(
  12. array(‘onKernelResponsePre’, 10),
  13. array(‘onKernelResponsePost’, 10),
  14. ),
  15. OrderPlacedEvent::NAME => ‘onStoreOrder’,
  16. );
  17. }
  18. public function onKernelResponsePre(FilterResponseEvent $event)
  19. {
  20. // …
  21. }
  22. public function onKernelResponsePost(FilterResponseEvent $event)
  23. {
  24. // …
  25. }
  26. public function onStoreOrder(OrderPlacedEvent $event)
  27. {
  28. // …
  29. }
  30. }

Очень похож на слушателя, за исключением того, что данный класс сам «говорит» диспетчеру какие события будет обрабатывать. Для регистрации подписчика в диспетчере, используйте метод addSubscriber():

  1. use Acme\Store\Event\StoreSubscriber;
  2. // …
  3. $subscriber = new StoreSubscriber();
  4. $dispatcher>addSubscriber($subscriber);

Диспетчер автоматически зарегистрирует его для каждого события, которые тот вернет через метод getSubscribedEvents(). Возвращается массив с индексами – именами событий. В качестве значений — именам функций/методов. Приведенный выше пример показывает, как зарегистрировать несколько методов на одно и то же событие, а так же указать приоритет для каждого из них. Чем выше приоритет, тем раньше метод будет вызван. Когда событие kernel.response будет сгенерировано, сначала сработает метод onKernelResponsePre(), затем onKernelResponsePost(), именно в таком порядке.

Прерывание цепочки обработки события

В некоторых случаях, имеет смысл прекратить выполнение цепочки события, чтобы остальные слушатели (обработчики) не запускались. Другими словами, нужна возможность сказать диспетчеру, чтобы тот прекратил запускать слушателей с более низким приоритетом. Такое возможно из самого слушателя, через вызов метода stopPropagation():

  1. use Acme\Store\Event\OrderPlacedEvent;
  2. public function onStoreOrder(OrderPlacedEvent $event)
  3. {
  4. // …
  5. $event>stopPropagation();
  6. }

Теперь, любой обработчик события order.placed, тот что еще не был вызван, не запустится.

Существует способ определить, была ли прервана цепочка обработчиков события через stopPropagation(), вызвав isPropagationStopped(). Вернется true либо false:

  1. $dispatcher>dispatch(‘foo.event’, $event);
  2. if ($event>isPropagationStopped()) {
  3. // …
  4. }

События и слушатели EventDispatcher

EventDispatcher всегда передает слушателю само событие, его имя и ссылку на самого себя. Таким образом, открываются дополнительные возможности для вашего приложения: обработка других событий внутри слушателей, создание цепочек событий, или даже загрузка слушателей в объект диспетчера «на лету».

Сокращенный синтаксис

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

  1. $dispatcher>dispatch(‘order.placed’);

Более того, диспетчер всегда возвращает объект события, какой бы он ни был – независимо от того вы его передали, или тот был создан по умолчанию, внутри диспетчера. Данная возможность позволяет писать так:

  1. if (!$dispatcher>dispatch(‘foo.event’)>isPropagationStopped()) {
  2. // …
  3. }

Или:

  1. $event = new OrderPlacedEvent($order);
  2. $order = $dispatcher>dispatch(‘bar.event’, $event)>getOrder();

И так далее.

Проверка имени события

Экземпляр EventDispatcher, вместе с именем события, которое обрабатывается в данный момент, передается в качестве аргумента слушателю:

  1. use Symfony\Component\EventDispatcher\Event;
  2. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  3. class Foo
  4. {
  5. public function myEventListener(Event $event, $eventName, EventDispatcherInterface $dispatcher)
  6. {
  7. // … что-то делаем с именем события
  8. }
  9. }

Другие диспетчеры

Кроме часто используемого EventDispatcher, компонент поставляется с другими диспетчерами:

  • Container Aware Event Dispatcher
  • Immutable Event Dispatcher
  • Traceable Event Dispatcher