Компонент EventDispatcher
Дата обновления перевода: 2024-07-03
Компонент EventDispatcher
Компонент EventDispatcher предоставляет инструменты, которые позволяют компонентам вашего приложения общаться друг с другом, запуская события и слушая их.
Вступление
Объектно-ориентированный код прошёл длинный путь, чтобы гарантировать расширяемость кода. Создавая классы, которые имеют чётко определённые задачи, вы делаетеваш код более гибким, и разработчик может расширять их с помощью подклассов для настройки этого поведения. Но если они хотят поделиться изменениями с другими разработчиками, которые также создали свои собственные подклассы, наследование кода больше не является решением.
Представьте реальный пример, где вы хотите предоставить систему плагинов для вашего проекта. Плагин должен иметь возможность добавлять методы или делать что-то до или после того, как выполняется метод, не вмешиваясь в работу других плагинов. Это непростая задачка для одного только наследования, и даже если бы множественное наследование было возможно в PHP, оно имеет свои недостатки
Компонент Symfony EventDispatcher реализует шаблоны проектирования Mediator and Observer для того, чтобы сделать всё это возможным и предоставить вашим проектам быть действительно расширяемыми.
Возьмите простой пример из компонента HttpKernel.
Когда объект Response
уже создан, может быть ползеным позволить другим элементам
в системе изменять его (например, добавлять некоторые кеш-заголовки) до его реального
использования. Чтобы сделать это возможным, Ядро Symfony вызывает событие - kernel.response
.
Вот, как оно работает:
- Слушатель (PHP-объект) сообщает центральному объекту диспетчеру, что он
хочет слушать событие
kernel.response
; - В какой-то момент, ядро Symfony сообзщает объекту диспетчеру запустить
событие
kernel.response
, передавая его с объектомEvent
, который имеет доступ к объектуResponse
; - Диспетчер уведомляет (т.е. вызывает метод) всех слушателей события
kernel.response
, позволяя каждому из них делать изменения в объектеResponse
.
Установка
1
$ composer require symfony/event-dispatcher
Note
Если вы устанавливаете этот компонент вне приложения Symfony, вам нужно
подключить файл vendor/autoload.php
в вашем коде для включения механизма
автозагрузки классов, предоставляемых Composer. Детальнее читайте в
этой статье.
Применение
See also
Эта статья объясняет как использовать функции EventDispatcher как независимого компонента в любом приложении PHP. Прочитайте статью События и слушатели событий для понимания как использовать его в приложениях Symfony.
События
Когда развёртывается событие, оно определяется уникальным именем (например,
kernel.response
), которое может слушать любое количество слушателей. Также
создаётся и передаётся всем слушателям экземпляр Event.
Как вы увидите позже, сам объект Event
часто содержит данные о запускаемом
событии.
Имена и объекты событий
Когда диспетчер уведомляет слушателей, он передаёт настоящий объект Event
этим слушателям. Базовый класс Event
очень прост: он содержит метод для
остановки распространения события ,
и больше ничего.
See also
Прочтите "Объект Generic Event (событие общего назначения)", чтобы узнать больше об этом объекте базового события.
Зачастую, данные о конкретном событии должны быть переданы вместе с объектом
Event
, чтобы слушатели имели необходимую им информацию. В таком случае,
можно передать специальный подкласс, который имеет дополнительные методы для
извлечения и переопределения информации, при запуске события. Например, событие
kernel.response
использует FilterResponseEvent,
который содержит методы, чтобы получать и даже заменять объект Response
.
Диспетчер
Диспетчер - это центральный объект системы запуска событий. Обычно создаётся один диспетчер, который содержит реестр слушателей. Когда событие запускается через диспетчер, он уведомляет всех слушателей, зарегистрированных с этим событием:
1 2 3
use Symfony\Component\EventDispatcher\EventDispatcher;
$dispatcher = new EventDispatcher();
Соединение слушателей
Чтобы воспользоваться преимуществами существующего события, вам нужно соединить
слушателя с диспетчером, чтобы он мог быть уведомлён, когда событие будет запущено.
Вызов метода диспетчера addListener()
ассоциирует все вызываемые PHP с событием:
1 2
$listener = new AcmeListener();
$dispatcher->addListener('acme.foo.action', array($listener, 'onFooAction'));
Метод addListener()
имеет до трёх аргументов:
- Имя события (строка), которое хочет слушать этот слушатель;
- Вызываемое PHP, которое будет выполнено при запуске указанного события;
- Необязательное число приоритета (чем выше - тем важнее, следовательно
этот слушатель будет запущен раньше), которое определяет, когда вызывается
слушатель по отношению к другим слушателям (по умолчанию
0
). Если два слушателя имеют одинаковый приоритет, они выполняются в том порядке, в котором были добавлены в диспетчер.
Note
PHP вызываемое - это переменная PHP, которая может быть использована
функцией call_user_func()
и возвращает true
при передаче функции
is_callable()
. Это может быть экземпляр \Closure
, объект, реализующий
метод __invoke()
(то, чем на самом деле являются замыкания), строка,
представляющая функцию или массив, представляющий метод объекта или класса.
До этих пор вы видели, как можно зарегистрировать PHP объекты в качестве слушателей. Вы можете также зарегистрировать PHP Замыкания в качестве слушателей событий:
1 2 3 4 5
use Symfony\Contracts\EventDispatcher\Event;
$dispatcher->addListener('acme.foo.action', function (Event $event): void {
// будет выполнено при запуске события acme.foo.action
});
Когда слушатель зарегистрирован в диспетчере, он ждёт, пока не будет уведомления
о событии. В примере выше, когда запускается событие acme.foo.action
, диспетчер
вызывает метод AcmeListener::onFooAction()
и передаёт объект Event
в виде
единственного аргумента:
1 2 3 4 5 6 7 8 9 10 11
use Symfony\Contracts\EventDispatcher\Event;
class AcmeListener
{
// ...
public function onFooAction(Event $event): void
{
// ... сделать что-то
}
}
Аргумент $event
- это объект события, который был передан при запуске
события. Во многих случаях, передаётся специальный подкласс события с
дополнительной информацией. Вы можете посмотреть документацию или реализацию
каждого событий, чтобы определить, какой экземпляр передаётся.
Создание и запуск события
Кроме регистрации в слушателях существующих событий, вы можете создавать и запускать собственные события. Это полезно при создании сторонних библиотек, а также,когда вы хотите сохранять разные компоненты вашей собственной системы гибкими и разделёнными.
Создание класса событий
Представьте, что вы хотите создать новое событие - order.placed
- которое
запускается каждый раз, когда пользователь заказывает товар в вашем приложении.
При запуске этого события, вы передаёте пользовательский экземпляр события,
который имеет доступ к размещённому заказу. Начните с создания этого пользоватсклього
класса события и его документирования:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
namespace Acme\Store\Event;
use Acme\Store\Order;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Событие order.placed запускается каждый раз, когда создаётся заказ
* в системе.
*/
final class OrderPlacedEvent extends Event
{
public function __construct(private Order $order) {}
public function getOrder(): Order
{
return $this->order;
}
}
Каждый слушатель теперь имеет доступ к заказу через метод getOrder()
.
Запустите событие
Метод dispatch()
уведомляет всех слушателей о данном событии. Используется два аргумента: имя
события для запуска, и экземпляр Event
для передачи каждому слушателю этого
события:
1 2 3 4 5 6 7 8 9 10
use Acme\Store\Event\OrderPlacedEvent;
use Acme\Store\Order;
// создаёт или извлекает порядок каким-либо образом
$order = new Order();
// ...
// создаёт OrderPlacedEvent и запускает его
$event = new OrderPlacedEvent($order);
$dispatcher->dispatch($event);
Заметьте, что специальный объект OrderPlacedEvent
создаётся и передаётся
методу dispatch()
. Теперь, любой слушатель события order.placed
получит
OrderPlacedEvent
.
Note
Если вам не нужно передавать слушателям событий никаких дополнительных данных,
вы также можете использовать класс по умолчанию Event.
В таком случае, вы можете задокументировать событие и его имя в общем классе StoreEvents
,
аналогичном классу KernelEvents:
1 2 3 4 5 6 7 8 9
namespace App\Event;
class StoreEvents {
/**
* @Event("Symfony\Contracts\EventDispatcher\Event")
*/
public const ORDER_PLACED = 'order.placed';
}
И используйте класс Event для
оглашения события:
1 2 3
use Symfony\Contracts\EventDispatcher\Event;
$this->eventDispatcher->dispatch(new Event(), StoreEvents::ORDER_PLACED);
Использование подписчиков событий
Наиболее распротранённый способ слушать событие - зарегистрировать в диспетчере слушатель событий. Этот слушатель может слушать одно или более событий и уведомляется каждый раз, когда запускаются эти события.
Другим способом слушать события является через подписчика событий. Подписчик
событий - это PHP класс, который способен сообщить диспетчеру, на какие именно
события ему нужно подпистаься. Он реализует интерфейс
EventSubscriberInterface, который
требует одного статичного метода, под названием
getSubscribedEvents().
Возьмите следующий пример подписчика, который подписывается на события
kernel.response
и order.placed
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
namespace Acme\Store\Event;
use Acme\Store\Event\OrderPlacedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class StoreSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::RESPONSE => [
['onKernelResponsePre', 10],
['onKernelResponsePost', -10],
],
OrderPlacedEvent::class => 'onPlacedOrder',
];
}
public function onKernelResponsePre(ResponseEvent $event): void
{
// ...
}
public function onKernelResponsePost(ResponseEvent $event): void
{
// ...
}
public function onPlacedOrder(OrderPlacedEvent $event): void
{
$order = $event->getOrder();
// ...
}
}
Это очень похоже на класс слушателя, кроме того, что сам классможет сказать диспетчеру, какие события ему стоит слушать. Чтобы зарегистрирвать подписчика в диспетчере, используйте метод addSubscriber():
1 2 3 4 5
use Acme\Store\Event\StoreSubscriber;
// ...
$subscriber = new StoreSubscriber();
$dispatcher->addSubscriber($subscriber);
Диспетчер автоматически зарегистрирует подписчика для каждого события, возвращённого
методом getSubscribedEvents()
. Этот метод возвращает массив, индексированный по
именам событий, значения которых являются либо именем метода для вызова, либо массивом,
составленным из именим метода для вызова и приоритетом.
Вышеописанный пример демонстрирует,
как зарегистрировать несколько методов слушателя для одного и того же события в подписчике,
а также, как передать приоритет каждого метода слушателя. Чем выше приоритет, тем раньше
вызывается метод. В вышеописанном примере, когда запускается событие kernel.response
,
вызываются методы onKernelResponsePre()
и onKernelResponsePost()
в таком порядке.
Остановка потока / распространения событий
В некоторых случаях, слушателю имеет смысл предотвращать вызов любых слушателей. Другими словами, слушателю нужно иметь возможность сказать диспетчеру прекратить расространене события в будущие слушатели (т.е. более не уведомлять слушателей). Это можно сделать изнутри слушателя,с помощью метода stopPropagation():
1 2 3 4 5 6 7 8
use Acme\Store\Event\OrderPlacedEvent;
public function onPlacedOrder(OrderPlacedEvent $event): void
{
// ...
$event->stopPropagation();
}
Теперь, любые слушатели order.placed
, которые ещё не были вызваны, не
будут вызваны.
Возможно определить, было ли событие остановлено с использованием метода isPropagationStopped(), который возвращает булево значение:
1 2 3 4 5
// ...
$dispatcher->dispatch($event, 'foo.event');
if ($event->isPropagationStopped()) {
// ...
}
События и слушатели, знающие об EventDispatcher
EventDispatcher
всегда передаёт запущенное событие, имя события и ссылку на себя
самого слушателям. Это может привести к некоторым продвинутым применениям EventDispatcher
,
включая запуск других событий внутри слушателей, связывание событий или даже ленивую
загрузку слушателей в объекте диспетчера.
Интроспекция имени события
Экземпляр EventDispatcher
, так же как и имя события, которое запускается,
передаются в качестве аргументов слушателя:
1 2 3 4 5 6 7 8 9 10
use Symfony\Contracts\EventDispatcher\Event;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class MyListener
{
public function myEventListener(Event $event, string $eventName, EventDispatcherInterface $dispatcher): void
{
// ... сделать что-то с именем события
}
}
Другие диспетчеры
Кроме распространённого EventDispatcher
, компонент поставляется с некоторыми
другими диспетчерами: