Symfony 2009 advent calendar – это 24 урока продвинутого уровня о symfony. Все уроки представлены на 5 языках: английском, французском, испанском, итальянском, японском. Русского нет, это досадное упущение постараюсь исправить.
Для завершения проекта Sympal Builder нам нужно создать админку, в которой каждый клиент может управлять страницами своего сайтика. Для того, чтобы это реализовать, нам нужен набор экшенов (actions), которые будут осуществлять list, create, update, delete для объектов Page. Так как эти действия стандартные, мы можем сгенерировать модуль автоматически. Выполним следующий таск из командной строки, для генерации модуля в апликации backend:
Будет сгенерирован модуль с файлом actions.class.php и соответствующие ему шаблоны, которые позволят выполнять необходимые действия для объекта Page. Кастомизацию сгенерированного CRUD оставим за пределами нашего рассказа ))
Не смотря на то, что мы сгенерировали нужный модуль автоматически, нам нужно еще создать маршрут для каждого экшена. Так как мы использовали при генерации опцию –with-doctrine-route, то каждый экшен был сгенерирован таким образом, чтобы работать с объектом маршрута. Это уменьшает объем кода в каждом экшене. Например, экшн edit состоит из одной строчки:
В итоге, нам нужны маршруты для действий: index, new, create, edit, update, delete. Для создания этих маршрутов в RESTful стиле, внесем изменения в routing.yml:
Для того чтобы визуально представить эти маршруты, воспользуемся таском app:routes, который отображает данные о каждом маршруте для указанной апликации (в рамках проекта):
$ php symfony app:routes backend
>> app Current routes for application "backend"
Name Method Pattern
pageAdmin GET /pages
pageAdmin_new GET /pages/new
pageAdmin_create POST /pages
pageAdmin_edit GET /pages/:id/edit
pageAdmin_update PUT /pages/:id
pageAdmin_delete DELETE /pages/:id
pageAdmin_show GET /pages/:id
Заменяем маршруты на коллекцию маршрутов
К счастью, symfony предлагает нам более простой путь для создания маршрутов, которые относятся к традиционному CRUD. Заменим все что мы написали в routing.yml на один маршрут:
Еще раз выполним таск app:routes для того чтобы посмотреть что получилось. Как вы можете видеть, все наши маршруты остались на месте:
$ php symfony app:routes backend
>> app Current routes for application "backend"
Name Method Pattern
pageAdmin GET /pages.:sf_format
pageAdmin_new GET /pages/new.:sf_format
pageAdmin_create POST /pages.:sf_format
pageAdmin_edit GET /pages/:id/edit.:sf_format
pageAdmin_update PUT /pages/:id.:sf_format
pageAdmin_delete DELETE /pages/:id.:sf_format
pageAdmin_show GET /pages/:id.:sf_format
Коллекция маршрутов это специальный тип объекта маршрута, который представляет более чем один маршрут. Маршрут sfDoctrineRouteCollection, например, автоматически генерирует семь основных маршрутов, необходимых для CRUD. За кулисами же, sfDoctrineRouteCollection кроме создания семи маршрутов, которые мы раньше описывали в routing.yml, ничего не делает. Коллекции маршрутов, в основном существуют в качестве ярлыка для создания типичных групп маршрутизации.
Создание новой коллекции маршрутов
Сейчас, каждый клиент имеет возможность модифицировать объекты Page при помощи CRUD по адресу /pages. К несчастью, каждый клиент сейчас также может видеть и модифицировать все страницы, в том числе и не принадлежащие ему. Например, по адресу http://pete.sympalbuilder.com/backend.php/pages отобразится список, состоящий из обеих страниц из нашей фикстуры (см. первую часть) – страницы location из магазина зверюшек Пита и страницы menu из СитиПаба.
Для того чтобы поправить положение, мы используем ранее определенный для фронтэнда класс acClientObjectRoute. Класс sfDoctrineRouteCollection генерирует группу объектов sfDoctrineRoute. В нашем же приложении нужно сгенерировать группу объектов acClientObjectRoute.
Для того чтобы выполнить нашу задумку, нужно создать новый класс коллекции маршрутов. Создадим файл acClientObjectRouteCollection.class.php и разместим его в директории lib/routing. Его содержимое невероятно просто:
Свойство $routeClass определяет класс, который будет использоваться для создания каждого маршрута. Теперь, когда мы изменили тип маршрутов, основная часть работы выполнена. Например, по адресу http://pete.sympalbuilder.com/backend.php/pages теперь будет отображаться только одна страница: location из магазина зверюшек Пита. Благодаря нашему классу маршрута, экшн index возвращает только те объекты Page, которые соответствуют клиенту, основываясь на поддомене из запроса. Написав всего-лишь несколько строк кода, мы создали модуль для админки, который может безопасно использоваться различными клиентами.
Недостающая часть: создание новых страниц
Сейчас, при создании (или редактировании) страницы, отображается селектбокс для выбора клиента, к которому она относится. Вместо того , чтобы давать пользователю возможность выбирать клиента (что есть потенциальная дыра в безопасности), будем устанавливать клиента автоматически, по поддомену из запроса.
Для начала, изменим объект PageForm (lib/form/PageForm.class.php):
Теперь селектбокс не будет отображаться, чего мы и хотели. Но теперь при создании страницы мы не сможем установить client_id (( Для того чтобы исправить этот баг, укажем необходимого клиента для экшенов new и create:
Путем добавления свойства $client (которое принимает значение в методе matchesUrl()), мы легким движением руки сделаем доступным объект клиента через маршрут. Колонка client_id для нового объекта Page теперь автоматически (и правильно) устанавливается на основании поддомена текущего хоста.
Настройка коллекции маршрутов
Используя маршрутизатор, мы легко решили проблемы, которые у нас возникали при создании нашего вымышленного приложения Sympal Builder. По мере роста приложения, разработчик должен иметь возможность повторного использования маршрутов для других модулей в админке (например, для создания фотогалерей для каждого клиента).
С другой стороны, создавать коллекции маршрутов можно также для добавления часто используемых маршрутов. Например, предположим, в проекте есть много моделей с колонкой is_active. В админке нужно иметь простой способ переключения статуса is_active для любого объекта. Для начала, модифицируем acClientObjectRouteCollection и добавим новый маршрут в коллекцию:
// lib/routing/acClientObjectRouteCollection.class.php
protected function generateRoutes(){
parent::generateRoutes();if(isset($this->options['with_is_active'])&&$this->options['with_is_active']){$routeName=$this->options['name'].'_toggleActive';$this->routes[$routeName]=$this->getRouteForToggleActive();}}
Метод sfRouteCollection::generateRoutes() вызывается когда объект коллекции инициализирован и готов к созданию всех необходимых маршрутов и добавлению их переменную класса $routes. Тут мы добавляем новый protected метод getRouteForToggleActive():
protected function getRouteForToggleActive(){$url=sprintf('%s/:%s/toggleActive.:sf_format',$this->options['prefix_path'],$this->options['column']);$params=array('module'=>$this->options['module'],'action'=>'toggleActive','sf_format'=>'html');$requirements=array('sf_method'=>'put');$options=array('model'=>$this->options['model'],'type'=>'object','method'=>$this->options['model_methods']['object']);returnnew$this->routeClass($url,$params,$requirements,$options);}
Теперь остался один шаг – настройка коллекции маршрутов в routing.yml. Обратите внимание, что generateRoutes() ищет опцию with_is_active перед добавлением нового маршрута. Добавление этого куска кода дает нам лучший контроль в ситуации, когда мы хотим использовать acClientObjectRouteCollection где-то, где нет необходимости в маршруте toggleActive:
Выполним таск app:routes и удостоверимся что новый маршрут toggleActive наличествует в списке. Нам осталось лишь создать экшн, который будет делать грязную работу )) Так как вы возможно захотите использовать эту коллекцию маршрутов в различных модулях, создадим файл backendActions.class.php в директории apps/backend/lib/action (директорию надо будет создать):
# apps/backend/lib/action/backendActions.class.php
class backendActions extends sfActions
{publicfunction executeToggleActive(sfWebRequest $request){$obj=$this->getRoute()->getObject();$obj->is_active=!$obj->is_active;$obj->save();$this->redirect($this->getModuleName().'/index');}}
И, наконец, изменим базовый класс pageAdminActions, чтобы он наследовался от нашего только что созданного класса backendActions:
class pageAdminActions extends backendActions
{// ...}
И чего же мы достигли? Теперь каждый новый модуль может автоматически использовать новый функционал всего лишь используя acClientObjectRouteCollection и наследуясь от класса backendActions. Таким образом, общий функционал может быть легко использоваться различными модулями.
Свойства коллекции маршрутов
Объект коллекции маршрутов содержит ряд свойств (опций), которые позволяют производить его гибкую настройку. Во многих случаях, разработчик может использовать эти свойства для конфигурирования коллекции без необходимости создавать новый класс коллекции маршрутов. Подробный список свойств коллекции маршрутов вы можете посмотреть в справочнике The symfony Reference Book.
Маршруты экшенов
Каждый объект коллекции маршрутов принимает три различные опции, который определяют маршруты, которые будут сгенерированы в коллекции. Не вдаваясь в детали, следующая коллекция будет генерировать все семь маршрутов по-умолчанию, наряду с дополнительной коллекцией маршрутов и объектом маршрута:
По умолчанию, первичный ключ модели используется во всех сгенерированных URLs и используется в запросах к БД которые выполняются для получения объектов. Это легко можно изменить. Например, следующий код будет использовать колонку slug вместо первичного ключа:
По умолчанию, маршрут получает все зависимые объекты для коллекции маршрутов по указанной колонке. Если такое поведение нужно изменить, добавьте свойство model_methods в маршрут. В этом примере методы fetchAll() и findForRoute() должны быть добавлены к классу PageTable. Оба метода будут получать массив параметров запроса как аргумент:
Наконец, предположим что вам нужно сделать особый параметр запроса, доступным в запросе для каждого маршрута в коллекции. Это легко сделать при помощи параметра default_params:
pageAdmin:
class: acClientObjectRouteCollection
options:
# ...
default_params:
foo: bar
Заключение
Основная работа маршрутизатора – сверять и генерировать URLы – в symfony переросла в гибкую систему, способную обслуживать сложные URL, необходимые в проектах. Под контролем объекта маршрута, структура URL может быть абстрагирована от бизнес-логики и целиком храниться в маршруте, которому принадлежит. В результате мы получаем больший контроль, большую гибкость и более управляемый код.
<blockquote><strong>Symfony 2009 advent calendar</strong> – это 24 урока <strong>продвинутого</strong> уровня о symfony. Все уроки представлены на 5 языках: английском, французском, испанском, итальянском, японском. Русского нет, это досадное упущение постараюсь исправить.</blockquote>
The symfony 2009 Advent Calendar: день 3 – продвинутая маршрутизация (часть 2)
Перевод статьи Symfony 2009 Advent Calendar: Advanced Routing (part 2).
Коллекции маршрутов
Для завершения проекта Sympal Builder нам нужно создать админку, в которой каждый клиент может управлять страницами своего сайтика. Для того, чтобы это реализовать, нам нужен набор экшенов (actions), которые будут осуществлять list, create, update, delete для объектов Page. Так как эти действия стандартные, мы можем сгенерировать модуль автоматически. Выполним следующий таск из командной строки, для генерации модуля в апликации backend:
Будет сгенерирован модуль с файлом actions.class.php и соответствующие ему шаблоны, которые позволят выполнять необходимые действия для объекта Page. Кастомизацию сгенерированного CRUD оставим за пределами нашего рассказа ))
Не смотря на то, что мы сгенерировали нужный модуль автоматически, нам нужно еще создать маршрут для каждого экшена. Так как мы использовали при генерации опцию –with-doctrine-route, то каждый экшен был сгенерирован таким образом, чтобы работать с объектом маршрута. Это уменьшает объем кода в каждом экшене. Например, экшн edit состоит из одной строчки:
В итоге, нам нужны маршруты для действий: index, new, create, edit, update, delete. Для создания этих маршрутов в RESTful стиле, внесем изменения в routing.yml:
pageAdmin: url: /pages class: sfDoctrineRoute options: { model: Page, type: list } params: { module: page, action: index } requirements: sf_method: [get] pageAdmin_new: url: /pages/new class: sfDoctrineRoute options: { model: Page, type: object } params: { module: page, action: new } requirements: sf_method: [get] pageAdmin_create: url: /pages class: sfDoctrineRoute options: { model: Page, type: object } params: { module: page, action: create } requirements: sf_method: [post] pageAdmin_edit: url: /pages/:id/edit class: sfDoctrineRoute options: { model: Page, type: object } params: { module: page, action: edit } requirements: sf_method: [get] pageAdmin_update: url: /pages/:id class: sfDoctrineRoute options: { model: Page, type: object } params: { module: page, action: update } requirements: sf_method: [put] pageAdmin_delete: url: /pages/:id class: sfDoctrineRoute options: { model: Page, type: object } params: { module: page, action: delete } requirements: sf_method: [delete] pageAdmin_show: url: /pages/:id class: sfDoctrineRoute options: { model: Page, type: object } params: { module: page, action: show } requirements: sf_method: [get]Для того чтобы визуально представить эти маршруты, воспользуемся таском app:routes, который отображает данные о каждом маршруте для указанной апликации (в рамках проекта):
$ php symfony app:routes backend >> app Current routes for application "backend" Name Method Pattern pageAdmin GET /pages pageAdmin_new GET /pages/new pageAdmin_create POST /pages pageAdmin_edit GET /pages/:id/edit pageAdmin_update PUT /pages/:id pageAdmin_delete DELETE /pages/:id pageAdmin_show GET /pages/:idЗаменяем маршруты на коллекцию маршрутов
К счастью, symfony предлагает нам более простой путь для создания маршрутов, которые относятся к традиционному CRUD. Заменим все что мы написали в routing.yml на один маршрут:
pageAdmin: class: sfDoctrineRouteCollection options: model: Page prefix_path: /pages module: pageAdminЕще раз выполним таск app:routes для того чтобы посмотреть что получилось. Как вы можете видеть, все наши маршруты остались на месте:
$ php symfony app:routes backend >> app Current routes for application "backend" Name Method Pattern pageAdmin GET /pages.:sf_format pageAdmin_new GET /pages/new.:sf_format pageAdmin_create POST /pages.:sf_format pageAdmin_edit GET /pages/:id/edit.:sf_format pageAdmin_update PUT /pages/:id.:sf_format pageAdmin_delete DELETE /pages/:id.:sf_format pageAdmin_show GET /pages/:id.:sf_formatКоллекция маршрутов это специальный тип объекта маршрута, который представляет более чем один маршрут. Маршрут sfDoctrineRouteCollection, например, автоматически генерирует семь основных маршрутов, необходимых для CRUD. За кулисами же, sfDoctrineRouteCollection кроме создания семи маршрутов, которые мы раньше описывали в routing.yml, ничего не делает. Коллекции маршрутов, в основном существуют в качестве ярлыка для создания типичных групп маршрутизации.
Создание новой коллекции маршрутов
Сейчас, каждый клиент имеет возможность модифицировать объекты Page при помощи CRUD по адресу /pages. К несчастью, каждый клиент сейчас также может видеть и модифицировать все страницы, в том числе и не принадлежащие ему. Например, по адресу http://pete.sympalbuilder.com/backend.php/pages отобразится список, состоящий из обеих страниц из нашей фикстуры (см. первую часть) – страницы location из магазина зверюшек Пита и страницы menu из СитиПаба.
Для того чтобы поправить положение, мы используем ранее определенный для фронтэнда класс acClientObjectRoute. Класс sfDoctrineRouteCollection генерирует группу объектов sfDoctrineRoute. В нашем же приложении нужно сгенерировать группу объектов acClientObjectRoute.
Для того чтобы выполнить нашу задумку, нужно создать новый класс коллекции маршрутов. Создадим файл acClientObjectRouteCollection.class.php и разместим его в директории lib/routing. Его содержимое невероятно просто:
Свойство $routeClass определяет класс, который будет использоваться для создания каждого маршрута. Теперь, когда мы изменили тип маршрутов, основная часть работы выполнена. Например, по адресу http://pete.sympalbuilder.com/backend.php/pages теперь будет отображаться только одна страница: location из магазина зверюшек Пита. Благодаря нашему классу маршрута, экшн index возвращает только те объекты Page, которые соответствуют клиенту, основываясь на поддомене из запроса. Написав всего-лишь несколько строк кода, мы создали модуль для админки, который может безопасно использоваться различными клиентами.
Недостающая часть: создание новых страниц
Сейчас, при создании (или редактировании) страницы, отображается селектбокс для выбора клиента, к которому она относится. Вместо того , чтобы давать пользователю возможность выбирать клиента (что есть потенциальная дыра в безопасности), будем устанавливать клиента автоматически, по поддомену из запроса.
Для начала, изменим объект PageForm (lib/form/PageForm.class.php):
Теперь селектбокс не будет отображаться, чего мы и хотели. Но теперь при создании страницы мы не сможем установить client_id (( Для того чтобы исправить этот баг, укажем необходимого клиента для экшенов new и create:
Тут мы используем функцию getClient(), которая пока еще не существует )) в классе acClientObjectRoute. Добавим этот метод путем небольших модификаций:
Путем добавления свойства $client (которое принимает значение в методе matchesUrl()), мы легким движением руки сделаем доступным объект клиента через маршрут. Колонка client_id для нового объекта Page теперь автоматически (и правильно) устанавливается на основании поддомена текущего хоста.
Настройка коллекции маршрутов
Используя маршрутизатор, мы легко решили проблемы, которые у нас возникали при создании нашего вымышленного приложения Sympal Builder. По мере роста приложения, разработчик должен иметь возможность повторного использования маршрутов для других модулей в админке (например, для создания фотогалерей для каждого клиента).
С другой стороны, создавать коллекции маршрутов можно также для добавления часто используемых маршрутов. Например, предположим, в проекте есть много моделей с колонкой is_active. В админке нужно иметь простой способ переключения статуса is_active для любого объекта. Для начала, модифицируем acClientObjectRouteCollection и добавим новый маршрут в коллекцию:
Метод sfRouteCollection::generateRoutes() вызывается когда объект коллекции инициализирован и готов к созданию всех необходимых маршрутов и добавлению их переменную класса $routes. Тут мы добавляем новый protected метод getRouteForToggleActive():
Теперь остался один шаг – настройка коллекции маршрутов в routing.yml. Обратите внимание, что generateRoutes() ищет опцию with_is_active перед добавлением нового маршрута. Добавление этого куска кода дает нам лучший контроль в ситуации, когда мы хотим использовать acClientObjectRouteCollection где-то, где нет необходимости в маршруте toggleActive:
# apps/frontend/config/routing.yml pageAdmin: class: acClientObjectRouteCollection options: model: Page prefix_path: /pages module: pageAdmin with_is_active: trueВыполним таск app:routes и удостоверимся что новый маршрут toggleActive наличествует в списке. Нам осталось лишь создать экшн, который будет делать грязную работу )) Так как вы возможно захотите использовать эту коллекцию маршрутов в различных модулях, создадим файл backendActions.class.php в директории apps/backend/lib/action (директорию надо будет создать):
И, наконец, изменим базовый класс pageAdminActions, чтобы он наследовался от нашего только что созданного класса backendActions:
И чего же мы достигли? Теперь каждый новый модуль может автоматически использовать новый функционал всего лишь используя acClientObjectRouteCollection и наследуясь от класса backendActions. Таким образом, общий функционал может быть легко использоваться различными модулями.
Свойства коллекции маршрутов
Объект коллекции маршрутов содержит ряд свойств (опций), которые позволяют производить его гибкую настройку. Во многих случаях, разработчик может использовать эти свойства для конфигурирования коллекции без необходимости создавать новый класс коллекции маршрутов. Подробный список свойств коллекции маршрутов вы можете посмотреть в справочнике The symfony Reference Book.
Маршруты экшенов
Каждый объект коллекции маршрутов принимает три различные опции, который определяют маршруты, которые будут сгенерированы в коллекции. Не вдаваясь в детали, следующая коллекция будет генерировать все семь маршрутов по-умолчанию, наряду с дополнительной коллекцией маршрутов и объектом маршрута:
pageAdmin: class: acClientObjectRouteCollection options: # ... actions: [list, new, create, edit, update, delete, show] collection_actions: indexAlt: [get] object_actions: toggle: [put]Колонка (Column)
По умолчанию, первичный ключ модели используется во всех сгенерированных URLs и используется в запросах к БД которые выполняются для получения объектов. Это легко можно изменить. Например, следующий код будет использовать колонку slug вместо первичного ключа:
pageAdmin: class: acClientObjectRouteCollection options: # ... column: slugМетоды модели
По умолчанию, маршрут получает все зависимые объекты для коллекции маршрутов по указанной колонке. Если такое поведение нужно изменить, добавьте свойство model_methods в маршрут. В этом примере методы fetchAll() и findForRoute() должны быть добавлены к классу PageTable. Оба метода будут получать массив параметров запроса как аргумент:
pageAdmin: class: acClientObjectRouteCollection options: # ... model_methods: list: fetchAll object: findForRouteПараметры по умолчанию
Наконец, предположим что вам нужно сделать особый параметр запроса, доступным в запросе для каждого маршрута в коллекции. Это легко сделать при помощи параметра default_params:
pageAdmin: class: acClientObjectRouteCollection options: # ... default_params: foo: barЗаключение
Основная работа маршрутизатора – сверять и генерировать URLы – в symfony переросла в гибкую систему, способную обслуживать сложные URL, необходимые в проектах. Под контролем объекта маршрута, структура URL может быть абстрагирована от бизнес-логики и целиком храниться в маршруте, которому принадлежит. В результате мы получаем больший контроль, большую гибкость и более управляемый код.