Статья рассчитана на самураев, кто находится в самом начале пути 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 канале.
Спасибо за внимание!
Комментарии (5)
BoShurik
14.09.2022 17:25+1Если в проекте подключен
symfony/serializer
, то будет работать из коробкиcomposer create-project symfony/skeleton symfony-accept-format cd symfony-accept-format composer req serializer
GET http://127.0.0.1:8880/ Accept: application/json #{ # "type": "https:\/\/tools.ietf.org\/html\/rfc2616#section-10", # "title": "An error occurred", # "status": 500, # "detail": "Internal Server Error" #} ### GET http://127.0.0.1:8880/ Accept: text/html ### #<body> #<div class="container"> # <h1>Oops! An Error Occurred</h1> # <h2>The server returned a "500 Internal Server Error".</h2> # # <p> # Something is broken. Please let us know what you were doing when this error occurred. # We will fix it as soon as possible. Sorry for any inconvenience caused. # </p> #</div> #</body> #</html>
BoShurik
14.09.2022 17:53+1Как бонус:
GET http://127.0.0.1:8880/ Accept: application/xml ### #<?xml version="1.0"?> #<response> # <type>https://tools.ietf.org/html/rfc2616#section-10</type> # <title>An error occurred</title> # <status>500</status> # <detail>Internal Server Error</detail> #</response>
misantron
Кроме красивого отображения ошибки стандартный Symfony обработчик еще логирует информацию об ошибке, в данной версии логирование тоже не помешало бы
gophp Автор
Стандартный обработчик, который занимается логированием также зарегистрирован, у него более низкий приоритет.
Конечно, в классе App\Core\EventListener\ExceptionListener лучше бы обозначить метод exceptionFormatConverter, и определить его тегом kernel.event_listener, чтобы было нагляднее назначение созданного преобразователя ошибок.
App\Core\EventListener\ExceptionListener:
tags:
- { name: kernel.event_listener, event: kernel.exception, method: exceptionFormatConverter }
misantron
я реализовывал через EventSubscriber возможно с приоритетами не разобрался
как еще одна мысль по улучшению - передавать в конструктор env приложения, чтобы вызов логики обработчика можно было варьировать для разных окружений