Управление ответами api для исключений на Symfony с помощью KernelEvents

RedDeveloper
03.01.2023 20:04
Управление ответами api для исключений на Symfony с помощью KernelEvents

Много раз при создании api нам нужно возвращать клиентам разные ответы в зависимости от возникшего исключения.

В этой небольшой статье я хотел бы показать вам, как добиться этого с помощью события Symfony Kernel Exception.

Представим, что мы создаем json api, который выбрасывает (среди прочих) следующие исключения:

  • Symfony\Component\Validator\Exception\ValidationFailedException: Когда валидация входных данных не прошла.
  • Symfony\Component\HttpClient\Exception\TransportException: Когда вызов внешнего ресурса не удается.

И предположим, что мы хотим:

  • При возникновении исключения ValidationFailedException вернуть клиенту список ошибок и код ответа 400 Http bad request.
  • При возникновении TransportException верните простое сообщение о том, что внешний ресурс недоступен, а также код ответа 400 Http bad request.

Начнем с создания сервиса, который управляет поведением каждого исключения.

class ExceptionResponseBuilder
{

    public function __construct(
        private readonly ValidationFailedErrorsBuilder $validationFailedErrorsBuilder
    ) { }

    public function getExceptionResponse(mixed $exception): ?JsonResponse
    {
        return match (get_class($exception)) {
            ValidationFailedException::class   => $this->getResponseForValidationFailedException($exception),
            TransportExceptionInterface::class => $this->getResponseForTransportException($exception),
            default => null
        };
    }

    private function getResponseForValidationFailedException(ValidationFailedException $exception): JsonResponse
    {
        $errors = $this->validationFailedErrorsBuilder->build($exception->getViolations());
        return new JsonResponse(['error' => 'INPUT_DATA_ERRORS', 'data' => $errors], Response::HTTP_BAD_REQUEST);
    }

    private function getResponseForTransportException(TransportExceptionInterface $exception): JsonResponse
    {
        return new JsonResponse(['error' => 'EXTERNAL_RESOURCE_UNAVAILABLE', 'data' => []], Response::HTTP_BAD_REQUEST);
    }
}

Метод getExceptionResponse получает брошенное исключение в качестве параметра, сопоставляет имя его класса с исключениями, которыми мы хотим управлять, и возвращает JsonResponse для соответствующего исключения или null, если совпадений не найдено.

Метод getResponseForValidationFailedException, полагается на сервис ValidationFailedErrorsBuilder для построения массива ошибок из класса Symfony validation ConstraintViolationList. Давайте посмотрим, как это выглядит:

class ValidationFailedErrorsBuilder
{
    public function build(ConstraintViolationListInterface $list): array
    {
        $errors     = [];
        foreach ($list as $violation){
            $errors[$violation->getPropertyPath()] = $violation->getMessage();
        }

        return $errors;
    }
}

Как мы видим, он перебирает нарушения и строит ассоциативный массив, где ключами являются пути ошибок (в форме это будут имена полей), а значениями - сообщения об ошибках.

Теперь давайте создадим подписчика событий, который будет слушать событие KernelEvents::EXCEPTION.

class KernelSubscriber implements EventSubscriberInterface
{

    public function __construct(
        private readonly ExceptionResponseBuilder $exceptionResponseBuilder
    ) { }
    
    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::EXCEPTION => ['onKernelException']
        ];
    }

    public function onKernelException(ExceptionEvent $event): void
    {
        $exception = $event->getThrowable();
        $response  = $this->exceptionResponseBuilder->getExceptionResponse($exception);
        if ($response){
            $event->setResponse($response);
        }
    }
}

Когда возникает исключение и выполняется метод onKernelException, он использует наш сервис ExceptionResponseBuilder для получения JsonResponse в соответствии с классом исключения. Если JsonResponse будет возвращен, он будет помещен в событие

Теперь давайте протестируем наш конструктор ответов на исключения, чтобы убедиться, что он ведет себя так, как ожидается.

Для создания и выполнения тестов я использовал пакет symfony phpunit. Подробнее о нем вы можете узнать здесь: https://symfony.com/doc/current/testing.html
class ExceptionResponseBuilderTest extends KernelTestCase
{
    public function testTransportExceptionResponse(): void
    {
        $exceptionResponseBuilder = static::getContainer()->get('App\Exception\ExceptionResponseBuilder');

        $response = $exceptionResponseBuilder->getExceptionResponse(new TransportException('Resource unavailable'));
        $this->assertInstanceOf(JsonResponse::class, $response);

        $content = json_decode($response->getContent(), true);
        $this->assertEquals('EXTERNAL_RESOURCE_UNAVAILABLE', $content['error']);
    }

    public function testValidationExceptionResponse(): void
    {
        $exceptionResponseBuilder = static::getContainer()->get('App\Exception\ExceptionResponseBuilder');

        $validationFailedException = new ValidationFailedException(
            null,
            new ConstraintViolationList(
                [
                    new ConstraintViolation('Name invalid', null, [], null, 'name', '658v')
                ]
            )
        );

        $response = $exceptionResponseBuilder->getExceptionResponse($validationFailedException);
        $this->assertInstanceOf(JsonResponse::class, $response);

        $content = json_decode($response->getContent(), true);
        $this->assertEquals('INPUT_DATA_ERRORS', $content['error']);
        $this->assertNotEmpty($content['data']);
    }

    public function testNoMappedException(): void
    {
        $exceptionResponseBuilder = static::getContainer()->get('App\Exception\ExceptionResponseBuilder');
        $response = $exceptionResponseBuilder->getExceptionResponse(new \Exception('error'));
        $this->assertNull($response);
    }
}

Как мы можем видеть в классе, мы протестировали каждый случай соответствия ExceptionResponseBuilder, чтобы убедиться, что все случаи работают так, как ожидалось. После выполнения тестов мы видим успешный результат

Ускорьте разработку веб-приложений Laravel с помощью этих бесплатных стартовых наборов
Ускорьте разработку веб-приложений Laravel с помощью этих бесплатных стартовых наборов

31.03.2023 11:40

Laravel - это мощный PHP-фреймворк, используемый для создания масштабируемых и надежных веб-приложений. Одним из преимуществ Laravel является его обширная экосистема пакетов и стартовых наборов, которые делают создание веб-приложений более быстрым и эффективным. В этой статье мы рассмотрим лучшие...

Что такое двойные вопросительные знаки (??) в JavaScript?
Что такое двойные вопросительные знаки (??) в JavaScript?

31.03.2023 11:16

Как безопасно обрабатывать неопределенные и нулевые значения в коде с помощью Nullish Coalescing

Создание ресурсов API Laravel: Советы по производительности и масштабируемости
Создание ресурсов API Laravel: Советы по производительности и масштабируемости

31.03.2023 11:06

Создание API-ресурса Laravel может быть непростой задачей. Она требует глубокого понимания возможностей Laravel и лучших практик, чтобы обеспечить масштабируемость, производительность и безопасность вашего API. В этой статье мы рассмотрим несколько советов по созданию ресурсов API Laravel,...

Как сделать компонент справочного центра с помощью TailwindCSS
Как сделать компонент справочного центра с помощью TailwindCSS

31.03.2023 10:15

Справочный центр - это веб-сайт, где клиенты могут найти ответы на свои вопросы и решения своих проблем. Созданный для решения многих распространенных вопросов, которые получает бренд, справочный центр должен упрощать клиентам поиск ответов, которые они ищут.

Асинхронная передача данных с помощью sendBeacon в JavaScript
Асинхронная передача данных с помощью sendBeacon в JavaScript

30.03.2023 14:11

В современных веб-приложениях отправка данных из JavaScript на стороне клиента на сервер является распространенной задачей. Одним из популярных способов решения этой задачи является использование запросов AJAX. Однако существуют определенные ситуации, когда AJAX не подходит, например, когда...

Как подобрать выигрышные акции с помощью анализа и визуализации на Python
Как подобрать выигрышные акции с помощью анализа и визуализации на Python

30.03.2023 13:54

Отказ от ответственности: Эта статья предназначена только для демонстрации и не должна использоваться в качестве инвестиционного совета.