Обработка исключений в Symfony 3.4

Не исключено, что в процессе долгого путешествия от запроса до ответа, возникнет та или иная ошибка. По умолчанию, ядро проинструктировано перехватывать любое исключение и даже после этого оно пытается подобрать подходящий для него ответ Response. Как мы уже видели, обработка каждого запроса обёрнута в блок try/catch:

public function handle(
    Request $request,
    $type = HttpKernelInterface::MASTER_REQUEST,
    $catch = true
) {
    try {
        return $this->handleRaw($request, $type);
    } catch (\Exception $e) {
        if (false === $catch) {
            throw $e;
        }

        return $this->handleException($e, $request, $type);
    }
}

Когда переменная $catch имеет значение истина (true), вызывается метод handleException() и ожидается, что он вернёт объект ответа. Этот метод отправляет событие KernelEvents::EXCEPTION (kernel.exception) c экземпляром события GetResponseForExceptionEvent.

use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;

private function handleException(\Exception $e, $request, $type)
{
    $event = new GetResponseForExceptionEvent($this, $request, $type, $e);
    $this->dispatcher->dispatch(KernelEvents::EXCEPTION, $event);

    // a listener might have replaced the exception
    $e = $event->getException();

    if (!$event->hasResponse()) {
        throw $e;
    }

    $response = $event->getResponse();

    ...
}

Слушатели события kernel.exception могут:

  • Установить объект ответа Response, соответствующий пойманному исключению.
  • Заменить исходный объект исключения.

Если ни один из слушателей не вызвал метод события setResponse(), исключение будет вызвано еще раз, но в этот раз оно не будет перехвачено автоматически. Таким образом, если в настройках вашего интерпритатора PHP параметр display_errors имеет значение истина (true), PHP просто отобразит ошибку как есть, без изменений (если отображение ошибок у вас отключено — не будет выведено ничего). В случае же, если один из слушателей установил объект ответа Response, класс HttpKernel проверяет этот объект на предмет корректной установки http статус-кода:

// проверяем наличие специфического статус-кода
if ($response->headers->has('X-Status-Code')) {
    $response->setStatusCode($response->headers->get('X-Status-Code'));

    $response->headers->remove('X-Status-Code');
} elseif (
    !$response->isClientError()
    && !$response->isServerError()
    && !$response->isRedirect()
) {
    // убеждаемся, что у нас действительно есть ответ
    if ($e instanceof HttpExceptionInterface) {
        // сохраняем HTTP статус-код и заголовки
        $response->setStatusCode($e->getStatusCode());
        $response->headers->add($e->getHeaders());
    } else {
        $response->setStatusCode(500);
    }
}

Это очень удобно, так как мы можем форсировать любой статус-код, добавляя заголовок X-Status-Code к объекту ответа Response (важно! это будет работать лишь для исключений, которые были перехвачены в HttpKernel), или же создав исключение, которое реализует интерфейс HttpExceptionInterface. В противном случае статус-код будет иметь значение по-умолчанию — 500, что означает Internal server error. Это намного лучше, чем то, что предлагает нам «чистый» PHP, который в случае ошибки вернул бы ответ со статусом 200, что означает OK. Когда слушатель установил объект ответа, этот ответ не будет обрабатываться каким-то иным образом, нежели обычный ответ, поэтому последний шаг в этом процессе — фильтрация ответа. Если в ходе фильтрации ответа будет сгенерировано другое исключение, это исключение будет проигнорировано и клиенту будет отправлен неотфильтрованный ответ:

try {
    return $this->filterResponse($response, $request, $type);
} catch (\Exception $e) {
    return $response;
}

Примечательные слушатели события kernel.exception

Слушатель ExceptionListener компонента HttpKernel пытается самостоятельно обработать исключение, логгируя его (если доступен логгер) и выполняя контроллер, который может отобразить страницу с информацией об ошибке. Как правило, это контроллер, который указан в файле конфигурации config.yml:

twig:
    # points to Symfony\Bundle\TwigBundle\Controller\ExceptionController
    exception_controller: twig.controller.exception:showAction

Другой важный слушатель — это ExceptionListener компонента Security. Этот слушатель проверяет, является ли исходное исключение экземпляром AuthenticationException или же AccessDeniedException. В первом случае он начинает процесс аутентификации, если это возможно. Во втором случае он пытается перелогинить пользователя (например, если тот использовал remember me опцию при логине), если же это не выходит, предоставляет возможность access denied handler разобраться с возникшей ситуацией.