Как избежать регистрации ожидаемых исключений, которые корректно преобразуются в коды состояния Api-Platform? - PullRequest
3 голосов
/ 07 апреля 2020

В некоторых маршрутах для нашего проекта Api-Platform исключения составляют throw n для некоторых распространенных ошибок.

Например, при вызове POST /orders, NewOrderHandler может выбросить любой из этих двух, если это необходимо :

  • NotEnoughStock
  • NotEnoughCredit

Все эти исключения относятся к иерархии DomainException.

Эти исключения правильно преобразованы в код состояния 400 в ответе с использованием конфигурации exception_to_status, и ответ содержит соответствующее сообщение об ошибке. Пока все хорошо.

exception_to_status:
    App\Order\NotEnoughStock: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST
    App\Order\NotEnoughCredit: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST

Единственная проблема заключается в том, что исключение по-прежнему регистрируется как ошибка CRITICAL, которая рассматривается как "неперехваченное исключение". Это регистрируется даже в рабочей среде.

Я бы ожидал, что при преобразовании в правильный код состояния (например, !== 500) эти исключения будут рассматриваться как «обработанные» и, следовательно, не будут загрязнять журналы.

Бросать исключения из обработчика удобно, потому что это помогает справиться с транзакционностью и автоматически генерирует соответствующее сообщение об ошибке. Это работает для сети и консоли.

Разве эти транзакции не должны рассматриваться как обработанные? Нужно ли создавать другого слушателя исключений, чтобы справиться с этим? И если создать прослушиватель исключений, как это сделать, чтобы он не мешал нормализации ошибок Api-Platform?

Ответы [ 3 ]

3 голосов
/ 15 апреля 2020

Ответ прост: обработка исключения не приводит к перехвату исключения.

Даже если вы преобразуете свое исключение в ошибку 400, ваше исключение все еще не обработано ... поэтому symfony регистрирует его и это делается здесь .

Если вы не хотите регистрировать какие-либо DomainException, просто переопределите метод logException(), чтобы пропустить ведение журнала, если это экземпляр DomainException.

Вот пример:

namespace App\EventListener;

use Symfony\Component\HttpKernel\EventListener\ErrorListener;

class ExceptionListener extends ErrorListener
{
    protected function logException(\Exception $exception, string $message): void
    {
        if ($exception instanceof DomainException) {
            return;
        }

        parent::logException($exception, $message);
    }
}

Наконец, вам нужно указать Symfony использовать этот класс вместо Symfony. Поскольку для определения сервиса exception_listener не существует параметра класса, я рекомендую использовать проход компилятора для замены класса.

namespace App;

use App\EventListener\ExceptionListener;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class OverrideServiceCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $definition = $container->getDefinition('exception_listener');
        $definition->setClass(ExceptionListener::class);
    }
}

Подробнее см. Переопределение пакета .

В качестве альтернативы, просто украсьте exception_listener сервисом самостоятельно и без необходимости прохода компилятора:

App\EventListener\ExceptionListener:
        decorates: 'exception_listener' 
2 голосов
/ 12 апреля 2020

Я протестировал его на фиктивном приложении и получил:

11 апреля 21:36:11 | CRITI | REQUES Uncaught PHP Exception App \ Exception \ DomainException: «Это больше не регистрируется» в D: \ www\campagne \ src \ DataPersister \ StationDataPersister. php строка 53 апр. 11 23:36:12 | WARN | SERVER POST (400) / api / station

Вы можете реализовать собственную стратегию активации журнала:

Этот код основан на стратегии активации HttpCode

namespace App\Log

use App\Exception\DomainException;
use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy;
use Symfony\Component\HttpKernel\Exception\HttpException;

/**
 * Activation strategy for logs
 */
class LogActivationStrategy extends ErrorLevelActivationStrategy
{

    public function __construct()
    {
        parent::__construct('error');
    }

    public function isHandlerActivated(array $record): bool
    {
        $isActivated = parent::isHandlerActivated($record);
        if ($isActivated && isset($record['context']['exception'])) {
            $exception = $record['context']['exception'];

            // This is a domain exception, I don't log it
            return !$exception instanceof DomainException;

            // OR if code could be different from 400
            if ($exception instanceof DomainException) {
                // This is a domain exception 
                // You log it when status code is different from 400.
                return 400 !== $exception->getStatusCode();
            }
        }

        return $isActivated;
    }
}

Мы также должны указать Monolog использовать нашу ActivationStrategy

monolog:
    handlers:
        main:
            type: fingers_crossed
            action_level: info
            handler: nested
            activation_strategy: App\Log\LogActivationStrategy 
        nested:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: info
       console:
            type: console
            process_psr_3_messages: false
            channels: ["!event", "!doctrine", "!console"]

Теперь мой журнал содержит только:

11 апреля 23:41:07 | WARN | SERVER POST (400) / api / station

Как и @yivi, я не люблю свое решение, потому что каждый раз, когда приложение пытается что-то зарегистрировать, вы теряете время в этой функции .. И этот метод не меняет журнал, он удаляет его.

1 голос
/ 09 апреля 2020

В то время как в Monolog, при использовании обработчика журнала fingers_crossed, позволит вам исключить из запросов регистрации, отвечающих с определенными статусами , это будет сделано только в том случае, если исключение является экземпляром HttpException :

Я справился с этим, внедрив подписчика для преобразования исключения в BadRequestHttpException.

final class DomainToHttpExceptionSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): iterable
    {
        return [ KernelEvents::EXCEPTION => 'convertException'];
    }

    public function convertException(ExceptionEvent $event): void
    {
        $exception = $event->getThrowable();

        if ($exception instanceof DomainException) {
            $event->setThrowable(
                new BadRequestHttpException(
                    $exception->getMessage(),
                    $exception
                )
            );

        }
    }
}

. Это в сочетании с этой монологической конфигурацией помогает:

monolog:
    handlers:
        fingers:
              type: fingers_crossed
              action_level: warning
              excluded_http_codes:
                - 404
                - 400

Я получил это от этого ответа по проблеме GitHub. Это работает, но я не люблю решение. Надеюсь, какой-то другой ответ улучшит это.

...