Symfony2 перехватчик исключений с помощью сервисов или как избежать использования Event Listener

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

Я считаю, что использование Event Listener'ов в обычном приложении делает код запутанным, к тому же многие неопытные разработчики злоупотребляют данным подходом (сам так делал). А вот использование сервисов делает код понятным, так как они вызываются в том месте, в котором объявлены. И как вы уже поняли, далее речь пойдет именно о сервисах.

Итак, начнем.

Сначала переопределим ExceptionController, о чем скромно намекает официальная документация:

namespace AppBundle\Controller;

use Symfony\Bundle\TwigBundle\Controller\ExceptionController as Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\FlattenException;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
use AppBundle\Exception\ExceptionHandler;

class ExceptionController extends Controller
{
    public function __construct(ExceptionHandler $handler) 
    {
        $this->handler = $handler;
    }

    public function showAction(Request $request, FlattenException $exception, DebugLoggerInterface $logger = null)
    {
        $message = $this->handler->handle($exception)->getMessage();

        return new JsonResponse(array(
            'message' => $message
        ));
    }

}

Далее создадим сервис, который занимается обработкой исключений:

namespace AppBundle\Exception;

use Symfony\Component\Security\Core\Exception\AccessDeniedException;

class ExceptionHandler
{
    private $message = null;

    public function handle($exception)
    {
        switch($exception->getClass()) {
            case 'Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException' :
                $this->message = "Need full authentication";
                break;
            case 'Symfony\Component\Security\Core\Exception\AccessDeniedException':
                $this->message = "Access Denied";
                break;
            /**
            * Указываем действия для всех нужных исключений
            **/
            default:
                break;
        }

        return $this;
    }

    public function getMessage()
    {
        return $this->message;
    }
}

Теперь регистрируем наш сервис:

# services.yml
app_bundle.exception.handler:
    class: AppBundle\Exception\ExceptionHandler


Далее регистрируем наш контроллер как сервис(не забываем передать в него Exception Handler):

# services.yml
app_bundle.exception.controller:
    class: AppBundle\Controller\ExceptionController
    arguments:
        - @app_bundle.exception.handler

Осталось самое главное: указать в config.yml, что исключения обрабатывает именно наш контроллер:

# config.yml
# Twig Configuration
twig:
    exception_controller: app_bundle.exception.controller:showAction

Надеюсь на вашу конструктивную критику, а также на то, что для кого-то эта статья окажется полезной.
Поделиться публикацией
Ой, у вас баннер убежал!

Ну, и что?
Реклама
Комментарии 20
  • +1
    Я правильно понимаю, что один сервис отвечает за обработку всех исключений, кто бы их ни кинул?
    • +1
      В данном примере — да, но ничто не мешает сделать сервисы для каждой группы исключений и заинжектить их в основной сервис.
      • –1
        То есть если я пишу бандл, которы хочет как-то реагировать на исключения, я должен реализовать сервис, подходящий под ваши нужды и перед использованием попросить вас заинжектить его и дёрнуть в контроллере?
        • +1
          Нет, не так. В своем посте я писал не про бандл, а про обычное приложение. Само собой, для бандла или компонента нужно реализовывать листенер. На мое мировоззрение в этом плане немало повлияло общение с разработчиками Симфони (IRC и Stackoverflow), в частности здесь мне отвечали на этот вопрос.
          • –1
            В итоге мы получаем смешение подходов и еще большую путаницу при поддержке: бандлы, которые мы подключаем используют одну модель, приложение (я так понимаю, состоящее из одного бандла) имеет другую. Либо так, либо мы сразу говорим о том, что приложение не подразумевает расширения.
            • 0
              Расширение можно делать очень просто — изменяя код. В случае с third-party библиотеками ты не можешь менять их код, поэтому прибегают к разным подходам, в т.ч. к ивент-модели. Но когда ты можешь менять код и этот код не будет работать вне твоего приложения, нужно делать наиболее прямо.
    • +3
      По поводу осоновного утверждения не соглашусь. Использование диспетчера событий наоборот делает код более гибким и расширямым. Если глянуть на зависимости разных бандлов, то EventDispatcher есть почти везде. Концентрирование всё в одном месте — это скорее антипаттерн.
      • +1
        Конечно. Описанный подход ломает модульность, увеличивает связанность, усложняет поддержку, убивает идею бандлов.
        • +1
          Идея бандлов нужна только для распространения кода, а не для конкретного приложения. Бизнес логика самого приложения вообще не должна быть в каком-либо бандле, иначе ты как раз увеличиваешь связанность своей бизнес-логики с фреймворком symfony2
          • 0
            Мы говорим о бизнес-логике приложения или об уровне представления? Я вижу тут обработку исключений на уровне контроллера и общего хендлера. И чем обработка исключений через ExceptionController отвязывает нас от фреймворка? Скорее, наоборот, привязывает намертво: события – это простые value-объекты, и их можно переиспользовать, а перегруженный ExcetpionController – это прямое наследие симфони.
            • 0
              Уровень предстваления твоего приложения входит в рамки твоего приложения. События привязывают тебя к symfony/event-dispatcher, сервис ни к чему тебя не привязывает, а контроллер это адаптер.
              • 0
                Контроллер не меньше завязан на симфони, чем диспетчер. Если ты отвязываешься от симфони, тебе надо реализовать DI, контроллер, респонс. В случае с диспетчером – диспетчер.
                • 0
                  Как я сказал, контроллер — это адаптер.
            • 0
              У меня бизнес-логика реализованна на уровне бандлов и приложение собирается из таких вот внутренних бандлов. Благодаря этому, над моим приложением работает кучас разработчиков, не мешая друг другу, мне просто тестировать все компоненты приложения по отдельности и я могу собирать на разных инстансах разные конфигурации приложения(одно приложение умеет работать с платежами и пользователями, другое только с платежами, третье только с пользователями и так далее).
              А вот этого:
              Бизнес логика самого приложения вообще не должна быть в каком-либо бандле, иначе ты как раз увеличиваешь связанность своей бизнес-логики с фреймворком symfony2

              Я прямо таки откровенно не понял.
              • 0
                То же самое можно сделать не используя бандлы, в чём аргумент?

                Бизнес логика самого приложения вообще не должна быть в каком-либо бандле, иначе ты как раз увеличиваешь связанность своей бизнес-логики с фреймворком symfony2


                Я прямо таки откровенно не понял.


                Попробуй теперь перевезти свой код из папочки `src/` на другой фреймворк. Там не просто какой-нибудь слой адаптеров поправить, там придется перелопатить иерархию директорий. Если есть желание разбить два компонента — один для работы с юзерами и один для работы с платежами, ничего не мешает это сделать просто вынеся их в разные директории, для этого не нужны разные бандлы.
              • 0
                Бизнес логика самого приложения вообще не должна быть в каком-либо бандле


                Бизнес-логика в либе, а бандл — интеграция либы с Симфони. Разделять их или нет на отдельные пакеты композера, держать в одном дереве каталогов или нет и т. п. — может быть хорошо в одном случае и плохо в другом.
            • 0
              Единственное, что могу сказать: данный подход описан для обычного приложения, а не для бандла. Например, я этот перехватчик использую в своем http api, когда нужно отреагировать на определенную ошибку на фронтенде. В противном случае, у меня будет возвращаться ошибка 500 для всех исключений, которые мое приложение выбрасывает (кстати, их немного).

              А по поводу вашего утверждения я согласен.
              • 0
                Возможно в курсе, но кому-то может быть полезно следующее. Есть готовый FOSRestBundle. Рекомендую погуглить как его правильно «готовить». Там и сериализация и правила форматирования ответа и, в том числе, спец обработчик исключений, который можно кастомизировать специально для своего REST-API. Успехов!
                • 0
                  Необрабатываемое бизнес-логикой исключение это и есть ошибка 500.
              • +1
                В этом кейсе использование событий очень даже хорошее решение. На одном из проектов попробовал такую архитектуру: приложение ничего не знает о том как обрабатывать исключения и занимается лишь их бросанием, а уже middleware в зависимости от разных факторов, занимается конвертированием исключений в различное представление, будь то json или xml.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое