Статья рассчитана на самураев, кто находится в самом начале пути Symfony и не способен самостоятельно постичь фреймворк силой одной лишь документации.
Зачем я пишу об этом?
Первая причина - это моя личная мотивация начать-таки наконец писать статьи на Хабре. Говорят, дорога возникает под шагами идущего. И вот я встал на путь менторства и уже веду Youtube канал, где публикую обучающие видео по фреймворку Symfony. Но еще никогда не писал технические статьи.
Вторая причина - этот материал кому-то окажется полезен.
Работая над проектом, у меня возникла задача: возвращать информацию об Exception в формате JSON если клиент указывает поддерживаемый им MIME тип application/json в запросе, используя заголовок Accept.
Простыми словами - если клиенту нужна ошибка в JSON, то дать ему JSON. В других фатальных запросах возвращать стандартную ошибку в формате HTML.
Давайте рассмотрим пример, как решить эту задачу используя механизм обработки встроенного события Symfony.
В официальной документации в разделе о событиях и слушателях приводится пример обработки встроенного события исключения. Воспользуемся этим.
Есть 2 способа обработки встроенного события exception: через слушатели и подписчики.
Первый способ - EventListener
Создадим класс слушателя:
<?php
declare(strict_types=1);
namespace App\Core\EventListener;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class ExceptionListener
{
const MIME_JSON = 'application/json';
public function onKernelException(ExceptionEvent $event): void
{
// Получаем MIME тип из заголовка Accept
$acceptHeader = $event->getRequest()->headers->get('Accept');
if ($acceptHeader === self::MIME_JSON) {
$exception = $event->getThrowable();
$response = new JsonResponse();
$response->setContent($this->exceptionToJson($exception));
// HttpException содержит информацию о заголовках и статусе, испольузем это
if ($exception instanceof HttpExceptionInterface) {
$response->setStatusCode($exception->getStatusCode());
$response->headers->replace($exception->getHeaders());
} else {
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
}
$event->setResponse($response);
}
}
public function exceptionToJson(\Throwable $exception): string
{
return json_encode(
[
'message' => $exception->getMessage(),
'code' => $exception->getCode(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString(),
]
);
}
}
Далее нам необходимо зарегистрировать класс в файле services.yaml с указанием соответствующего тега.
App\Core\EventListener\ExceptionListener:
tags:
- { name: kernel.event_listener, event: kernel.exception }
Проверим, зарегистрировался ли слушатель в диспетчере событий, используя команду:
php bin/console debug:event-dispatcher kernel.exception
Результат:

Второй способ - Event Subscriber
Добавим класс подписчика
<?php
declare(strict_types=1);
namespace App\Core\EventListener;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class BuiltInEventsSubscriber
{
public function onKernelException(ExceptionEvent $event)
{
// Код обработки ошибки можно взять из класса ExceptionListener
}
public static function getSubscribedEvents()
{
return [
KernelEvents::EXCEPTION => 'onKernelException',
];
}
}
Если в проекте включена автоматическая регистрация сервисов autoconfigure: true, то дополнительно регистрировать подписчик не нужно. В противном случае пропишем сервис в файле services.yaml
App\Core\EventListener\BuiltInEventsSubscriber:
tags:
- { name: kernel.event_subscriber }
Проверим, зарегистрировался ли подписчик в диспетчере событий, используя команду:
php bin/console debug:event-dispatcher kernel.exception
Результат:

Тестирование
Проверим как выглядит ответ с ошибкой исключения при запросе в браузере:

Как выглядит ответ при запросе с указанием заголовка Accept.
Запрос:
curl http://127.0.0.1:888/health-check -H "Accept: application/json"
Ответ:
{
"message":"Division by zero",
"code":0,
"file":"\/var\/www\/src\/Shared\/Infrastructure\/Controller\/HealthCheckAction.php",
"line":16,
"trace":"#0 \/var\/www\/vendor\/symfony\/http-kernel\/HttpKernel.php(153): ....... require_once('\/var\/www\/vendor...')\n#6 {main}"
}
Перечень данных возвращаемой ошибки можно изменить под нужды в методе onKernelException.
Рабочий пример можно посмотреть в репозитории, где есть еще много чего интересного :)
Другой обучающий материал по Symfony 6 представлен на моем Youtube канале.
Спасибо за внимание!