Спасибо! И позвольте, я объяснюсь.
В первую очередь, спасибо всем, кто прочитал предыдущие части. Много интересных комментариев написали, и я понял, что должен объяснить, зачем я, собственно, всё это пишу? Зачем мне вообще нужно отделять контроллеры от фреймворка? Скорее всего, об этом не придется думать, потому что
Шансы, что контроллеры придется переносить на другой фреймворк, близки к нулю. (Рафаэль Домс)
Отлично сказано, и я согласен с этим. Вам ничего этого делать не надо, потому что фреймворки никто обычно не меняет. Хотя возможно, что кто-то захочет использовать ваш код в приложении на другом фреймворке. Но:
Если у вас «тонкий» контроллер, в котором все зависит от сервисного слоя, то переписать его на другой фреймворк будет элементарно (Рафаэль Домс)
Действительно, в контроллерах почти не должно быть кода, только вызовы сервисов, получение данных от них и возвращение какого-либо ответа. Их может даже и переписывать на другой фреймворк не понадобится. И если вы не собираетесь распространять свой код, то опять же смысла в отделении контроллеров от фреймворка особо нет.
Но когда вы это сделаете, то станете чуть счастливее. Не только ваши контроллеры станут более независимы, независимы станете и вы как разработчик. Больше не надо полагаться на функции-хелперы, аннотации или магию: вы всё можете сделать самостоятельно. Мне очень нравится следующая цитата, потому что она точно описывает мои впечатления:
Теперь написание нового контроллера приносит больше пользы, чем раньше, и из-за этого я больше думаю о самой сути кода (Кевин Бонд)
И вот почему я пишу эту серию постов. Они дают разработчикам лучшее понимание того, что на самом деле происходит внутри фреймворка, и каким образом он им помогает. Разработчики становятся более уверенными, более независимыми. Они перестают быть «симфони-разработчиками», они становятся более умелыми разработчиками в общем смысле. Этот цикл статей — отличное упражнение по части выявления зависимостей. Соглашения в контроллерах, кстати, тоже являются зависимостями: пусть они и скрыты от наших глаз, зато у нас есть возможность потренировать наши «радары связанностей».
Шаблоны Twig
Давайте сделаем несколько последних шагов к независимым от фреймворка контроллерам. В предыдущей части мы убрали все аннотации и использовали вместо них конфигурационные файлы. Еще мы внедрили несколько зависимостей, которые позволили нам получить данные из БД и отрендерить шаблон. Но имя шаблона всё еще содержало в себе имя бандла в качестве пространства имен:
class ClientController
{
...
public function detailsAction(Client $client)
{
return new Response(
$this->templating->render(
'@MatthiasClientBundle/Resources/views/Client/Details.html.twig',
...
)
);
}
}
Раз уж мы решили заставить этот контроллер работать в приложениях, где нет никаких бандлов, придется выбрать более общее имя, например, MatthiasClient
. Теперь надо зарегистрировать это имя как простраство имен для шаблонов Twig. Для этого нужно вызвать метод Twig_Loader_Filesystem::addPath('/путь/до/шаблонов', 'ПространствоИменДляШаблонов')
. Что хорошо, так это то, что в приложении на Symfony 2 это можно сделать, указав параметр конфигурации twig.paths
:
# в config.yml:
twig:
paths:
"%kernel.root_dir%/../src/Matthias/Client/View": "MatthiasClient"
Когда вы добавите этот путь в config.yml
, код в контроллере можно будет поменять таким образом:
return new Response(
$this->templating->render(
'@MatthiasClient/Client/Details.html.twig',
...
)
);
Еще лучше: предварительное подключение конфигурации
У нас получилось не очень элегантное решение, потому что надо править конфиг при первом подключении MatthiasClientBundle
в проект. Есть вариант получше: вы можете программно добавить значения в конфигурацию из класса расширения вашего бандла. Нужно, чтобы расширение реализовало PrependExtensionInterface
и предоставляло массив значений, которые должны быть добавлены раньше основных значений в config.yml
:
use SymfonyComponentDependencyInjectionExtensionPrependExtensionInterface;
class MatthiasClientExtension extends Extension implements PrependExtensionInterface
{
...
public function prepend(ContainerBuilder $container)
{
$container->prependExtensionConfig(
'twig',
array(
'paths' => array(
'%kernel.root_dir%/../src/Matthias/Client/View' => 'MatthiasClient'
)
)
);
}
}
Теперь можно убрать лишнюю строчку из config.yml
, потому что теперь это значение добавляется автоматически.
Избавляемся от зависимости от HttpFoundation
Предыдущая статья вызвала сомнения у некоторых читателей:
А теперь контроллер явно зависит от Doctrine и HttpFoundation! В версии с аннотациями такого не было! (Джерри Вандермэйзен)
На мой взгляд, зависеть от Doctrine не так уж плохо. Это просто приглянувшаяся мне библиотека (как и Twig). Отвязывание от ORM/ODM — тоже интересная штука, и вполне реализуемая, но она все же за пределами данной серии постов.
Но Джерри прав: убирая аннотации, мы привносим зависимость от классов Request
и Response
из компонента HttpFoundation. Правда, в отличие от него я считаю, что это уже хороший шаг в сторону отвязки от фреймворка, поскольку все больше других фреймворков помимо Symfony поддерживают HttpFoundation как слой абстракции для HTTP.
Тем не менее, мы можем сделать еще один шаг и перестать зависеть от HttpFoundation. Нам нужно избавиться от объектов Request
и Response
. Изменим контроллер вот таким образом:
public function detailsAction($id)
{
$client = $this->clientRepository->find($id);
if (!($client instanceof Client)) {
return array(null, 404);
}
return array(
$this->templating->render(
'@MatthiasClient/Client/Details.html.twig',
array('client' => $client)
),
200
);
}
Ни один класс из HttpFoundation теперь тут не упоминается! Теперь можно добавить контроллер-обертку, который бы связывал наш контроллер с этим компонентом:
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentHttpKernelExceptionNotFoundHttpException;
class SymfonyClientController
{
private $clientController;
public function __construct(ClientController $clientController)
{
$this->clientController = $clientController;
}
public function detailsAction(Request $request)
{
$result = $this->clientController->detailsAction($request->attributes->get('id'));
list($content, $status) = $result;
if ($status === 404) {
throw new NotFoundHttpException($content);
}
return new Response($content, $status);
}
}
Да, код становится несколько более сложным для понимания. Так что я не рекомендую вам так делать. Но все же хотелось бы показать вам, что это действительно возможно.
Избавимся от «action»-методов
Последняя тема для обсуждения: экшны контроллеров. Стандарт де-факто таков, что связанные действия упаковываются в один класс контроллера. Например, все действия с клиентами должны находиться в классе ClientController
. То есть, в нем есть методы вроде newAction
, editAction
и т.д. И если использовать внедрение зависимостей в конструкторе, то вполне возможно, что некоторые из них даже не будут использованы в некоторых экшнах.
Решение этой проблемы на самом деле очень простое, и заметно упрощает восприятие кода контроллера. А еще контроллеры становится проще искать. Просто экшены надо группировать несколько иначе. По большому счету, каждый из экшенов выделяется в отдельный класс, а директория, в которой оказывается такой экшн-контроллер, становится связующим звеном, вот пример: ControllerClientNew
, ControllerClientEdit
, и так далее. У каждого из этих классов будет один публичный метод, вызываемый при исполнении контроллера. Назовем его __invoke
:
namespace MatthiasClientControllerClient;
class Details
{
public function __construct(...)
{
// здесь у нас внедряются только зависимости данного конкретного экшена
}
public function __invoke(Client $client)
{
...
}
Про эту технику я впервые узнал в книге Пола Джонса Modernizing Legacy Applications in PHP. Я применял ее уже несколько раз, и надо сказать, что в некоторых случаях работать с контроллерами стало намного приятнее!
Заключение
Ну что ж, я подал вам несколько идей на следующий раз, когда вы будете создавать очередной контроллер. Также я надеюсь, что вы теперь лучше познакомились с самим фреймворком. А еще я надеюсь, что ваш разум освободился :)
Автор: kix