Маршруты¶
Маршруты лежат в отдельном файле. Файл возвращает функцию, которая получает роутер, — так же, как 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()¶
Вызывает функцию из файла маршрутов, передавая ей роутер. Без кэша это то же самое, что (require ...)($events). С ключом route_cache_file при наличии файла кэша функция не вызывается: таблица берётся из кэша. Правила для режима с кэшем — в рецепте Включить кэш маршрутов.
listen()¶
- Первый аргумент — шаблон топика относительно префикса группы. Внутри группы может быть пустым:
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:
- роутер → внешняя группа → вложенная группа → маршрут;
- на одном уровне последний добавленный выполняется первым.
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;
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 |