Skip to content

Диспетчер событий

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

Цель данного PSR — установить общий механизм расширения и взаимодействия на основе событий, чтобы библиотеки и компоненты можно было свободнее повторно использовать в различных приложениях и фреймворках.

Ключевые слова «ОБЯЗАН» («MUST»), «НЕ ДОЛЖЕН» («MUST NOT»), «ТРЕБУЕТСЯ» («REQUIRED»), «ДОЛЖЕН» («SHALL»), «НЕ ДОЛЖЕН» («SHALL NOT»), «СЛЕДУЕТ» («SHOULD»), «НЕ СЛЕДУЕТ» («SHOULD NOT»), «РЕКОМЕНДУЕТСЯ» («RECOMMENDED»), «МОЖЕТ» («MAY») и «НЕОБЯЗАТЕЛЬНО» («OPTIONAL») в данном документе следует интерпретировать в соответствии с RFC 2119.

Цель

Наличие общих интерфейсов для диспетчеризации и обработки событий позволяет разработчикам создавать библиотеки, способные единообразно взаимодействовать со многими фреймворками и другими библиотеками.

Несколько примеров:

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

Определения

  • Событие (Event) — Событие является сообщением, производимым Отправителем. Оно может быть любым произвольным PHP-объектом.
  • Слушатель (Listener) — Слушатель — это любой PHP-вызываемый объект (callable), ожидающий передачи ему События. Одному и тому же Событию МОЖЕТ быть передано ноль или более Слушателей. Слушатель МОЖЕТ поставить в очередь какое-либо асинхронное поведение по своему усмотрению.
  • Отправитель (Emitter) — Отправитель — это любой произвольный код, желающий диспетчеризировать Событие. Также называется «вызывающим кодом». Он не представлен какой-либо конкретной структурой данных, а лишь относится к варианту использования.
  • Диспетчер (Dispatcher) — Диспетчер — это объект-сервис, которому Отправитель передаёт объект События. Диспетчер отвечает за то, чтобы Событие было передано всем соответствующим Слушателям, однако ДОЛЖЕН делегировать определение ответственных Слушателей Поставщику слушателей.
  • Поставщик слушателей (Listener Provider) — Поставщик слушателей отвечает за определение того, какие Слушатели актуальны для данного События, однако НЕ ДОЛЖЕН вызывать Слушателей самостоятельно. Поставщик слушателей МОЖЕТ указывать ноль или более актуальных Слушателей.

События

События — это объекты, выполняющие роль единицы связи между Отправителем и соответствующими Слушателями.

Объекты событий МОГУТ быть изменяемыми, если вариант использования предполагает передачу информации Слушателями обратно Отправителю. Однако если такая двусторонняя связь не нужна, РЕКОМЕНДУЕТСЯ определять Событие как неизменяемое, то есть без мутирующих методов.

Разработчики ОБЯЗАНЫ исходить из того, что один и тот же объект будет передан всем Слушателям.

РЕКОМЕНДУЕТСЯ, но НЕ ТРЕБУЕТСЯ, чтобы объекты событий поддерживали сериализацию и десериализацию без потерь; выражение $event == unserialize(serialize($event)) ДОЛЖНО быть истинным. Объекты МОГУТ использовать интерфейс PHP Serializable, магические методы __sleep() или __wakeup() или аналогичные возможности языка, если это уместно.

Останавливаемые события

Останавливаемое событие (Stoppable Event) — это особый вид События, содержащий дополнительные средства для предотвращения вызова последующих Слушателей. Оно обозначается реализацией интерфейса StoppableEventInterface.

Событие, реализующее StoppableEventInterface, ОБЯЗАНО возвращать true из метода isPropagationStopped(), когда всё, что оно представляет, завершено. Реализатор класса сам определяет момент наступления этого условия. Например, Событие, запрашивающее сопоставление объекта RequestInterface из PSR-7 с соответствующим объектом ResponseInterface, может иметь метод setResponse(ResponseInterface $res) для вызова Слушателем, после чего isPropagationStopped() начнёт возвращать true.

Слушатели

Слушатель может быть любым PHP-вызываемым объектом (callable). Слушатель ОБЯЗАН иметь ровно один параметр — Событие, на которое он реагирует. Слушателям СЛЕДУЕТ указывать тип этого параметра настолько конкретно, насколько это уместно для их варианта использования; то есть Слушатель МОЖЕТ указать интерфейс в качестве подсказки типа, обозначая совместимость с любым типом Событий, реализующим этот интерфейс, или с конкретной реализацией этого интерфейса.

Слушатель ДОЛЖЕН иметь возвращаемый тип void и СЛЕДУЕТ явно указывать его. Диспетчер ОБЯЗАН игнорировать возвращаемые значения Слушателей.

Слушатель МОЖЕТ делегировать действия другому коду. Это включает случай, когда Слушатель является тонкой обёрткой над объектом, выполняющим реальную бизнес-логику.

Слушатель МОЖЕТ помещать информацию из События в очередь для последующей обработки вторичным процессом с использованием cron, сервера очередей или аналогичных технологий. При этом он МОЖЕТ сериализовать сам объект Событий; однако следует учитывать, что не все объекты событий могут быть безопасно сериализованы. Вторичный процесс ОБЯЗАН исходить из того, что любые внесённые им изменения в объект Событий НЕ будут переданы другим Слушателям.

Диспетчер

Диспетчер — это объект-сервис, реализующий EventDispatcherInterface. Он отвечает за получение Слушателей от Поставщика слушателей для диспетчеризируемого События и за вызов каждого Слушателя с этим Событием.

Диспетчер:

  • ОБЯЗАН вызывать Слушателей синхронно в порядке, в котором они возвращаются из ListenerProvider.
  • ОБЯЗАН возвращать тот же объект Событий, который был ему передан, после завершения вызова Слушателей.
  • НЕ ДОЛЖЕН возвращать управление Отправителю до тех пор, пока все Слушатели не завершили выполнение.

При получении Останавливаемого события Диспетчер:

  • ОБЯЗАН вызывать isPropagationStopped() у Событий перед вызовом каждого Слушателя. Если этот метод возвращает true, Диспетчер ОБЯЗАН немедленно вернуть Событие Отправителю и НЕ ДОЛЖЕН вызывать дальнейших Слушателей. Это означает, что если Событию, переданному Диспетчеру, isPropagationStopped() всегда возвращает true, ни один Слушатель не будет вызван.

Диспетчер СЛЕДУЕТ считать, что любой Слушатель, возвращённый ему Поставщиком слушателей, является типобезопасным. То есть Диспетчеру СЛЕДУЕТ предполагать, что вызов $listener($event) не приведёт к TypeError.

Обработка ошибок

Исключение или ошибка, выброшенные Слушателем, ОБЯЗАНЫ блокировать выполнение всех последующих Слушателей. Исключение или ошибка, выброшенные Слушателем, ОБЯЗАНЫ иметь возможность распространяться обратно до Отправителя.

Диспетчер МОЖЕТ перехватить выброшенный объект для его логирования, выполнения дополнительных действий и т.д., однако затем ОБЯЗАН повторно выбросить исходный объект исключения.

Поставщик слушателей

Поставщик слушателей — это объект-сервис, отвечающий за определение того, какие Слушатели актуальны для данного Событий и должны быть вызваны. Он может определять как сами актуальные Слушатели, так и порядок их возврата любым удобным способом. Это МОЖЕТ включать:

  • Предоставление некоего механизма регистрации, позволяющего реализаторам назначать Слушателя Событию в фиксированном порядке.
  • Формирование списка применимых Слушателей через рефлексию на основе типа Событий и реализуемых им интерфейсов.
  • Генерацию заранее скомпилированного списка Слушателей, доступного во время выполнения.
  • Реализацию некоторой формы контроля доступа, при которой определённые Слушатели вызываются только при наличии у текущего пользователя определённых прав.
  • Извлечение информации из объекта, на который ссылается Событие (например, Сущности), и вызов предопределённых методов жизненного цикла этого объекта.
  • Делегирование своих обязанностей одному или нескольким другим Поставщикам слушателей с использованием произвольной логики.

Любая комбинация перечисленного или иных механизмов МОЖЕТ использоваться по усмотрению разработчика.

Поставщикам слушателей СЛЕДУЕТ использовать имя класса Событий для разграничения одного события от другого. Они также МОГУТ учитывать любую другую информацию о событии, если это уместно.

Поставщики слушателей ОБЯЗАНЫ обрабатывать родительские типы наравне с собственным типом Событий при определении применимости Слушателей. В следующем случае:

class A {}

class B extends A {}

$b = new B();

function listener(A $event): void {};

Поставщик слушателей ОБЯЗАН считать listener() применимым слушателем для $b, поскольку они совместимы по типу, если только иные критерии не препятствуют этому.

Композиция объектов

Диспетчеру СЛЕДУЕТ включать Поставщика слушателей для определения актуальных Слушателей. РЕКОМЕНДУЕТСЯ реализовывать Поставщика слушателей как отдельный объект от Диспетчера, однако это НЕ ТРЕБУЕТСЯ.

Интерфейсы

namespace Psr\EventDispatcher;

/**
 * Defines a dispatcher for events.
 */
interface EventDispatcherInterface
{
    /**
     * Provide all relevant listeners with an event to process.
     *
     * @param object $event
     *   The object to process.
     *
     * @return object
     *   The Event that was passed, now modified by listeners.
     */
    public function dispatch(object $event);
}
namespace Psr\EventDispatcher;

/**
 * Mapper from an event to the listeners that are applicable to that event.
 */
interface ListenerProviderInterface
{
    /**
     * @param object $event
     *   An event for which to return the relevant listeners.
     * @return iterable<callable>
     *   An iterable (array, iterator, or generator) of callables.  Each
     *   callable MUST be type-compatible with $event.
     */
    public function getListenersForEvent(object $event) : iterable;
}
namespace Psr\EventDispatcher;

/**
 * An Event whose processing may be interrupted when the event has been handled.
 *
 * A Dispatcher implementation MUST check to determine if an Event
 * is marked as stopped after each listener is called.  If it is then it should
 * return immediately without calling any further Listeners.
 */
interface StoppableEventInterface
{
    /**
     * Is propagation stopped?
     *
     * This will typically only be used by the Dispatcher to determine if the
     * previous listener halted propagation.
     *
     * @return bool
     *   True if the Event is complete and no further listeners should be called.
     *   False to continue calling listeners.
     */
    public function isPropagationStopped() : bool;
}