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

Разработка → Как делать независимые от фреймворка контроллеры? перевод tutorial

Обычно считается, что контроллеры — наиболее связанные классы в приложении. Как правило, на основании данных запроса они получают или сохраняют данные в базу данных, затем превращают данные или результат сохранения в HTML, который выступает в качестве ответа клиенту, который произвел запрос.

Получается, что контроллеры — повсюду, они соединяют те части приложения, которые обычно достаточно независимы друг от друга. Это сильно повышает связанность контроллеров: среди их зависимостей есть менеджер сущностей Doctrine, шаблонизатор Twig, базовый контроллер из FrameworkBundle, и прочее.

В этой записи я покажу, что этот уровень связанности совершенно не нужен. Я покажу вам, как значительно понизить связанность, предприняв всего несколько простых шагов. В результате мы получим контроллер, который можно будет повторно использовать в разных типах приложений, например, на базе Silex или даже Drupal.

Часть I: не используйте стандартные контроллеры



Излишняя связанность: наследование от базового контроллера

В основном, классы контроллеров, с которыми я сталкиваюсь, наследуют от класса Controller из FrameworkBundle:

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class MyController extends Controller
{
    ...
}



Базовый класс контроллера добавляет несколько полезных сокращений, вроде createNotFoundException() и redirect(). А еще он автоматически делает ваш контроллер наследником ContainerAware («осведомленным» о контейнере), в результате чего в контроллер будет автоматически инъектироваться контейнер внедрения зависимостей (Dependency Injection Container, DIC). Из контейнера можно будет получить любой нужный вам сервис.

Не используйте вспомогательные методы!

Всё это, конечно, кажется очень удобным, но если чуть меньше лениться, можно заметно понизить связанность. Нет ничего страшного в том, чтобы выкинуть эти вспомогательные методы: тела большинства из них все равно состоят всего из одной строки (посмотрите в базовый контроллер, чтобы понять, что происходит там на самом деле!). Вы запросто можете заменить вызовы этих методов кодом из них:

class MyController extends Controller
{
    public function someAction()
    {
        // было:
        throw $this->createNotFoundException($message);

        // стало:
        throw new NotFoundHttpException($message);
    }
}


Используйте внедрение зависимостей

Если вы больше не используете вспомогательные методы из родительского класса контроллера, то можно сделать еще один шаг для избавления от этого ненужного на самом деле наследования. Вместо того, чтобы получать зависимости из контейнера, инъектируйте их вручную в конструкторе:

class MyController extends Controller
{
    public function someAction()
    {
        $this->get('doctrine')->getManager(...)->persist(...);
    }
}

// превратится в:

use Doctrine\Common\Persistence\ManagerRegistry;

class MyController extends Controller
{
    public function __construct(ManagerRegistry $doctrine)
    {
        $this->doctrine = $doctrine;
    }

    public function someAction()
    {
        $this->doctrine->getManager(...)->persist(...);
    }
}


Сделайте ваш контроллер сервисом

Вам также надо убедиться в том, что новый контроллер-сервис собирается не просто через вызов оператора new, как обычно поступает ControllerResolver, а все зависимости вашего контроллера корректно инъектируются. Для этого вам нужно объявить сервис:

services:
    my_controller:
        class: MyController
        arguments:
            - @doctrine

И настройки роутинга тоже надо поправить. Если вы пользуетесь аннотациями:

/**
 * @Route(service="my_controller")
 */
class MyController extends Controller
{
    ...
}

И если ваш роутинг сохранен в YML-файле конфигурации:

my_action:
    path: /some/path
    defaults:
        _controller: my_controller:someAction


Наконец, больше нет необходимости наследоваться от стандартного симфонийского контроллера, нам также не надо, чтобы контроллер знал что-либо о контейнере DI, поскольку все его зависимости инъектируются в аргументах конструктора. И мы больше не зависим от вспомогательных методов базового класса, что значит, что теперь мы можем окончательно убрать extends Controller вместе с use-выражением из объявления класса нашего контроллера:

class MyController
{
}


Вот таким образом мы и получаем много бонусных очков за несвязанный код!

В следующем посте мы поговорим про аннотации и про то, как наш код станет еще менее связанным, если мы от них избавимся.
Перевод: Matthias Noback
kix @kix
карма
33,7
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

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

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

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

  • +1
    А зачем собственно нам нужно делать такие изменения? Есть ли какой нибудь пример из реальной жизни где такой вариант будет реально полезен? У вас действительно есть контроллер который используется в 2-х фреймворках одновременно?
    • +2
      В третьей части автор поясняет свою точку зрения на этот вопрос, скоро я эту часть допереведу, сможете там прочитать. А вообще, обобщая позицию автора, суть тут в том, что разработчик сам по себе начинает лучше замечать зависимости, и ему больше не кажутся «магией» аннотации и конвертеры параметров. Ну и плюс, контроллер-как-сервис очень проще для восприятия, чем ContainerAware-контроллер — все зависимости сразу видно; сразу, например, понятно, нужен ли контроллеру доступ к БД, и если нужен, то к каким репозиториям, и так далее.
      • +1
        Вообще в свое время ContainerAware контроллеры приходилось делать, это только с Symfony 2.3 DiC поддерживает ленивую инициализацию зависимостей (оборачивает их в прокси-объекты). Раньше же при инициализации контроллера инициировались и все все все сервисы, которые мы инджектили. Что бы этого избежать, приходилось инджектить контейнер целиком.
    • 0
      У меня был случай, когда в рамках рефакторинга проекта и его последующего переноса на другой фреймворк, пришлось вносить отдельный слой отделяющий компоненты фреймворка от приложения. В целом обычно это хорошая практика. Например отвязать контроллеры от Doctrine (инджектить отдельно репозитории и завязывать их на свои интерфейсы, не используя снаружи функционал Doctrine), что позволит быстро и безболезненно сменить хранилище данных. В последней части этой серии статей автор даже сам говорит, что отвязывать контроллеры и от HttpKernel это перебор, хотя раз уж начали то так будет правильно.
    • 0
      На самом деле, основные плюсы кроются не в том, что контроллер можно реюзать, а скорее, наоборот, тонкий клиент и связывание дает кунштюки:

      — Можно заменить сервис, реализующий ту или иную плюшку (например, подменив EntityManager доктрины часть самых тяжелых методов перепишется на максимально быстрый нейтив)
      — С инъекциями проще потом поверх нужных методов подцепить что-то еще. Например, логгер.
      — Замена некоторых зависимостей полностью — не такой уж частый, но важный кейс в разработке хоть сколько-то дольше месяца,
      — Наконец, позволяет тестировать сервисы модульно, в своих контекстах, так что контроллер, который тянет за собой все окружение, тестировать или не надо, или просто, или достаточно пары функциональных стандартных тестов.
      • 0
        Спасибо все ответы весьма неплохие. Возможно тогда стоит немного изменить название темы например: «Как уменьшить связность контроллера с фреймворком». Все ответы говорят о том что основная цель это не использование нескольких фреймворков с одним кодом, а именно уменьшение связности классов контроллеров. И в таком варианте это действительно имеет смысл. Правда я думаю что в таком случае подходить стоит без особого фанатизма например явная инъекция зависимостей это однозначно хорошо а вот отказываться от сахара аннотаций думаю нет большого смысла.
        • 0
          Ну, мопед не мой, а по футеру так и текст переводной.

          Про аннотации — мне самому они очень нравятся, особенно в роли описания кеша/темплейтов/и т.п., но порой действительно вынос роутингов в дерево yaml дает большую гибкость, особенно в больших проектах. По крайней мере, ищется нужный даже в большой команде легко, роуты видно в одном месте (и на одном экране, что важно),

          и самое главное — для Symfony — проще делать иерархию роутинга (чтобы все роутинги /api/ были описаны в другом файле, при этом префикс задается в одном месте).

          Хотя, когда писали самодокументируемый REST-сервис с помощью NelmioApiDocBundle, многое было на аннотациях и было очень вкусно и красиво, даже описание парамтетров, передаваемых в query, и принимаемых в POST сущностей.
          • 0
            роуты видно в одном месте (и на одном экране, что важно)


            Вы все еще в ручную глазами в конфигах ищите соответствия роутов экшенам?

            php app/console router:debug в связке с grep будте в любом случаее удобней.

            В PhpStorm можно просто по названию роута сразу в нужный экшен попасть:

            image
            • +1
              Ну, это про поиск и навигацию по аннотированным @Route экшенам. А если вы вдруг, например, захотите во всех экшенах суффикс _list заменить на _index, например? Или, например, для всех рутов с суффиксом _new ограничить методы только POST'ом?
              Имхо, куда удобнее будет сделать изменения в одном или нескольких routing.yml и получить красивый и аккуратный коммит, по changeset'у которого явно видно, что он касается только роутинга, нежели получить изменения в большинстве, а то и во всех контроллерах.
              Кстати, это еще один аргумент за конфиги и против аннотаций.
              • 0
                Согласен, но вы описали редкую задачу. Для меня удобство видеть с кодом экшена и его роут, поддерживаемые http-методы, шаблоны и т. д. перевешивает недостатки.

                С хранением роутов в одном файл сталкивался в rails. Для больших проектов это выглядит довольно дико.
                • 0
                  kix привел более важный довод нежели удобство поиска или редактирования — история изменения проекта выглядит более лаконично. Посмотрите хотя бы историю изменений приведенного вами конфига Redmine, все локонично, видно все изменения косательно раутинга…
                • +1
                  Но ведь вам никто не мешает разносить роутинг по бандлам! Более того, это даже рекомендуемый путь для Symfony2-приложений. При генерации бандла конфигурация роутинга для него не конкатится к app/config/routing.yml, а инклудится в нем:

                  ck_finder:
                      resource: "@JonlilCKFinderBundle/Resources/config/routing/routing.yml"
                      prefix: /cms/ckfinder
                  

                  И что еще важнее, вы вполне можете объявить несколько конфигураций роутинга в рамках одного бандла, либо точно так же инклудить другие конфиги при помощи resource.
                  Получается, что в пути src/My/AcmeBundle/resources/config могут быть, например, сразу routing_front.yml и routing_admin.yml, импортируемые в app/config/routing:

                  acme_front:
                    resource: '@MyAcmeBundle/Resources/config/routing_front.yml'
                    prefix: /
                  
                  acme_admin:
                    resource: '@MyAcmeBundle/Resources/config/routing_admin.yml'
                    prefix: /admin/
                  
        • +1
          Все что описывается в статье не имеет никакого отношения к небольшим проектам и быстрой разработке. То есть обычно на таких проектах и так не пользуются аннотациями или хотя бы стараются не использовать их. Если уж выбран один формат хранения конфигов — стараются придерживаться именно его. В зависимости от проекта можно перенимать лишь часть практик.
          • 0
            А тут как в жизни — когда говорим об энтерпрайзе, задумываемся об архитектуре, масштабируемости и всем таком. Когда говорим о небольших проектах, речь идет только о том, как построить сам процесс разработки максимально быстро. Остальное имеет стремящееся к нулю значение.
    • 0

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