Как стать автором
Обновить

Какими будут контроллеры в PR2?

Время на прочтение7 мин
Количество просмотров628
Автор оригинала: Fabien Potencier
Привет! Продолжаем следить за развитием фреймворка Symfony 2. В данном топике попытаемся проследить за дискуссией: каким будет механизм контроллеров в новом релизе Symfony 2 (PR2). Под катом 6 вариантов построения интерфейса контроллера модели MVC.

Symfony 2 сейчас в стадии Preview Release (PR1). Судя по количеству сообщений в твиттере Фабьена и разработчика Doctrine 2 Jonathan Wage из Sensio Labs, работа над фреймворком идет полным ходом. Например, в последние дни появилось аж 4 новых компонента, о которых можно прочесть здесь. Детальнее про отдельные компоненты можно прочесть в моих переводах про Finder и CssSelector. Также стоит отметить большое число обсуждений касающихся Symfony 2 на google groups. Параллельно с разработкой фреймворка, интенсивно развивается вторая ветка популярной ORM Doctrine и шаблонизатор TWIG. Все это в совокупности с развитием самого PHP 5.3 создает образ такого себе растущего технологического организма, за развитием которого очень интересно следить. Чуть позже купить бутылку шампанского, поставить в бар и с нетерпением ждать финального релиза. Извините чуть отвлекся, давайте проследим за мыслями Symfony Community по поводу усовершенствования механизма контроллеров при переходе к стадии фреймворка Symfony 2 PR2 (смотрите ссылки на дискуссию в конце топика) и возможно даже принять в нем активное участие: статус обсуждения RFC — то есть вы тоже можете предложить интересную идею и тем самым улучшить фреймворк.

На самом деле это не совсем перевод, но все-таки основная часть материала взята из одного источника, — поэтому решил оформить как перевод. Текста довольно много, но структурирован и читается легко, поэтому все в одном топике.

Контроллеры


В Symfony 2 контроллером может служить любая валидная вызываемая конструкция: функция, метод класса/объекта или лямбда/замыкание. В данном топике речь пойдет о контроллере в виде метода объекта, как наиболее распространенный случай.

Для того чтобы делать свою работу (вызывать Model для передачи параметров во View), контроллер должен иметь доступ к некоторым параметрам (strings) и сервисам (objects):
  • Параметры: приходящие с объекта запроса (переменные пути (/hello/:name), GET/POST параметры, HTTP заголовки) и глобальные переменные (которые обрабатываются Dependency Injector).
  • Сервисы («глобальные» объекты) получаемые с Dependency Injector (такие как объект запроса, объект User, почтовый объект (Swift Mailer), соединение с БД и т.д.)
Контроллер — это центральная часть MVC представления, поэтому нужна особая осторожность при выборе лучшего варианта его интерфейса, и пути передачи ему параметров и доступа к сервисам. Решение по выбору лучшего варианта интерфейса должно быть принято учитывая эти вопросы (в порядке спадания важности):
  1. насколько легко/интуитивно создать новый контроллер?
  2. насколько быстра его реализация?
  3. насколько легко провести его автоматическое тестирование (Unit tests)? [в топике я этих вопросо не рассматриваю, в дополнительных ссылках это есть]
  4. насколько многословно/компактно обращаться к параметрам и сервисам?
  5. насколько реализация отвечает концепции разделения и шаблону проектирования MVC?
Собственно говоря, это и есть тот базис, с точки зрения которого будем оценивать различные варианты интерфейсов контроллеров для Symfony 2.

Вариант 1. Так устроен механизм контролеров в Symfony 2 сейчас (PR1):


Для обеспечения доступа к сервисам и параметрам, Symfony вставляет контейнер в конструктор контроллера (который потом хранится в protected свойстве):
$controller = new Controller($container);

Доступ к параметрам и сервисам

Когда выполняется действие (action), аргументы метода вставляются через совпадение их имен с переменными пути:
function showAction($slug){ ... }
Аргумент slug будет передан, если у соответствующего пути есть переменная slug: /articles/:slug.

Передача переменных пути происходит таким образом:
  1. если имя аргумента совпадает с именем переменной пути, мы используем это значение ($slug в примере, даже если оно не передано в URL и определено значение по умолчанию);
  2. если нет, и если определено значению по умолчанию для аргумента, и если аргумент необязательный, мы используем значение по умолчанию;
  3. если нет, мы выбрасываем исключение.
Доступ к параметрам происходит так:
function showAction($slug)
{
 // доступ к глобальным переменным происходит через контейнер
 // так:
 $global = $this->container->getParameter('max_per_page');
 // или так:
 $global = $this->container['max_per_page'];

 // доступ к параметрам запроса, через сервис request
 $limit = $this->container->request->getParameter('max');
 // если объект запроса существует всегда, он может быть доступен через конструктор автоматически:
 $limit = $this->request->getParameter('max');
}

Доступ к сервисам происходит так:
function indexAction() {
 // доступ довольно простой
 $this->container->getUserService()->setAttribute(...);
 
 // или более коротко через параметры контейнера
 $this->container->user->setAttribute(...);
}

Достоинства и недостатки:

Как очевидные достоинства можно выделить понятность, простоту, хорошее быстродействие и удобное тестирование контейнеров специфических видов.

Недостатки:
  1. Контроллер нагружен контейнером (разделение сущностей);
  2. Довольно открытый доступ к контейнеру, что требует аккуратности от разработчика;
  3. Доступ к параметрам и сервисам несколько многословен;
  4. Разработчики могут начать думать в так называемом sfContext контексте. То есть если они имеют доступ к контейнеру с контроллера, легко передать его в класс модели, но это не самая лучшая идея;
  5. При тестировании, разработчик будет вынужден ознакомиться с реализацией и знать к каким сервисам контроллер имеет доступ .

Вариант 2


Этот вариант отличается от предыдущего работой с параметрами/сервисами. Вместо передачи в конструктор контейнера, мы передаем только нужные параметры и сервисы:
protected $user, $request, $maxPerPage;

function __construct(User $user, Request $request, $maxPerPage)
{
 $this->user = $user;
 $this->request = $request;
 $this->maxPerPage = $maxPerPage;
}

На самом деле, не сложно заметить, что первый вариант в какой-то степени частный случай этого варианта, то есть варианты вполне совместимы.

Доступ к параметрам и сервисам

Доступ к параметрам и методам похож на предыдущий вариант, чуть более краток:
function showAction($slug)
{
 // если сервис определен в конструкторе, доступ более краткий
 $limit = $this->request->getParameter('max');
 // доступ к параметрам и сервисам прямой
 $global = $this->maxPerPage;
 $this->user->setAttribute(...);
}

Достоинства и недостатки:

Достоинства:
  1. Дает большую гибкость и полностью совместим с первым вариантом;
  2. Дает возможность контроля типов при передаче параметров/сервисов;
  3. Более ясные зависимости;
  4. Не большой накладной код;
Недостатки:
  1. Конструктор требует все сервисы и параметры (но в большинстве случаев только некоторые будут использоваться) — но успокаивает факт, что вы можете использовать метод контейнера в этих случаях;
  2. Более шаблонный код: теперь разработчику нужно хранить все передаваемые сервисы в защищенных переменных.

Вариант 3


Вместо вставки сервисов в конструктор, в этом варианте они напрямую вставляются в каждый метод контроллера:
function showAction($slug, $userService, $doctrineManagerService, $maxPerPageParameter){ ... }
Аргументом может быть переменная пути, сервис, или глобальный параметр, при этом правила передачи параметров должны быть уточнены:
  1. Если имя аргумента заканчивается на «Service», мы используем соответствующий сервис ($userService в примере);
  2. Если имя аргумента заканчивается на «Parameter», мы используем соответствующий параметр с Dependency Injector ($maxPerPageParameter в примере);
  3. Если нет, и если имя аргумента совпадает с переменной пути, мы используем его ($slug в примере);
  4. В других случаях, если аргумент не определен, выбрасываем исключение.
Если вы не хотите описывать все services/parameters в сигнатуре метода, вы можете использовать контейнер (как сделано в предыдущем варианте):
// передаем переменную пути `slug` и контейнер
function showAction($slug, Container $containerService){ ... }

// передаем Request и контейнер
function showAction(Request $requestService, Container $containerService){ ... }
Доступ к параметрам и сервисам

В данном случае доступ к параметрам и сервисам осуществляется почти напрямую:
function showAction($id, $userService, $doctrineManagerService) {
 $user->setAttribute(...);
}
Достоинства и недостатки:

Достоинства:
  • Каждое действие независимое и работет автономно;
  • Внутри метода краткий и четкий код;
  • Хорошая производительность (так как мы сами делаем разбор и анализ аргументов метода);
  • Очень гибкий вариант (вы можете использовать полный контейнер, если захотите).
Недостатки:
  • Если у нас есть большой список переменных пути и список сервисов, сигнатура может быть очень многословна — но успокаивает факт, что можно создать Request и контейнер в таком случае:
    function showAction($year, $month, $day, $slug, $userService, $doctrineManagerService) { ... }
  • Методы становятся похожими на функции (так как их ничего не объединяет);
  • Даже если мы передаем сервисы как аргументы метода, некоторые из них могут быть из них необязательными для метода. В таком случае, мы получаем еще больше лишнего кода чем получать доступ к сервисам с контейнера по требованию.

Вариант 4


Этот вариант — смесь вариантов 2 и 3. Сервисы и параметры могут передаваться как в конструктор, так и в методы actions:
protected $user;

function __construct(User $user) {
 $this->user = $user;
}

function showAction($slug, $mailerService, $maxPerPageParameter){ ... }
В этом варианте уже нет проблемы, что методы стают похожи на функции из PHP4. Вы можете создать глобальные сервисы для класса, и локальные для каждого метода action.

Это приближение на 100% совместимо с первым вариантом, когда все везде опционально. Вы можете использовать параметры и в конструкторе и в действиях, или использовать контейнер, созданный по умолчанию (или сделать зависимым от контекста).

Это также позволяет эмулировать действия из ветки Symfony 1.x:
function showAction(Request $requestService){ ... }

Так как этот вариант наиболее гибкий, документация должна содержать лучшие примеры использования (best practices).

Вариант 5


Вариант почти полная копия варианта 4, за исключением того, что переменные пути не могут быть включены в методы actions. Это позволяет убрать лишние суффиксы (Parameter и Service). А доступ к переменным пути происходит через обращение к объекту запроса:
protected $user;

function __construct(User $user) {
 $this->user = $user;
}

function showAction(Request $request, $mailer, $maxPerPage) {
 $id = $request->getPathParameter('id');
 // ...
}
Еще хорошей договоренностью может быть включение объекта Request первым аргументом (для последовательности подхода).

Вариант 6


Еще одним альтернативным вариантом может быть использование аннотаций для включений параметров. Это пока что особо не обсуждается, потому как Symfony 2 пока что не использует аннотаций.

Достоинства и недостатки:

Достоинства:
  • Некоторые сторонние библиотеки начали использовать аннотации (Doctrine 2, но пока что опционально).
Недостатки:
  • Дополнительный код;
  • PHP разработчики редко используют аннотации;
  • Аннотации это не «родная» конструкция языка PHP.
Дополнительную информацио можно получить из дополнительный ссылок внизу. В комментариях интересно почитать какой вариант вам больше всего по душе, или может кто-то предложит что-то новенькое. Мне лично нравятся варианты 1 и 5.

Полезные ссылки: RFC: Controllers in Symfony 2 и обсуждение на google groups: часть 1, часть 2, часть 3.
Теги:
Хабы:
Всего голосов 25: ↑21 и ↓4+17
Комментарии10

Публикации