Привет! Продолжаем следить за развитием фреймворка 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):
Для обеспечения доступа к сервисам и параметрам, Symfony вставляет контейнер в конструктор контроллера (который потом хранится в protected свойстве):
Когда выполняется действие (action), аргументы метода вставляются через совпадение их имен с переменными пути:
Передача переменных пути происходит таким образом:
Доступ к сервисам происходит так:
Как очевидные достоинства можно выделить понятность, простоту, хорошее быстродействие и удобное тестирование контейнеров специфических видов.
Недостатки:
Этот вариант отличается от предыдущего работой с параметрами/сервисами. Вместо передачи в конструктор контейнера, мы передаем только нужные параметры и сервисы:
На самом деле, не сложно заметить, что первый вариант в какой-то степени частный случай этого варианта, то есть варианты вполне совместимы.
Доступ к параметрам и методам похож на предыдущий вариант, чуть более краток:
Достоинства:
Вместо вставки сервисов в конструктор, в этом варианте они напрямую вставляются в каждый метод контроллера:
В данном случае доступ к параметрам и сервисам осуществляется почти напрямую:
Достоинства:
Этот вариант — смесь вариантов 2 и 3. Сервисы и параметры могут передаваться как в конструктор, так и в методы actions:
Это приближение на 100% совместимо с первым вариантом, когда все везде опционально. Вы можете использовать параметры и в конструкторе и в действиях, или использовать контейнер, созданный по умолчанию (или сделать зависимым от контекста).
Это также позволяет эмулировать действия из ветки Symfony 1.x:
Так как этот вариант наиболее гибкий, документация должна содержать лучшие примеры использования (best practices).
Вариант почти полная копия варианта 4, за исключением того, что переменные пути не могут быть включены в методы actions. Это позволяет убрать лишние суффиксы (Parameter и Service). А доступ к переменным пути происходит через обращение к объекту запроса:
Еще одним альтернативным вариантом может быть использование аннотаций для включений параметров. Это пока что особо не обсуждается, потому как Symfony 2 пока что не использует аннотаций.
Достоинства:
Полезные ссылки: RFC: Controllers in Symfony 2 и обсуждение на google groups: часть 1, часть 2, часть 3.
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), соединение с БД и т.д.)
- насколько легко/интуитивно создать новый контроллер?
- насколько быстра его реализация?
- насколько легко провести его автоматическое тестирование (Unit tests)? [в топике я этих вопросо не рассматриваю, в дополнительных ссылках это есть]
- насколько многословно/компактно обращаться к параметрам и сервисам?
- насколько реализация отвечает концепции разделения и шаблону проектирования MVC?
Вариант 1. Так устроен механизм контролеров в Symfony 2 сейчас (PR1):
Для обеспечения доступа к сервисам и параметрам, Symfony вставляет контейнер в конструктор контроллера (который потом хранится в protected свойстве):
$controller = new Controller($container);
Доступ к параметрам и сервисам
Когда выполняется действие (action), аргументы метода вставляются через совпадение их имен с переменными пути:
function showAction($slug){ ... }
Аргумент slug будет передан, если у соответствующего пути есть переменная slug: /articles/:slug.Передача переменных пути происходит таким образом:
- если имя аргумента совпадает с именем переменной пути, мы используем это значение ($slug в примере, даже если оно не передано в URL и определено значение по умолчанию);
- если нет, и если определено значению по умолчанию для аргумента, и если аргумент необязательный, мы используем значение по умолчанию;
- если нет, мы выбрасываем исключение.
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(...);
}
Достоинства и недостатки:
Как очевидные достоинства можно выделить понятность, простоту, хорошее быстродействие и удобное тестирование контейнеров специфических видов.
Недостатки:
- Контроллер нагружен контейнером (разделение сущностей);
- Довольно открытый доступ к контейнеру, что требует аккуратности от разработчика;
- Доступ к параметрам и сервисам несколько многословен;
- Разработчики могут начать думать в так называемом sfContext контексте. То есть если они имеют доступ к контейнеру с контроллера, легко передать его в класс модели, но это не самая лучшая идея;
- При тестировании, разработчик будет вынужден ознакомиться с реализацией и знать к каким сервисам контроллер имеет доступ .
Вариант 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(...);
}
Достоинства и недостатки:
Достоинства:
- Дает большую гибкость и полностью совместим с первым вариантом;
- Дает возможность контроля типов при передаче параметров/сервисов;
- Более ясные зависимости;
- Не большой накладной код;
- Конструктор требует все сервисы и параметры (но в большинстве случаев только некоторые будут использоваться) — но успокаивает факт, что вы можете использовать метод контейнера в этих случаях;
- Более шаблонный код: теперь разработчику нужно хранить все передаваемые сервисы в защищенных переменных.
Вариант 3
Вместо вставки сервисов в конструктор, в этом варианте они напрямую вставляются в каждый метод контроллера:
function showAction($slug, $userService, $doctrineManagerService, $maxPerPageParameter){ ... }
Аргументом может быть переменная пути, сервис, или глобальный параметр, при этом правила передачи параметров должны быть уточнены:- Если имя аргумента заканчивается на «Service», мы используем соответствующий сервис ($userService в примере);
- Если имя аргумента заканчивается на «Parameter», мы используем соответствующий параметр с Dependency Injector ($maxPerPageParameter в примере);
- Если нет, и если имя аргумента совпадает с переменной пути, мы используем его ($slug в примере);
- В других случаях, если аргумент не определен, выбрасываем исключение.
// передаем переменную пути `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.
Полезные ссылки: RFC: Controllers in Symfony 2 и обсуждение на google groups: часть 1, часть 2, часть 3.