В предыдущей части этой серии мы понизили связанность симфонийского контроллера и фреймворка, удалив зависимость от базового класса контроллера из FrameworkBundle
. А в этой части мы избавимся от некоторых неявных зависимостей, которые появляются из-за аннотаций.
Теперь давайте посмотрим на аннотации. Первоначально они были подключены для ускорения разработки (исчезает потребность редактировать конфигурационный файл, просто решайте проблемы прямо на месте!):
namespace MatthiasClientBundleController;
use SensioBundleFrameworkExtraBundleConfigurationRoute;
use SensioBundleFrameworkExtraBundleConfigurationMethod;
use SensioBundleFrameworkExtraBundleConfigurationTemplate;
use SensioBundleFrameworkExtraBundleConfigurationParamConverter;
/**
* @Route("/client")
*/
class ClientController
{
/**
* @Route('/{id}')
* @Method("GET")
* @ParamConverter(name="client")
* @Template
*/
public function detailsAction(Client $client)
{
return array(
'client' => $client
);
}
}
Когда вы подключите эти аннотации, detailsAction
будет выполнена, когда URL совпадет с шаблоном /client/{id}
. Конвертер параметров получит из БД сущность клиента на основании параметра id
, который будет извлечен из УРЛа роутером. И аннотация @Template
укажет на то, что возвращаемый массив является набором переменных для шаблона Resources/views/Client/Details.html.twig
.
Отлично! И всего в несколько строк кода. Но все эти автомагические вещи незаметно связывают наш контроллер с фреймворком. Пусть явных зависимостей тут и нет, есть несколько зависимостей неявных. Контроллер будет работать только при подключенном SensioFrameworkExtraBundle
в силу следующих причин:
1. Он (SensioFrameworkExtraBundle) генерирует роутинг на основе аннотаций
2. Он заботится о превращении возвращаемого массива в корректный объект Response
3. Он угадывает, какой шаблон нужно применить
4. Он превращает параметр id
из запроса в реальную модель
Казалось бы, не так это все и страшно, но SensioFrameworkExtraBundle
— бандл, а значит, что работает он только в контексте приложения Symfony 2. Но мы же не хотим быть привязанными к конкретному фреймворку (в этом, собственно, суть этой серии постов), так что от этой зависимости нам надо избавиться.
Вместо аннотаций мы будем использовать обычные конфигурационные файлы и PHP-код.
Используем конфигурацию роутера
В первую очередь убедимся, что наши роуты подключаются в Resources/config/routing.xml
:
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="client.details" path="/client/{id}" methods="GET">
<default key="_controller">client_controller:detailsAction</default>
</route>
</routes>
Можете использовать YAML, но я последнее время что-то подсел на XML.
Убедитесь, что сервис client_controller
на самом деле существует, и не забудьте импортировать новый routing.xml
в настройках приложения, в файле app/config/routing.yml
:
MatthiasClientBundle:
resource: @MatthiasClientBundle/Resources/config/routing.xml
Теперь можно убрать аннотации @Route
и @Method
из класса контроллера!
Самостоятельно создавайте объект Response
Теперь, вместо того, чтобы надеяться на аннотацию @Template
, вы вполне можете рендерить шаблон самостоятельно, и создавать объект Response, содержащий результат рендеринга. Вам просто надо инъектировать шаблонизатор в ваш контроллер, и указать имя шаблона, который вы хотите отрендерить:
use SensioBundleFrameworkExtraBundleConfigurationTemplate;
use SensioBundleFrameworkExtraBundleConfigurationParamConverter;
use SymfonyComponentTemplatingEngineInterface;
use SymfonyComponentHttpFoundationResponse;
class ClientController
{
private $templating;
public function __construct(EngineInterface $templating)
{
$this->templating = $templating;
}
/**
* @ParamConverter(name="client")
*/
public function detailsAction(Client $client)
{
return new Response(
$this->templating->render(
'@MatthiasClientBundle/Resources/views/Client/Details.html.twig',
array(
'client' => $client
)
)
);
}
}
В объявлении сервиса для этого контроллера также надо указать сервис @templating
как аргумент конструктора:
services:
client_controller:
class: MatthiasClientBundleControllerClientController
arguments:
- @templating
После этих изменений можно смело убирать аннотацию @Template
Самостоятельно получайте требуемые данные
И еще один шаг, чтобы понизить связанность нашего контроллера. Мы по-прежнему зависим от SensioFrameworkExtraBundle
, он автоматически превращает параметр id
из запроса в реальные сущности. Это должно быть несложно исправить, мы ведь можем просто получать сущность сами, используя репозиторий сущностей напрямую:
...
use DoctrineCommonPersistenceObjectRepository;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpKernelExceptionNotFoundHttpException;
class ClientController
{
private $clientRepository;
...
public function __construct(ObjectRepository $clientRepository, ...)
{
$this->clientRepository = $clientRepository;
...
}
public function detailsAction(Request $request)
{
$client = $this->clientRepository->find($request->attributes->get('id'));
if (!($client instanceof Client) {
throw new NotFoundHttpException();
}
return new Response(...);
}
}
Объявление сервиса должно возвращать нужный нам репозиторий. Мы добьемся этого вот таким способом:
services:
client_controller:
class: MatthiasClientBundleControllerClientController
arguments:
- @templating
- @client_repository
client_repository:
class: DoctrineCommonPersistenceObjectRepository
factory_service: doctrine
factory_method: getRepository
public: false
arguments:
- "MatthiasClientBundleEntityClient"
Наконец, мы избавились от аннотаций, значит, наш контроллер вполне можно использовать вне приложения Symfony 2 (то есть такого, которое не зависит ни от FrameworkBundle
, ни от SensioFrameworkExtraBundle
). Все зависимости явные, то есть чтобы контроллер заработал, вам нужны:
— компонент HttpFoundation (для классов Response
и NotFoundHttpException
)
— шаблонизатор (для EngineInterface
)
— какая-либо реализация репозиториев Doctrine (Doctrine ORM, Doctrine MongoDB ODM, ...)
— Twig для шаблонизации
Остался только один слабый момент: имена наших шаблонов все еще основаны на соглашениях фреймворка (т.е. используют имя бандла в качестве пространства имен, напр. @MatthiasClientBundle/...
). Это неявная зависимость от фреймворка, поскольку эти пространства имен регистрируются в загрузчике из файловой системы Twig. В следующем посте мы разберемся и с этой проблемой тоже.
Автор: kix