in Профессиональное

The symfony 2009 Advent Calendar: день 3 – продвинутая маршрутизация (часть 2)

Symfony 2009 advent calendar – это 24 урока продвинутого уровня о symfony. Все уроки представлены на 5 языках: английском, французском, испанском, итальянском, японском. Русского нет, это досадное упущение постараюсь исправить.

Перевод статьи Symfony 2009 Advent Calendar: Advanced Routing (part 2).

Коллекции маршрутов

Для завершения проекта Sympal Builder нам нужно создать админку, в которой каждый клиент может управлять страницами своего сайтика. Для того, чтобы это реализовать, нам нужен набор экшенов (actions), которые будут осуществлять list, create, update, delete для объектов Page. Так как эти действия стандартные, мы можем сгенерировать модуль автоматически. Выполним следующий таск из командной строки, для генерации модуля в апликации backend:

$ php symfony doctrine:generate-module backend pageAdmin Page --with-doctrine-route --with-show

Будет сгенерирован модуль с файлом actions.class.php и соответствующие ему шаблоны, которые позволят выполнять необходимые действия для объекта Page. Кастомизацию сгенерированного CRUD оставим за пределами нашего рассказа ))

Не смотря на то, что мы сгенерировали нужный модуль автоматически, нам нужно еще создать маршрут для каждого экшена. Так как мы использовали при генерации опцию –with-doctrine-route, то каждый экшен был сгенерирован таким образом, чтобы работать с объектом маршрута. Это уменьшает объем кода в каждом экшене. Например, экшн edit состоит из одной строчки:

public function executeEdit(sfWebRequest $request)
{
  $this->form = new PageForm($this->getRoute()->getObject());
}

В итоге, нам нужны маршруты для действий: 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. Его содержимое невероятно просто:

// lib/routing/acClientObjectRouteCollection.class.php
class acClientObjectRouteCollection extends sfObjectRouteCollection
{
  protected
    $routeClass = 'acClientObjectRoute';
}

Свойство $routeClass определяет класс, который будет использоваться для создания каждого маршрута. Теперь, когда мы изменили тип маршрутов, основная часть работы выполнена. Например, по адресу http://pete.sympalbuilder.com/backend.php/pages теперь будет отображаться только одна страница: location из магазина зверюшек Пита. Благодаря нашему классу маршрута, экшн index возвращает только те объекты Page, которые соответствуют клиенту, основываясь на поддомене из запроса. Написав всего-лишь несколько строк кода, мы создали модуль для админки, который может безопасно использоваться различными клиентами.

Недостающая часть: создание новых страниц

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

Для начала, изменим объект PageForm (lib/form/PageForm.class.php):

public function configure()
{
  $this->useFields(array(
    'title',
    'content',
  ));
}

Теперь селектбокс не будет отображаться, чего мы и хотели. Но теперь при создании страницы мы не сможем установить client_id (( Для того чтобы исправить этот баг, укажем необходимого клиента для экшенов new и create:

public function executeNew(sfWebRequest $request)
{
  $page = new Page();
  $page->Client = $this->getRoute()->getClient();
  $this->form = new PageForm($page);
}

Тут мы используем функцию getClient(), которая пока еще не существует )) в классе acClientObjectRoute. Добавим этот метод путем небольших модификаций:

// lib/routing/acClientObjectRoute.class.php
class acClientObjectRoute extends sfDoctrineRoute
{
  // ...

  protected $client = null;

  public function matchesUrl($url, $context = array())
  {
    // ...

    $this->client = $client;

    return array_merge(array('client_id' => $client->id), $parameters);
  }

  public function getClient()
  {
    return $this->client;
  }
}

Путем добавления свойства $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']
  );

  return new $this->routeClass(
    $url,
    $params,
    $requirements,
    $options
  );
}

Теперь остался один шаг – настройка коллекции маршрутов в 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 (директорию надо будет создать):

# apps/backend/lib/action/backendActions.class.php
class backendActions extends sfActions
{
  public function 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.

Маршруты экшенов

Каждый объект коллекции маршрутов принимает три различные опции, который определяют маршруты, которые будут сгенерированы в коллекции. Не вдаваясь в детали, следующая коллекция будет генерировать все семь маршрутов по-умолчанию, наряду с дополнительной коллекцией маршрутов и объектом маршрута:

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 может быть абстрагирована от бизнес-логики и целиком храниться в маршруте, которому принадлежит. В результате мы получаем больший контроль, большую гибкость и более управляемый код.

<blockquote><strong>Symfony 2009 advent calendar</strong> – это 24 урока <strong>продвинутого</strong> уровня о symfony. Все уроки представлены на 5 языках: английском, французском, испанском, итальянском, японском. Русского нет, это досадное упущение постараюсь исправить.</blockquote>

Write a Comment

Comment

*