Сегодня я хочу поделиться своим скромным опытом и показать, как можно сделать перехватчик исключений, не используя 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. coylOne
    29.01.2016 11:50
    +1

    Я правильно понимаю, что один сервис отвечает за обработку всех исключений, кто бы их ни кинул?


    1. ivanuzzo
      29.01.2016 11:57
      +1

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


      1. coylOne
        29.01.2016 12:02
        -1

        То есть если я пишу бандл, которы хочет как-то реагировать на исключения, я должен реализовать сервис, подходящий под ваши нужды и перед использованием попросить вас заинжектить его и дёрнуть в контроллере?


        1. ivanuzzo
          29.01.2016 12:09
          +1

          Нет, не так. В своем посте я писал не про бандл, а про обычное приложение. Само собой, для бандла или компонента нужно реализовывать листенер. На мое мировоззрение в этом плане немало повлияло общение с разработчиками Симфони (IRC и Stackoverflow), в частности здесь мне отвечали на этот вопрос.


          1. coylOne
            29.01.2016 13:32
            -1

            В итоге мы получаем смешение подходов и еще большую путаницу при поддержке: бандлы, которые мы подключаем используют одну модель, приложение (я так понимаю, состоящее из одного бандла) имеет другую. Либо так, либо мы сразу говорим о том, что приложение не подразумевает расширения.


            1. nikita2206
              29.01.2016 14:04

              Расширение можно делать очень просто — изменяя код. В случае с third-party библиотеками ты не можешь менять их код, поэтому прибегают к разным подходам, в т.ч. к ивент-модели. Но когда ты можешь менять код и этот код не будет работать вне твоего приложения, нужно делать наиболее прямо.


  1. ruFog
    29.01.2016 12:02
    +3

    По поводу осоновного утверждения не соглашусь. Использование диспетчера событий наоборот делает код более гибким и расширямым. Если глянуть на зависимости разных бандлов, то EventDispatcher есть почти везде. Концентрирование всё в одном месте — это скорее антипаттерн.


    1. coylOne
      29.01.2016 12:04
      +1

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


      1. nikita2206
        29.01.2016 13:17
        +1

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


        1. coylOne
          29.01.2016 13:36

          Мы говорим о бизнес-логике приложения или об уровне представления? Я вижу тут обработку исключений на уровне контроллера и общего хендлера. И чем обработка исключений через ExceptionController отвязывает нас от фреймворка? Скорее, наоборот, привязывает намертво: события – это простые value-объекты, и их можно переиспользовать, а перегруженный ExcetpionController – это прямое наследие симфони.


          1. nikita2206
            29.01.2016 14:01

            Уровень предстваления твоего приложения входит в рамки твоего приложения. События привязывают тебя к symfony/event-dispatcher, сервис ни к чему тебя не привязывает, а контроллер это адаптер.


            1. coylOne
              29.01.2016 14:17

              Контроллер не меньше завязан на симфони, чем диспетчер. Если ты отвязываешься от симфони, тебе надо реализовать DI, контроллер, респонс. В случае с диспетчером – диспетчер.


              1. nikita2206
                29.01.2016 14:43

                Как я сказал, контроллер — это адаптер.


        1. hlogeon
          29.01.2016 17:18

          У меня бизнес-логика реализованна на уровне бандлов и приложение собирается из таких вот внутренних бандлов. Благодаря этому, над моим приложением работает кучас разработчиков, не мешая друг другу, мне просто тестировать все компоненты приложения по отдельности и я могу собирать на разных инстансах разные конфигурации приложения(одно приложение умеет работать с платежами и пользователями, другое только с платежами, третье только с пользователями и так далее).
          А вот этого:

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

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


          1. nikita2206
            29.01.2016 17:34

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

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


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


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


        1. VolCh
          31.01.2016 20:00

          Бизнес логика самого приложения вообще не должна быть в каком-либо бандле


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


    1. ivanuzzo
      29.01.2016 12:18

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

      А по поводу вашего утверждения я согласен.


      1. ruFog
        29.01.2016 12:22

        Возможно в курсе, но кому-то может быть полезно следующее. Есть готовый FOSRestBundle. Рекомендую погуглить как его правильно «готовить». Там и сериализация и правила форматирования ответа и, в том числе, спец обработчик исключений, который можно кастомизировать специально для своего REST-API. Успехов!


      1. shoomyst
        29.01.2016 13:56

        Необрабатываемое бизнес-логикой исключение это и есть ошибка 500.


  1. alxsad
    29.01.2016 14:15
    +1

    В этом кейсе использование событий очень даже хорошее решение. На одном из проектов попробовал такую архитектуру: приложение ничего не знает о том как обрабатывать исключения и занимается лишь их бросанием, а уже middleware в зависимости от разных факторов, занимается конвертированием исключений в различное представление, будь то json или xml.