Обработать ошибки слушателей¶
Слушатель упал. Сервис бонусов недоступен, база не ответила, пришли неожиданные данные. Вопрос не в том, случится ли это, а в том, что будет дальше.
По умолчанию роутер делает так:
- перехватывает исключение и записывает его в отчёт со статусом
Failed; - пишет его в лог роутера;
- вызывает следующих слушателей, как будто ничего не случилось.
Одна поломка не превращается в цепную реакцию. Дальше — как это настроить под себя.
Поймать ошибку в самом слушателе¶
Лучшее место для обработки ошибки — там, где понятно, что она значит. Слушатель знает, что пошло не так и что с этим делать:
final class AccrueBonuses implements ListenerInterface
{
public static function handle(EventInterface $event): void
{
try {
Container::get(BonusService::class)->accrue($event->getAttribute('order_id'));
} catch (BonusServiceUnavailable $error) {
Container::get(LoggerInterface::class)->warning('Бонусы не начислены', ['exception' => $error]);
}
}
}
Такой слушатель завершается без исключения, и в отчёте у него статус Handled. Обработка ошибок роутера — страховка для того, что слушатель не поймал, а не основной путь.
Отправить ошибки в своё место¶
Чтобы ошибки уходили не в лог, а в мониторинг, Sentry или чат, напишите свой обработчик. Он реализует ErrorHandlerInterface и получает исключение, событие с параметрами маршрута и класс упавшего слушателя:
use Selyusize\EventsRouter\Contract\Error\ErrorHandlerInterface;
final class EchoErrorHandler implements ErrorHandlerInterface
{
public function handle(Throwable $error, EventInterface $event, string $listener): void
{
echo ' ошибка: ', $listener, ' — ', $error->getMessage(), PHP_EOL;
}
}
Свой обработчик заменяет запись в лог. Если нужно и то и другое, вызовите логгер внутри своего обработчика.
Если нужно писать ошибки в другой логгер или с другим уровнем, свой класс не нужен:
use Psr\Log\LogLevel;
use Selyusize\EventsRouter\Service\Error\PsrLoggerErrorHandler;
$events->setErrorHandler(new PsrLoggerErrorHandler($alertLogger, LogLevel::CRITICAL));
Остановиться на первой ошибке¶
Иногда продолжать нельзя: если не прошла оплата, отправлять письмо «спасибо за покупку» бессмысленно. Для этого есть стратегии:
use Selyusize\EventsRouter\Dispatch\ErrorStrategyEnum;
use Selyusize\EventsRouter\Event;
use Selyusize\EventsRouter\EventRouterFactory;
foreach ([ErrorStrategyEnum::Continue, ErrorStrategyEnum::Stop, ErrorStrategyEnum::Throw] as $strategy) {
echo $strategy->name, ':', PHP_EOL;
$events = EventRouterFactory::create()
->setErrorHandler(new EchoErrorHandler())
->setErrorStrategy($strategy);
$events->listen('order.paid', MarkOrderPaid::class);
$events->listen('order.paid', AccrueBonuses::class);
$events->listen('order.paid', SendReceipt::class);
try {
$report = $events->dispatch(new Event('order.paid'));
foreach ($report->getListeners() as $listener) {
echo ' ', $listener->getListener(), ': ', $listener->getStatus()->name, PHP_EOL;
}
} catch (RuntimeException $error) {
echo ' исключение из dispatch(): ', $error->getMessage(), PHP_EOL;
}
}
Continue:
MarkOrderPaid
ошибка: AccrueBonuses — сервис бонусов недоступен
SendReceipt
MarkOrderPaid: Handled
AccrueBonuses: Failed
SendReceipt: Handled
Stop:
MarkOrderPaid
ошибка: AccrueBonuses — сервис бонусов недоступен
MarkOrderPaid: Handled
AccrueBonuses: Failed
SendReceipt: Skipped
Throw:
MarkOrderPaid
исключение из dispatch(): сервис бонусов недоступен
| Стратегия | Что происходит |
|---|---|
ErrorStrategyEnum::Continue |
по умолчанию: ошибка записана, остальные слушатели работают |
ErrorStrategyEnum::Stop |
ошибка записана, остальные слушатели получают статус Skipped |
ErrorStrategyEnum::Throw |
исключение выходит из dispatch() как есть, обработчик не вызывается |
Throw подходит для тестов и для случаев, когда снаружи открыта транзакция и её нужно откатить. Ошибку обрабатывает вызывающий код, поэтому роутер её не пишет: иначе она попала бы в лог дважды.
Остановить рассылку без ошибки¶
Слушатель может решить, что остальным событие не нужно. Для этого событие или его payload реализует PSR-14 StoppableEventInterface. Когда isPropagationStopped() вернёт true, оставшиеся слушатели получат статус Skipped.
Флаг удобно держать в payload: это общий объект для всех копий события, поэтому изменение, сделанное одним слушателем, увидят следующие.
Что не перехватывается¶
Две ошибки роутер намеренно выпускает наружу при любой стратегии:
- исключение в middleware роутера (
$events->add()): оно не относится ни к одному слушателю; - исключение в самом обработчике ошибок: сломанный обработчик лучше заметить сразу, чем терять ошибки молча.