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

Маршруты

Маршруты лежат в отдельном файле. Файл возвращает функцию, которая получает роутер, — так же, как routes.php в Slim.

use App\Events\Listener;
use App\Events\Middleware;
use Selyusize\EventsRouter\EventRouter;
use Selyusize\EventsRouter\Routing\RouteGroup;

return static function (EventRouter $events): void {
    $events->setPrefix('shop');

    // =================================== Пользователи ===================================

    $events->group('user', static function (RouteGroup $group): void {
        $group->listen('registered', Listener\User\CreateBonusAccount::class);
        $group->listen('registered', Listener\User\SendWelcomeEmail::class);
        $group->listen('{user_id}.deactivated', Listener\User\RevokeTokens::class);
    })
        ->add(Middleware\EventLog\EventLogger::class);

    // =================================== Заказы ===================================

    $events->group('', static function (RouteGroup $group): void {
        $group->group('order', static function (RouteGroup $order): void {
            $order->listen('created', Listener\Order\ReserveStock::class);
            $order->listen('created', Listener\Order\SendConfirmationEmail::class);

            $order->group('{order_id}', static function (RouteGroup $one): void {
                $one->listen('paid', Listener\Order\MarkOrderPaid::class);
                $one->listen('paid', Listener\Bonuses\AccrueBonuses::class);
                $one->listen('payment.#', Listener\Order\AuditPayment::class);
            })
                ->add(Middleware\Idempotency\IdempotencyGuard::class);

            $order->listen('*.cancelled', Listener\Order\RefundPayment::class);
        })
            ->add(Middleware\DataEnrichment\SetUserData::class);

        // тот же префикс 'order', но другой набор middleware
        $group->group('order', static function (RouteGroup $order): void {
            $order->listen('{order_id}.unpaid_reminder', Listener\Order\SendUnpaidReminder::class);
        });
    })
        ->add(Middleware\EventLog\EventLogger::class);
};

Итоговый список маршрутов с полными шаблонами и middleware:

use Selyusize\EventsRouter\EventRouterFactory;

$events = EventRouterFactory::create();
$events->loadRoutes(require __DIR__ . '/events.php');

$short = static fn (object|string $class): string => substr(strrchr('\\' . (is_string($class) ? $class : $class::class), '\\'), 1);

foreach ($events->getRoutes() as $route) {
    printf(
        "%-38s %-22s %s\n",
        $route->getPattern()->getPattern(),
        $short($route->getListener()),
        implode(' → ', array_map($short, $route->getMiddleware())),
    );
}
shop.user.registered                   CreateBonusAccount     EventLogger
shop.user.registered                   SendWelcomeEmail       EventLogger
shop.user.{user_id}.deactivated        RevokeTokens           EventLogger
shop.order.created                     ReserveStock           EventLogger → SetUserData
shop.order.created                     SendConfirmationEmail  EventLogger → SetUserData
shop.order.{order_id}.paid             MarkOrderPaid          EventLogger → SetUserData → IdempotencyGuard
shop.order.{order_id}.paid             AccrueBonuses          EventLogger → SetUserData → IdempotencyGuard
shop.order.{order_id}.payment.#        AuditPayment           EventLogger → SetUserData → IdempotencyGuard
shop.order.*.cancelled                 RefundPayment          EventLogger → SetUserData
shop.order.{order_id}.unpaid_reminder  SendUnpaidReminder     EventLogger

Методы

Метод Где есть Что делает
loadRoutes(callable $routes) роутер подключает файл маршрутов; с route_cache_file — через кэш
listen(string $pattern, string $listener) роутер, группа вешает одного слушателя на шаблон, возвращает маршрут
group(string $prefix, callable $routes) роутер, группа группа с общим префиксом и middleware, возвращает группу
add($middleware) роутер, группа, маршрут добавляет middleware
setPrefix(string $prefix) роутер общий префикс всех топиков, аналог setBasePath()
match(string $topic) роутер какие маршруты получат событие, с параметрами
getRoutes() роутер все маршруты с полными шаблонами и middleware

loadRoutes()

$events->loadRoutes(require __DIR__ . '/events-routes.php');

Вызывает функцию из файла маршрутов, передавая ей роутер. Без кэша это то же самое, что (require ...)($events). С ключом route_cache_file при наличии файла кэша функция не вызывается: таблица берётся из кэша. Правила для режима с кэшем — в рецепте Включить кэш маршрутов.

listen()

$group->listen('{order_id}.paid', Listener\Order\MarkOrderPaid::class);
  • Первый аргумент — шаблон топика относительно префикса группы. Внутри группы может быть пустым: listen('', ...) слушает само событие с префиксом группы.
  • Второй — ровно один слушатель: имя класса, реализующего ListenerInterface. Ни массивов, ни объектов.
  • Несколько слушателей на одно событие — несколько строк listen().

Порядок вызова

Слушатели одного события вызываются строго в порядке объявления маршрутов, сквозь все группы. Приоритетов нет. Чтобы слушатель сработал раньше, объявите его выше.

$order->listen('created', Listener\Order\ReserveStock::class);           // первым
$order->listen('created', Listener\Order\SendConfirmationEmail::class);  // вторым

group()

$events->group('order', static function (RouteGroup $group): void {
    // маршруты с префиксом order
})
    ->add(Middleware\EventLog\EventLogger::class);
  • Префикс соединяется с шаблонами через точку. Группы вкладываются на любую глубину, префиксом может быть параметр: group('{order_id}', ...).
  • Пустой префикс — group('', ...) — группа только ради общих middleware, как $app->group('', ...) в Slim.
  • Один префикс в нескольких группах с разными middleware — можно, маршруты не смешиваются.

add()

Middleware указывается именем класса или готовым объектом. По имени класса объект создаётся контейнером, когда событие дойдёт до этого middleware.

Порядок выполнения, как в Slim:

  1. роутер → внешняя группа → вложенная группа → маршрут;
  2. на одном уровне последний добавленный выполняется первым.
use Selyusize\EventsRouter\EventRouterFactory;
use Selyusize\EventsRouter\Routing\RouteGroup;

$events = EventRouterFactory::create();

$events->group('order', static function (RouteGroup $order): void {
    $order->group('{order_id}', static function (RouteGroup $one): void {
        $one->listen('paid', MarkOrderPaid::class)
            ->add(RouteA::class)
            ->add(RouteB::class);
    })
        ->add(InnerA::class)
        ->add(InnerB::class);
})
    ->add(OuterA::class)
    ->add(OuterB::class);

echo implode(' → ', $events->getRoutes()[0]->getMiddleware()), ' → MarkOrderPaid', PHP_EOL;
OuterB → OuterA → InnerB → InnerA → RouteB → RouteA → MarkOrderPaid

add() на группе можно вызвать после того, как маршруты внутри уже объявлены: middleware применится ко всем маршрутам группы.

Middleware роутера ($events->add()) выполняется один раз на событие и оборачивает всех слушателей сразу. Подробнее — в Рассылке.

setPrefix()

Общий префикс всех топиков. Можно вызвать в любой момент: шаблоны уже объявленных маршрутов пересоберутся. Пустая строка убирает префикс. Если с новым префиксом какой-то шаблон станет некорректным, роутер бросит исключение и останется с прежним префиксом.

match()

Какие маршруты сработают на событие: все подходящие, строго в порядке объявления, у каждого свои параметры.

use Selyusize\EventsRouter\EventRouterFactory;

$events = EventRouterFactory::create();
$events->loadRoutes(require __DIR__ . '/events.php');

foreach (['shop.order.created', 'shop.order.42.paid', 'shop.order.42.payment.failed', 'shop.catalog.updated'] as $topic) {
    echo $topic, PHP_EOL;

    $matches = $events->match($topic);

    if ($matches === []) {
        echo '    слушателей нет', PHP_EOL;
    }

    foreach ($matches as $i => $match) {
        printf(
            "    %d. %s %s\n",
            $i + 1,
            substr(strrchr('\\' . $match->getRoute()->getListener(), '\\'), 1),
            json_encode($match->getParameters(), JSON_FORCE_OBJECT),
        );
    }
}
shop.order.created
    1. ReserveStock {}
    2. SendConfirmationEmail {}
shop.order.42.paid
    1. MarkOrderPaid {"order_id":"42"}
    2. AccrueBonuses {"order_id":"42"}
shop.order.42.payment.failed
    1. AuditPayment {"order_id":"42"}
shop.catalog.updated
    слушателей нет

Когда проверяются ошибки

Всё проверяется при объявлении, а не когда придёт событие. Ошибка в файле маршрутов видна при первой загрузке приложения, а стек вызовов указывает на строку с ней.

Что Когда Исключение
шаблон вместе с префиксами групп и роутера listen(), setPrefix() InvalidTopicPattern
класс слушателя существует и реализует ListenerInterface listen() InvalidRoute
класс middleware существует и реализует MiddlewareInterface add() InvalidRoute
с кэшем: маршрут объявлен вне loadRoutes(), второй вызов loadRoutes(), middleware-объект listen(), group(), setPrefix(), loadRoutes() InvalidRoute