Избавьтесь от аннотаций в своих контроллерах!

в 16:07, , рубрики: dependency injection, php, symfony, symfony2

В предыдущей части этой серии мы понизили связанность симфонийского контроллера и фреймворка, удалив зависимость от базового класса контроллера из 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js