Я считаю, что использование 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)
ruFog
29.01.2016 12:02+3По поводу осоновного утверждения не соглашусь. Использование диспетчера событий наоборот делает код более гибким и расширямым. Если глянуть на зависимости разных бандлов, то EventDispatcher есть почти везде. Концентрирование всё в одном месте — это скорее антипаттерн.
coylOne
29.01.2016 12:04+1Конечно. Описанный подход ломает модульность, увеличивает связанность, усложняет поддержку, убивает идею бандлов.
nikita2206
29.01.2016 13:17+1Идея бандлов нужна только для распространения кода, а не для конкретного приложения. Бизнес логика самого приложения вообще не должна быть в каком-либо бандле, иначе ты как раз увеличиваешь связанность своей бизнес-логики с фреймворком symfony2
coylOne
29.01.2016 13:36Мы говорим о бизнес-логике приложения или об уровне представления? Я вижу тут обработку исключений на уровне контроллера и общего хендлера. И чем обработка исключений через ExceptionController отвязывает нас от фреймворка? Скорее, наоборот, привязывает намертво: события – это простые value-объекты, и их можно переиспользовать, а перегруженный ExcetpionController – это прямое наследие симфони.
nikita2206
29.01.2016 14:01Уровень предстваления твоего приложения входит в рамки твоего приложения. События привязывают тебя к symfony/event-dispatcher, сервис ни к чему тебя не привязывает, а контроллер это адаптер.
coylOne
29.01.2016 14:17Контроллер не меньше завязан на симфони, чем диспетчер. Если ты отвязываешься от симфони, тебе надо реализовать DI, контроллер, респонс. В случае с диспетчером – диспетчер.
hlogeon
29.01.2016 17:18У меня бизнес-логика реализованна на уровне бандлов и приложение собирается из таких вот внутренних бандлов. Благодаря этому, над моим приложением работает кучас разработчиков, не мешая друг другу, мне просто тестировать все компоненты приложения по отдельности и я могу собирать на разных инстансах разные конфигурации приложения(одно приложение умеет работать с платежами и пользователями, другое только с платежами, третье только с пользователями и так далее).
А вот этого:
Бизнес логика самого приложения вообще не должна быть в каком-либо бандле, иначе ты как раз увеличиваешь связанность своей бизнес-логики с фреймворком symfony2
Я прямо таки откровенно не понял.nikita2206
29.01.2016 17:34То же самое можно сделать не используя бандлы, в чём аргумент?
Бизнес логика самого приложения вообще не должна быть в каком-либо бандле, иначе ты как раз увеличиваешь связанность своей бизнес-логики с фреймворком symfony2
Я прямо таки откровенно не понял.
Попробуй теперь перевезти свой код из папочки `src/` на другой фреймворк. Там не просто какой-нибудь слой адаптеров поправить, там придется перелопатить иерархию директорий. Если есть желание разбить два компонента — один для работы с юзерами и один для работы с платежами, ничего не мешает это сделать просто вынеся их в разные директории, для этого не нужны разные бандлы.
VolCh
31.01.2016 20:00Бизнес логика самого приложения вообще не должна быть в каком-либо бандле
Бизнес-логика в либе, а бандл — интеграция либы с Симфони. Разделять их или нет на отдельные пакеты композера, держать в одном дереве каталогов или нет и т. п. — может быть хорошо в одном случае и плохо в другом.
ivanuzzo
29.01.2016 12:18Единственное, что могу сказать: данный подход описан для обычного приложения, а не для бандла. Например, я этот перехватчик использую в своем http api, когда нужно отреагировать на определенную ошибку на фронтенде. В противном случае, у меня будет возвращаться ошибка 500 для всех исключений, которые мое приложение выбрасывает (кстати, их немного).
А по поводу вашего утверждения я согласен.ruFog
29.01.2016 12:22Возможно в курсе, но кому-то может быть полезно следующее. Есть готовый FOSRestBundle. Рекомендую погуглить как его правильно «готовить». Там и сериализация и правила форматирования ответа и, в том числе, спец обработчик исключений, который можно кастомизировать специально для своего REST-API. Успехов!
alxsad
29.01.2016 14:15+1В этом кейсе использование событий очень даже хорошее решение. На одном из проектов попробовал такую архитектуру: приложение ничего не знает о том как обрабатывать исключения и занимается лишь их бросанием, а уже middleware в зависимости от разных факторов, занимается конвертированием исключений в различное представление, будь то json или xml.
coylOne
Я правильно понимаю, что один сервис отвечает за обработку всех исключений, кто бы их ни кинул?
ivanuzzo
В данном примере — да, но ничто не мешает сделать сервисы для каждой группы исключений и заинжектить их в основной сервис.
coylOne
То есть если я пишу бандл, которы хочет как-то реагировать на исключения, я должен реализовать сервис, подходящий под ваши нужды и перед использованием попросить вас заинжектить его и дёрнуть в контроллере?
ivanuzzo
Нет, не так. В своем посте я писал не про бандл, а про обычное приложение. Само собой, для бандла или компонента нужно реализовывать листенер. На мое мировоззрение в этом плане немало повлияло общение с разработчиками Симфони (IRC и Stackoverflow), в частности здесь мне отвечали на этот вопрос.
coylOne
В итоге мы получаем смешение подходов и еще большую путаницу при поддержке: бандлы, которые мы подключаем используют одну модель, приложение (я так понимаю, состоящее из одного бандла) имеет другую. Либо так, либо мы сразу говорим о том, что приложение не подразумевает расширения.
nikita2206
Расширение можно делать очень просто — изменяя код. В случае с third-party библиотеками ты не можешь менять их код, поэтому прибегают к разным подходам, в т.ч. к ивент-модели. Но когда ты можешь менять код и этот код не будет работать вне твоего приложения, нужно делать наиболее прямо.