Пользователь
0,0
рейтинг
26 июня 2014 в 20:07

Разработка → Избавьтесь от аннотаций в своих контроллерах! перевод tutorial

В предыдущей части этой серии мы понизили связанность симфонийского контроллера и фреймворка, удалив зависимость от базового класса контроллера из FrameworkBundle. А в этой части мы избавимся от некоторых неявных зависимостей, которые появляются из-за аннотаций.

Теперь давайте посмотрим на аннотации. Первоначально они были подключены для ускорения разработки (исчезает потребность редактировать конфигурационный файл, просто решайте проблемы прямо на месте!):

namespace Matthias\ClientBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;

/**
 * @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 Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\HttpFoundation\Response;

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: Matthias\ClientBundle\Controller\ClientController
        arguments:
            - @templating


После этих изменений можно смело убирать аннотацию @Template

Самостоятельно получайте требуемые данные

И еще один шаг, чтобы понизить связанность нашего контроллера. Мы по-прежнему зависим от SensioFrameworkExtraBundle, он автоматически превращает параметр id из запроса в реальные сущности. Это должно быть несложно исправить, мы ведь можем просто получать сущность сами, используя репозиторий сущностей напрямую:
...
use Doctrine\Common\Persistence\ObjectRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

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: Matthias\ClientBundle\Controller\ClientController
        arguments:
            - @templating
            - @client_repository

    client_repository:
        class: Doctrine\Common\Persistence\ObjectRepository
        factory_service: doctrine
        factory_method: getRepository
        public: false
        arguments:
            - "Matthias\ClientBundle\Entity\Client"

Наконец, мы избавились от аннотаций, значит, наш контроллер вполне можно использовать вне приложения Symfony 2 (то есть такого, которое не зависит ни от FrameworkBundle, ни от SensioFrameworkExtraBundle). Все зависимости явные, то есть чтобы контроллер заработал, вам нужны:

— компонент HttpFoundation (для классов Response и NotFoundHttpException)
— шаблонизатор (для EngineInterface)
— какая-либо реализация репозиториев Doctrine (Doctrine ORM, Doctrine MongoDB ODM, ...)
— Twig для шаблонизации

Остался только один слабый момент: имена наших шаблонов все еще основаны на соглашениях фреймворка (т.е. используют имя бандла в качестве пространства имен, напр. @MatthiasClientBundle/...). Это неявная зависимость от фреймворка, поскольку эти пространства имен регистрируются в загрузчике из файловой системы Twig. В следующем посте мы разберемся и с этой проблемой тоже.
Перевод: Matthias Noback
kix @kix
карма
33,7
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (21)

  • +12
    Я вот прочитал первую статью, вторую…
    В голове крутиться одна мысль — ну, отвязал автор контроллеры от фреймворка. В экшнах контроллеры всё равно хавают тот-же HttpFoundation\Request и пр.
    Какая от этого реальная польза, кроме как «разобраться, что это за магия»?
    • +2
      Во-первых, на HttpFoundation уже не только Symfony живет, вот, кажется, Laravel тоже на этот компонент переехал. Так что мы уже не от фреймворка будем зависеть, а от компонента фреймворка.
      Во-вторых, в третьей статье серии автор рассматривает отвязку и от HttpFoundation тоже, при помощи выделения дополнительной прослойки.
      • +1
        А зачем? Какая вероятность, что именно этот контролер где-то еще будет юзаться? А протестить его и с аннотациями проблем нет.
  • +16
    Абсолюно идиотский подход. С таким подходом можно взять чистый php — никаких зависимостей, полная свобода. А лучше взять плюсы. И скорость, и прибиндить к php/js/ruby/python можно.
    Вместо 2-3 аннотаций нужно написать конфигов строк 30 и лишнего кода строк 10, ну шикарно че. Вдруг завтра с симфони на Yii перейдем!
    • +3
      Ну, насчет чистого PHP вы конечно загнули. Как минимум придется реализовать свой DIC, persistence layer и шаблонизатор.
      А про «идиотский подход» — ну так, знаете, автор и не утверждает, что это единственно верный путь разработки, он просто показывает, что типичный уровень связанности в Sf2-приложении совершенно необязателен; хотите — отказываетесь от аннотаций, хотите — от ContainerAware, но никто вас это все сразу делать не обязывает.
  • +2
    Может быть, переводчик оставит свой комментарий, с какой целью он это перевёл?
    Я просто никак не пойму сакрального смысла — зачем начинать использовать какой-то инструмент, чтобы усиленно «отвязываться» от тех преимуществ, которые он даёт
    • 0
      Бывает так, что проект растёт и развивается и текущий инструмент перестаёт удовлетворять нужды. Поэтому переносят его на что-то более гибкое или современное. Такие вещи начинаешь ценить именно при переносе проекта, конечно если остаётся основной стек технологий при этом.
      Но как показала практика, в нашем случае, такие вот привязки к фреймворку отнимают немного врмени, чтобы их перенести. Намного больше занимают формы или шаблоны, при смене технологии или фреймворка…
      • +5
        Ну норм, отвязали слой бизнес-логики от контроллеров и переносите когда угодно и куда угодно. А формы, обработку request, csrf, шаблоны, роутинг, контроллеры — оставляйте фреймворку, он для этого создан.
      • +1
        Ну и я живо себе представляю перенос таких контроллеров на какой-нибудь Silex или еще хуже ZendFramework, в котором, скажем, свои формы, которые ни разу не совместимы с SymfonyForm
    • 0
      Позвольте, так ведь автор и не отказывается от каких-либо преимуществ, он наоборот выделяет преимущества неочевидные.
      Так, например, очень многие разработчики наследуют свои контроллеры от базового класса, а значит, и от ContainerAware в то время, когда практика выделения контроллеров в сервисы может заметно упростить понимание того, каковы зависимости контроллера.
      Прелесть тут в том, что гибкость Symfony дает несколько путей реализации по сути одного и того же функционала, и в зависимости от случая, вы сможете использовать любой из них. Или аннотации, или конфиги; или ContainerAware-контроллеры, или контроллеры как сервисы — выбор всегда за вами, и зависит от вашей задачи.
  • +9
    Мне кажется автор этих статей как-то неправильно понял framework-agnostic подход при разработке веб-приложений. Мне кажется речь о том, что слой бизнес-логики не должен зависеть от фреймворка, а не контроллеры. Должны быть сервисы там всякие, уровень домена — они должны быть framework-agnostic, чтобы можно было взять, скажем, и перетащить приложение с symfony на silex+angular. Но тащить готовые контроллеры между фреймворками?? Извините, это какая-то наркомания.
  • +8
    Фух. Я сначала было подумал, что с утра чего то не догоняю, но судя по комментариям, все норм)
  • +1
    На мой взгляд, аннотации как раз увеличивают читаемость и переносимость кода. Вот избавились от аннотаций, понаписали лишнего кода, который будет как раз несовместим с другими фреймворками.
    И вообще, контроллеры — это такой тонкий связной слой, в котором не должно быть бизнес-логики (хорошо написано в этой статье: habrahabr.ru/post/175465/), а потому не нужно их «отвязывать», а необходимо именно грамотно раскидывать функциональность по слоям, и тогда решение задачи «сменить фреймворк» сведется в идеале на переименование классов и минимально-механическая адаптация к особенностям нового фреймворка с выкидыванием атавизмов от старого.
    • 0
      Добавлю, что магии стало больше в разы. Вместо того что бы увидеть достаточно понятный код в виде аннотаций, мы видим какие то магические инжекты с которыми еще нужно постараться разобраться. В случае необходимости исправить код в таком варианте нужно править как минимум в двух местах. Кроме этого я не понимаю выигрыша от таких действий.

    • 0
      Я могу вам на это сказать, что выделенные в отдельные файлы конфигурации тоже заметно упрощают восприятие кода в целом. Да, удобно бывает с @Route-аннотациями прямо в контроллере, но точно так же удобно и хранить те же роуты в отдельном конфигурационном файле. Вопрос, во-первых, привычки, во-вторых — проекта.
  • +1
    Эти статьи можно использовать для введения в Symfony новичков. Начинаешь им показывать как использовать симфони, но исключив на начальном этапе некоторые вещи:
    • не использования sensio/framework-extra-bundle
    • контроллеры наследовать не от FrameworkBundle\Controller, а сразу от ContainerAware
    • вместо твига — пхп шаблоны


    А потом уже начать вводить аннотации из FrameworkExtraBundle, начинаешь использовать обертки из FrameworkBundle\Controller и показываешь преимущества использования твиг-шаблонов. В похожем ключе рассказывал у себя в конторе про симфони другим разработчикам достаточно успешно, даже черновики докладов остались.
    • 0
      Да-да-да, полностью с вами согласен. У Бенджамина Эберлея про это статейка была хорошая. Может, и её стоит перевести?
      • +1
        больше статей = больше коммьюнити = хорошо
    • 0
      Пожалуйста, не учите новичков использовать обычные PHP-шаблоны =( Потом XSS на каждом шагу бывает от такого.
      • 0
        Ну, согласитесь, что в учебных целях вполне можно и про PHP-шаблоны рассказать, а уже потом пояснить, почему оно плохо, и почему нужно учить еще и Jinja-синтаксис из Твига.
      • +2
        В этом и соль, показать на реальном примере разницу. В пхп-шаблоне в симфони для простого вывода переменной вам надо будет в ручную вызвать экранирование:

        <?php echo $view->escape($name); ?>

        В твиге в симфони все наоборот: экранирование идет по дефолту, и вам надо использовать фильтр raw чтобы отменить это автоматическое экранирование.

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