進化したルーティング

Ryan Weaver 著

symfony のルーティングフレームワークの中心となるのは、個別の URL とプロジェクトで使う特定のロケーションとを双方向に結びつけるマップです。ルーティングフレームワークを使うと、アプリケーションロジックとは完全に独立しながら、簡単にきれいな URL を生成できます。最新の symfony ではより高度なことができるようになっており、ルーティングフレームワークはさらに先を進んでいます。

この章では、各クライアントが別々のサブドメインで使えるシンプルな Web アプリケーションの作り方を説明していきます (例: client1.mydomain.comclient2.mydomain.com)。ルーティングフレームワークを拡張すると、このようなアプリケーションを簡単に構築できます。

NOTE この章では、プロジェクトの ORM として Doctrine を使って説明します。

プロジェクトセットアップ: 多くのクライアントのための CMS

このプロジェクトでは架空の会社 - Sympal Builder - の CMS を開発し、クライアントが sympalbuilder.com のサブドメインを使って Web サイトを構築できるようにします。具体的には、クライアント XXX のサイトは xxx.sympalbuilder.com で閲覧でき、管理者ページは xxx.sympalbuilder.com/backend.php から使えるようにします。

NOTE Sympal という名前は Jonathan Wage の Sympal からお借りしました。Sympal は symfony を使って作られたコンテンツマネージメントフレームワーク (CMF) です。

このプロジェクトには2つの基本要件があります。

  • ユーザーがページを作成でき、各ページのタイトル、コンテンツ、URL を指定できる必要があります。

  • アプリケーション全体を 1 つの symfony プロジェクトとして構築し、このプロジェクトですべてのクライアントサイトのフロントエンド、およびバックエンドを処理するようにします。また、サブドメインによりクライアントを判別し、正しいデータを読み込む必要があります。

NOTE このアプリケーションを作るため、サーバーは全ての *.sympalbuilder.com サブドメインが同じドキュメントルートを指すようセットアップする必要があります。ドキュメントルートは symfony プロジェクトの web ディレクトリです。

スキーマとデータ

本プロジェクト向けのデータベースは ClientPage オブジェクトから構成されます。各 Client オブジェクトは1つのサブドメインサイトを意味し、それぞれが多数の Page オブジェクトで構成されます。

[yml]
# config/doctrine/schema.yml
Client:
  columns:
    name:       string(255)
    subdomain:  string(50)
  indexes:
    subdomain_index:
      fields:   [subdomain]
      type:     unique

Page:
  columns:
    title:      string(255)
    slug:       string(255)
    content:    clob
    client_id:  integer
  relations:
    Client:
      alias:        Client
      foreignAlias: Pages
      onDelete:     CASCADE
  indexes:
    slug_index:
      fields:   [slug, client_id]
      type:     unique

NOTE 各テーブルごとのインデックスは必須ではありませんが、アプリケーションでこれらのカラムに基づいたクエリを頻繁に使うのであれば、インデックスを設定しておくとよいでしょう。

プロジェクトに命を吹き込むため、下記のテストデータを data/fixtures/fixtures.yml ファイルに記述します:

[yml]
# data/fixtures/fixtures.yml
Client:
  client_pete:
    name:      Pete's Pet Shop
    subdomain: pete
  client_pub:
    name:      City Pub and Grill
    subdomain: citypub

Page:
  page_pete_location_hours:
    title:     Location and Hours | Pete's Pet Shop
    content:   We're open Mon - Sat, 8 am - 7pm
    slug:      location
    Client:    client_pete
  page_pub_menu:
    title:     City Pub And Grill | Menu
    content:   Our menu consists of fish, Steak, salads, and more.
    slug:      menu
    Client:    client_pub

このテストデータにより、初期状態としてページが1つだけある Web サイトが2つ作られます。各ページの完全な URL は、Client オブジェクトの subdomain カラムと Page オブジェクトの slug カラムの両方を使って定義されます。

http://pete.sympalbuilder.com/location
http://citypub.sympalbuilder.com/menu

ルーティング

Sympal Builder の Web サイトの各ページは Page モデルオブジェクトと直接対応しており、出力するタイトルとコンテンツが Page モデルオブジェクトで定義されています。各 URL を Page オブジェクトへ厳密に関連づけるために、slug フィールドを使う sfDoctrineRoute というタイプのオブジェクトルートを作ります。次のコードで、URL にマッチする slug フィールドのある Page オブジェクトをデータベースから自動的に検索します:

[yml]
# apps/frontend/config/routing.yml
page_show:
  url:        /:slug
  class:      sfDoctrineRoute
  options:
    model:    Page
    type:     object
  params:
    module:   page
    action:   show

上記のルートにより、http://pete.sympalbuilder.com/location という URL のページについては正しい Page オブジェクトにマッチします。しかし、このルートは http://pete.sympalbuilder.com/menu という間違った URL でもマッチしてしまいます。これでは、レストランのメニューが Pete の Web サイトでも表示されてしまう、ということを意味します。現時点では、このルートはクライアントのサブドメインを考慮していないからです。

アプリケーションを正しく動作させるために、ルートでサブドメインを正しく判定する必要があります。つまり、ルートが cliend_idslug の両方を加味した正しい Page オブジェクトにマッチしなければいけません。ここで cliend_id は、ホスト (例 pete.sympalbuilder.com) を Client モデルオブジェクトの subdomain カラムと照合して決定します。このような機能を実現するためにルーティングフレームワークを活用し、カスタムルーティングクラスを作ります。

このような実装を行う前に、ルーティングシステムの動作についての理解を深めておきましょう。

ルーティングシステムの動作のしくみ

symfony において「ルート」とは sfRoute オブジェクトのことであり、このオブジェクトは次の2つの重要な仕事を担当します:

  • URL の生成 : たとえば、page_show というルートに slug パラメータを渡すと、実際のURLを生成できます (例 /location)。

  • 受け取った URL のマッチング : 受け取ったリクエストの URL に対して、各ルートはその URL が自身の条件にマッチするかどうかを決定します。

通常、個々のルートの情報は、各アプリケーションのコンフィグディレクトリにある app/yourappname/config/routing.yml ファイルでセットアップします。ここで各ルートは sfRoute オブジェクトであると繰り返し言っておきます。これらのシンプルな YAML エントリが、どのように処理されて sfRoute オブジェクトになるのでしょうか?

ルーティングのキャッシュコンフィグハンドラ

実際、ほとんどのルートは YAML ファイルで定義しますが、YAML ファイルの各エントリーはキャッシュコンフィグハンドラという特別なクラスにより、リクエスト時に実際のオブジェクトに変換されます。これは最終的に、アプリケーション内のすべてのルートをあらわす PHP コードになります。この処理の詳細は本章では取り上げませんが、page_show ルートのコンパイルされたバージョンが最終的にどのようになっているのか見ておきましょう。コンパイルされたファイルは、 アプリケーションと環境ごとに cache/yourappname/envname/config/config_routing_yml.php に保存されます。page_show ルートの部分のみを切り出すと、次のようになっています:

[php]
new sfDoctrineRoute('/:slug', array (
  'module' => 'page',
  'action' => 'show',
), array (
  'slug' => '[^/\\.]+',
), array (
  'model' => 'Page',
  'type' => 'object',
));

TIP 各ルートのクラス名は、routing.yml ファイルにて class キーで定義します。class キーが指定されていない場合は、デフォルトで sfRoute クラスのルートになります。よく使われるほかのルートクラスの例として、RESTful なルートを実装できる sfRequestRoute クラスがあります。すべてのルートクラスと利用可能なオプションの一覧については、symfony リファレンスを参照してください。

特定ルートに対するリクエストのマッチング

ルーティングフレームワークの主な仕事の1つは、リクエストされた各 URL を正しいルートオブジェクトとマッチングさせることです。このようなルーティングエンジンの中心は sfPatternRouting クラスで、マッチング処理を担当します。このように sfPatternRouting はとても重要なクラスですが、開発者が直接操作することはほとんどありません。

リクエストされた URL を正しいルートにマッチングさせるために、sfPatternRouting は各 sfRoute を順に処理し、URL にマッチするかどうかを各ルートに問い合わせます。この処理の内部では、sfPatternRouting により各ルートオブジェクトの ~sfRoute::matchsUrl()~ メソッドが呼び出されています。ルートがリクエストされた URL にマッチしない場合は、このメソッドは単純に false を返します。

ルートがリクエストされた URL にマッチする場合は、sfRoute::matchsUrl() は単純に true を返すのではなく、リクエストオブジェクトにマージされるパラメータ配列を返します。たとえば、http://pete.sympalbuilder.com/location という URL は page_show ルートにマッチし、matchsUrl() メソッドは次のような配列を返します:

[php]
array('slug' => 'location')

この情報はリクエストオブジェクトにマージされるので、アクションファイルやその他の場所からルートの変数 (例 slug) にアクセスすることが可能となります。

[php]
$this->slug = $request->getParameter('slug');

ここでお気づきの方もいらっしゃるかと思いますが、何らかの目的でルートを拡張したりカスタマイズするには、sfRoute::matchsUrl() メソッドをオーバーライドするとよいでしょう。

カスタムルートクラスの作成

Client オブジェクトのサブドメインにもとづいてマッチングするように page_show ルートを拡張するために、新しいカスタムルートクラスを作ります。次の内容で acClientObjectRoute.class.php という名前のファイルを作り、プロジェクトの lib/routing ディレクトリに配置します (ディレクトリを作る必要があります):

[php]
// lib/routing/acClientObjectRoute.class.php
class acClientObjectRoute extends sfDoctrineRoute
{
  public function matchesUrl($url, $context = array())
  {
    if (false === $parameters = parent::matchesUrl($url, $context))
    {
      return false;
    }

    return $parameters;
  }
}

ほかに必要なことは、page_show ルートでこのルートクラスを使うように設定するだけです。routing.yml ファイルで、次のようにルートの class キーを変更します:

[yml]
# apps/fo/config/routing.yml
page_show:
  url:        /:slug
  class:      acClientObjectRoute
  options:
    model:    Page
    type:     object
  params:
    module:   page
    action:   show

今までのところ、acClientObjectRoute クラスには何の機能も追加していませんが、全ての構成要素の準備は整っています。matchsUrl() メソッドに追加する2つの処理については、次の節で説明します。

カスタムルートクラスへのロジックの追加

必要な機能をカスタムルートクラスに追加するには、acClientObjectRoute.class.php ファイルの中身を次のコードに置き換えます:

[php]
class acClientObjectRoute extends sfDoctrineRoute
{
  protected $baseHost = '.sympalbuilder.com';

  public function matchesUrl($url, $context = array())
  {
    if (false === $parameters = parent::matchesUrl($url, $context))
    {
      return false;
    }

    // baseHost が見つからない場合は false を返す
    if (strpos($context['host'], $this->baseHost) === false)
    {
      return false;
    }

    $subdomain = str_replace($this->baseHost, '', $context['host']);

    $client = Doctrine_Core::getTable('Client')
      ->findOneBySubdomain($subdomain)
    ;

    if (!$client)
    {
      return false;
    }

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

最初に呼び出している parent::matchesUrl() により、通常のルートマッチング処理が実行されることにご注意ください。この例では、/location という URL が page_show ルートにマッチするので、マッチした slug パラメータを含む配列が parent::matchesUrl() から返されます。

[php]
array('slug' => 'location')

つまり、ルートマッチングの共通処理はこれで完了しており、メソッドの残りの部分では、正しい Client サブドメインに基づくマッチング処理にフォーカスできます。

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

  $subdomain = str_replace($this->baseHost, '', $context['host']);

  $client = Doctrine_Core::getTable('Client')
    ->findOneBySubdomain($subdomain)
  ;

  if (!$client)
  {
    return false;
  }

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

単純な文字列置換を実行することでホストのサブドメイン部を分離することができ、このサブドメインを持つ Client オブジェクトがあるかどうかをデータベースに問い合わせることができます。サブドメインにマッチする Client オブジェクトが見つからない場合は false を返し、これはリクエストされた URL がこのルートにマッチしないことを示します。リクエストされた URL のサブドメインをもった Client オブジェクトが存在する場合は、パラメータ配列に追加パラメータの client_id をマージして返します。

TIP matchesUrl() に渡される配列 $context には、現在のリクエストに関する多くの役立つ情報が事前にセットされています。たとえば host やブール値の is_securerequest_uri、および HTTP メソッドの method などがあります。

ところで、実際カスタムルートクラスにはどういった処理を実装したのでしょうか?acClientObjectRoute クラスは次の処理を実行します:

  • リクエストされた $urlhost が、Client オブジェクトの1つがもっているサブドメインを含む場合にのみマッチします。

  • ルートがマッチした場合、マッチした Client オブジェクトの client_id を追加パラメータとして返し、リクエストパラメータに結合されます。

カスタムルートクラスの利用

acClientObjectRoute によって正しい client_id パラメータが返されるので、リクエストオブジェクトから client_id パラメータにアクセスできます。たとえば、page/show アクションで client_id を使って、正しい Page オブジェクトを次のように取得できます:

[php]
public function executeShow(sfWebRequest $request)
{
  $this->page = Doctrine_Core::getTable('Page')->findOneBySlugAndClientId(
    $request->getParameter('slug'),
    $request->getParameter('client_id')
  );

  $this->forward404Unless($this->page);
}

NOTE findOneBySlugAndClientId() メソッドは Doctrine 1.2 から使えるようになったマジックファインダ型のメソッドで、複数のフィールドにもとづきオブジェクトを問い合わせます。

上で示したやり方でも問題ありませんが、ルーティングフレームワークを使ったより洗練された解決法があります。まず最初に、acClientObjectRoute クラスに次のメソッドを追加します:

[php]
protected function getRealVariables()
{
  return array_merge(array('client_id'), parent::getRealVariables());
}

次に示す最後のコードは、アクションから正しい Page オブジェクトを返す処理を完全にルートに任せています。page/show アクションは1行だけになりました:

[php]
public function executeShow(sfWebRequest $request)
{
  $this->page = $this->getRoute()->getObject();
}

これ以上の処理を追加しなくても、上記のコードで slugclient_id の両方のカラムにもとづいて Page オブジェクトを問い合わせます。さらに、他のオブジェクトルートと同じように、一致するオブジェクトがなければアクションは自動的に404ページにフォワードされます。

しかしどうやってこのような動作をするのでしょうか?acClientObjectRoute の継承元である sfDoctrineRoute のようなオブジェクトルートは、ルートの url キーにある変数にもとづいて関連するオブジェクトを自動的に問い合わせます。たとえば、page_show ルートには url キーのなかに :slug 変数があるので、slug カラムを使って Page オブジェクトを問い合わせます。

しかしながらこのアプリケーションでは、page_show ルートで Page オブジェクトを問い合わせる際に、client_id カラムも加味する必要があります。これを実現するため、~sfObjectRoute::getRealVariables()~ をオーバーライドしました。このメソッドは内部で呼び出され、オブジェクトの問い合わせに使うカラムを決定します。このメソッドが返す配列に client_id フィールドを追加することで、acClientObjectRouteslugclient_id の両方のカラムにもとづいてオブジェクトを問い合わせます。

NOTE オブジェクトルートでは、実在するカラムに一致しない変数は自動的に無視されます。たとえば、URL のキーに :page という変数が含まれていても、対応するテーブルに page カラムが存在しない場合、この変数は無視されます。

現時点では、カスタムルートクラスはほんの少しの努力で必要とされる全ての機能を達成しています。以降のセクションでは、新しいルートを再利用してクライアント固有の管理エリアを作っていきます。

正しい URL の生成

ルートの URL を生成すると、問題があることが分かります。次のコードのようにページへのリンクを作ったとしましょう:

[php]
<?php echo link_to('Locations', 'page_show', $page) ?>

-

生成された URL : /location?client_id=1

このように、URL に自動的に client_id が追加されています。ルートで URL を生成する際に、利用可能なルートの変数をすべて使うからです。このルートでは slug パラメータと client_id パラメータの両方を扱うようにしたので、URL を生成するときにも両方が使われてしまいます。

この処理を修正するため、acClientObjectRoute クラスに次のメソッドを追加します:

[php]
protected function doConvertObjectToArray($object)
{
  $parameters = parent::doConvertObjectToArray($object);

  unset($parameters['client_id']);

  return $parameters;
}

オブジェクトルートで URL を生成するとき、doConvertObjectArray() メソッドを呼び出して必要な全ての情報を取得しようと試みます。デフォルトでは、$parameter 配列で client_id も返すようになっています。この cliend_id を配列から破棄することで、生成される URL から client_id を除外できます。Client を識別する情報は URL のサブドメインであるということに注意してください。

TIP doConvertObjectToArray() のプロセス全体をオーバーライドし、モデルクラスに toParams() メソッドを追加することで、この処理を完全にカスタマイズできます。toParams() メソッドは、ルートでの URL 生成に使うパラメータの配列を返すようにします。

ルートコレクション

Sympal Builder のアプリケーションを仕上げるため、それぞれの Client が自身の Page 群を管理できるような管理エリアを作る必要があります。管理エリアには、Page オブジェクトの一覧表示、作成、更新、削除ができる一連のアクションが必要です。このような処理を行うモジュールは頻繁に使われるため、symfony ではモジュールを自動生成できます。コマンドラインから次のタスクを実行すると、backend と呼ばれるアプリケーション内に pageAdmin モジュールが生成されます:

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

上記のタスクでは、任意の Page オブジェクトの変更に必要なアクションファイルや関連するテンプレートを含むモジュールが生成されます。生成された CRUD の大部分はカスタマイズ可能ですが、カスタマイズの詳細については本章では扱いません。

上記のタスクで必要なモジュールは準備できていますが、まだ各アクションごとにルートを作成する必要があります。タスクに --with-doctrine-route オプションを指定しているので、各アクションはオブジェクトルートを使うように生成されています。こうすると、各アクション内のコード量を減少します。たとえば、edit アクションは次のように1行のみで構成されます:

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

全体で必要なルートは indexnewcreateeditupdatedelete アクションです。通常、RESTful の様式でこれらのルートを生成するには、次のように routing.yml に多くのセットアップ項目が必要になります。

[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 の内容は、次のようにたった1つのルートに置き換えることができます:

[yml]
pageAdmin:
  class:   sfDoctrineRouteCollection
  options:
    model:        Page
    prefix_path:  /pages
    module:       pageAdmin

再度 app:routes タスクを実行して、すべてのルートを表示してみてください。先ほどの7つルートがすべて存在しています。

$ 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 に必要なよく使われる7 つのルートが自動生成されます。sfDoctrineRouteCollection の内部では、routing.yml に手作業で指定していた7つのルートと同じルートを作っているだけです。基本的には、ルートコレクションはルートの共通グループを作るためのショートカットです。

カスタムルートコレクションの作成

現時点で、各 Client/pages URL からアクセスできる CRUD を使って Page オブジェクトを編集できます。しかし、Page オブジェクトが Client に関連づけられたものであるかどうかに関わらず、各 Clientすべての Page オブジェクトを閲覧し編集できてしまいます。たとえば http://pete.sympalbuilder.com/backend.php/pages という URL にアクセスすると、フィクスチャに記述した両方のページ、つまり Pete のペットショップの location ページと City Pub の menu ページが一覧表示されます。

この処理を修正するため、フロントエンド用に作った acClientObjectRoute を再利用します。sfDoctrineRouteCollection クラスは、sfDoctrineRoute オブジェクトのグループを生成します。このアプリケーションでは、代わりに acClientObjectRoute オブジェクトのグループを生成する必要があります。

このためには、カスタムルートコレクションクラスを使う必要があります。新しく acClientObjectRouteCollection.class.php という名前のファイルを作り、lib/routing ディレクトリに配置します。作ったファイルは信じられないくらいシンプルな構成です:

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

$routeClass プロパティで、コレクションの各ルートの生成に使われるクラスを定義します。こうすると、コレクションで生成する各ルーティングは acClientObjectRoute ルートになるので、必要な変更作業は完了しました。たとえば、http://pete.sympalbuilder.com/backend.php/pages という URL にアクセスすると、一覧には Pete のペットショップの location ページのみが表示されます。カスタムルートクラスのおかげで、インデックスアクションでは、リクエストのサブドメインに対応する Client に関連づけられた Page オブジェクトのみが返されます。数行のコードだけで、複数のクライアントが安全に使えるバックエンドモジュール全体を作成できました。

足りない要素: 新規ページの生成

現在は、バックエンドで Page オブジェクトを作成または編集するとき、Client の選択ボックスが表示されます。ユーザーに Client を選ばせるのはセキュリティリスクとなるため、現在のリクエストのサブドメインに応じた Client を自動でセットするようにしましょう。

まず最初に、lib/form/PageForm.class.php にある PageForm オブジェクトを変更します。

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

これで、意図したとおりセレクトボックスは Page フォームに表示されなくなりました。しかし、新しい Page オブジェクトを作っても、client_id はセットされません。これを修正するため、new アクションと create アクション内で関連する Client を手作業でセットします。

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

上のコードには新しい getClient() メソッドがあります。このメソッドは acClientObjectRoute クラスにまだ存在していません。以前のコードに多少の変更を加え、このメソッドをクラスに追加しましょう:

[php]
// 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 オブジェクトを利用できるようになります。新しい Page オブジェクトの client_id カラムには、現在のホストのサブドメインに対応する値が自動的にセットされます。

オブジェクトルートコレクションのカスタマイズ

ルーティングフレームワークを使うと、Sympal Builder アプリケーションの構築における問題を簡単に解決できます。アプリケーションが大きくなった場合、今回作成したカスタムルートをバックエンドエリアの他のモジュールで再利用できるでしょう (例 Client ごとに写真ギャラリを管理する)。

カスタムルートコレクションを作るもう1つのよくある理由は、頻繁に使われるルートの追加です。たとえば、is_active カラムのあるモデルがプロジェクトに多数あると仮定します。管理エリアでは、これらのモデルの特定のオブジェクトの is_active の値を簡単に調整するような方法が必要となります。最初に、acClientObjectRouteCollection を変更して新しいルートをコレクションに追加するようにします:

[php]
// 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();
  }
}

~sfObjectRouteCollection::generateRoutes()~ メソッドはコレクションオブジェクトがインスタンス化されるときに呼び出され、必要なすべてのルートを生成してクラスプロパティの $routes 配列へ追加します。このケースでは、実際のルートの生成処理は、次の getRouteForToggleActive() という新しい protected メソッドにまかせています:

[php]
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 ルートを必要としない場合に制御しやすくなります:

[yml]
# 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 ディレクトリに作ります (ディレクトリを作る必要があります):

[php]
# 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');
  }
}

最後に、新しい backendActions クラスを継承するように pageAdminActions クラスの基底クラスを変更します。

[php]
class pageAdminActions extends backendActions
{
  // ...
}

この作業でどのようなことを行ったのでしょうか?ルートコレクションにルートを追加し、関連するアクションを基本アクションファイルに追加することで、acClientObjectRouteCollection を使い、backendActions を継承するだけで、任意の新しいモジュールでこれらの機能を使えます。このようにして、共通機能を多くのモジュールで簡単に共有できます。

ルートコレクションのオプション

オブジェクトルートコレクションには、高度にカスタマイズするための一連のオプションがあります。多くの場合、新しいルートコレクションクラスを作るのではなく、これらのオプションを使ってコレクションを設定できます。ルートコレクションのオプションの詳細な一覧は symfony ファレンスを参照してください。

アクションルート

各オブジェクトルートコレクションには、コレクションで生成されるルートを厳密に定義するための3つのオプションがあります。詳細な説明は省きますが、次に示すコレクションによりデフォルトの7つのルートと、追加のコレクションルート、オブジェクトルートが生成されます:

[yml]
pageAdmin:
  class:   acClientObjectRouteCollection
  options:
    # ...
    actions:      [list, new, create, edit, update, delete, show]
    collection_actions:
      indexAlt:   [get]
    object_actions:
      toggle:     [put]

カラム

デフォルトでは、生成されたすべての URL、およびオブジェクトの問い合わせにモデルのプライマリキーが使われます。もちろん、このルールは簡単に変更できます。たとえば、次のコードではプライマリキーの代わりに slug カラムを使います:

[yml]
pageAdmin:
  class:   acClientObjectRouteCollection
  options:
    # ...
    column: slug

モデルメソッド

デフォルトでは、ルートはコレクションルートに関連するすべてのオブジェクトを取得し、オブジェクトルートに指定された culumn で問い合わせます。この処理を書き換える必要がある場合は、ルートに model_methods オプションを追加します。この例では、PageTable クラスに fetchAll() メソッドと findForRoute メソッドを追加する必要があります。これらのメソッドは、引数としてリクエストパラメータの配列を受け取ります:

[yml]
pageAdmin:
  class:   acClientObjectRouteCollection
  options:
    # ...
    model_methods:
      list:       fetchAll
      object:     findForRoute

デフォルトパラメータ

最後に、コレクションの各ルートに対して、リクエストで利用できる特定のリクエストパラメータを作る必要があると仮定します。これは default_params オプションを使えば簡単です:

[yml]
pageAdmin:
  class:   acClientObjectRouteCollection
  options:
    # ...
    default_params:
      foo:   bar

最後に

従来は URL のマッチングと生成を行うだけだったルーティングフレームワークは、プロジェクトにおける複雑な URL 要件のほとんどをカバーできる、完全にカスタマイズ可能なシステムに進化しました。ルートオブジェクトを制御することで、特殊な URL 構造をビジネスロジックから分離し、ルートの中で完結させることができます。その結果、より制御しやすく、柔軟性があり、管理しやすいコードになるのです。