Перейти к содержанию

Основные понятия

events-router устроен как HTTP-роутер Slim. Только вместо запроса — событие, а вместо Action — слушатель. Если держать в голове эту аналогию, остальное складывается само.

events-router Slim / PSR Что это
событие, EventInterface запрос, ServerRequestInterface что произошло: имя-топик, данные, атрибуты
топик путь URL имя события из сегментов через точку: shop.order.42.paid
слушатель, ListenerInterface Action, RequestHandlerInterface реакция на событие
middleware, MiddlewareInterface PSR-15 MiddlewareInterface обёртка вокруг слушателя
маршрут маршрут связка «шаблон топика → слушатель + middleware»

Главное отличие от HTTP одно: запрос попадает ровно в один Action, а у события может быть сколько угодно слушателей. Почти всё, чем роутер событий отличается от HTTP-роутера, следует из этого.

Событие

Событие — это имя, данные и атрибуты.

use Selyusize\EventsRouter\Event;

$event = new Event('shop.order.42.paid', ['amount' => 1500]);

echo $event->getName(), PHP_EOL;                         // shop.order.42.paid
echo $event->getPayload()['amount'], PHP_EOL;            // 1500

// Событие неизменяемо: withAttribute() возвращает копию
$withId = $event->withAttribute('order_id', '42');

echo $withId->getAttribute('order_id'), PHP_EOL;          // 42
echo $event->getAttribute('order_id', 'нет'), PHP_EOL;    // нет — исходное событие не изменилось

Имя — конкретный топик: что произошло и с чем. Не шаблон, а факт: shop.order.42.paid, а не shop.order.{order_id}.paid.

Payload — данные события: массив, DTO, любой объект. Роутер его не читает и не меняет.

Атрибуты — то, что событие собрало по дороге к слушателю: параметры из шаблона маршрута (order_id из order.{order_id}.paid) и данные, которые добавили middleware.

Событие неизменяемо, как PSR-7 запрос. withAttribute() возвращает копию. Поэтому каждый слушатель получает своё событие со своими параметрами и не может испортить его для остальных. Payload при этом не копируется: если это объект, все копии смотрят на один и тот же объект.

Слушатель

Один класс — одна реакция. Метод handle() статичный: роутер вызывает MarkOrderPaid::handle($event) и объектов не создаёт.

use Selyusize\EventsRouter\Contract\Core\EventInterface;
use Selyusize\EventsRouter\Contract\Core\ListenerInterface;
use Selyusize\EventsRouter\Contract\Core\MiddlewareInterface;
use Selyusize\EventsRouter\Event;

final class MarkOrderPaid implements ListenerInterface
{
    public static function handle(EventInterface $event): void
    {
        echo 'Заказ ', $event->getAttribute('order_id'), ' оплачен', PHP_EOL;
    }
}

Зависимости слушатель берёт сам, из контейнера. Почему так — в Почему так.

Middleware

Middleware оборачивает слушателя. Получает событие и замыкание $next — продолжение цепочки: следующий middleware или сам слушатель. Может что-то сделать до и после, передать дальше изменённое событие или не вызвать $next вовсе — тогда событие до слушателя не дойдёт.

final class EventLogger implements MiddlewareInterface
{
    public function process(EventInterface $event, Closure $next): void
    {
        echo '→ ', $event->getName(), PHP_EOL;

        $next($event);

        echo '← ', $event->getName(), PHP_EOL;
    }
}

Цепочку из middleware и слушателя собирает роутер. Изнутри это выглядит так:

// Так роутер соединяет middleware и слушателя. Вручную это писать не придётся.
(new EventLogger())->process(
    (new Event('shop.order.42.paid'))->withAttribute('order_id', '42'),
    MarkOrderPaid::handle(...),
);
→ shop.order.42.paid
Заказ 42 оплачен
← shop.order.42.paid

Маршрут

Маршрут связывает шаблон топика, одного слушателя и его middleware. Маршруты собираются в группы с общим префиксом и общими middleware, группы — в один файл.

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