symfony 1.2のルーティングまとめ

皆さんこんにちは、小川です。
花粉症には厳しい季節がやってきましたが、負けずにブログを書いていきたいと思います。

今回はsymfony 1.2で新しくなったルーティングまわりについて書いてみたいと思います。

◆ ルーティングの定義とREST

具体的に何が新しくなったのかというと、RESTfulなルーティングがサポートされました。
RESTについて非常に噛み砕いて説明すると、リソース(URI)にHTTPのGET,POST,PUT,DELETEなどといったメソッドを用いてアクセスして操作するものだと思ってください。

具体的なコードを見た方がたぶんわかりやすいと思うので進めていきます。まずは従来のルーティングです。


// apps/frontend/config/routing.yml
product:
  url:      /product
  param:    { module: product, action: index }

上記のrouting.ymlは、商品の一覧を取得するためのルーティングの例です。非常に単純で、/productでアクセスしたときにproductモジュールのindexアクションを実行するというものです。
この場合、RESTfulについての説明で出てきたHTTPのメソッドについては基本的に関係ありません。とにかく、/productでアクセスした場合はproductモジュールのindexアクションが実行されます。

symfony 1.2になるに伴い、この1つのルーティングがsfRouteというオブジェクトとして扱われるようになりました。
RESTfulなルーティングを行うためには、sfRouteを拡張したsfRequestRouteというオブジェクトとして定義する必要があります。


// apps/frontend/config/routing.yml
# 商品一覧
product:
  class:    sfRequestRoute
  url:      /product
  param:    { module: product, action: index }
  requirements:
    sf_method: [get]
# 商品追加入力
product_new:
  class:    sfRequestRoute
  url:      /product/new
  param:    { module: product, action: new }
  requirements:
    sf_method: [get]
# 商品追加実行
product_create:
  class:    sfRequestRoute
  url:      /product
  param:    { module: product, action: create }
  requirements:
    sf_method: [post]

商品一覧に加え、商品追加に関連のあるアクションのルーティングも定義してみました。ここで注目して欲しいのはproductとproduct_createのurlです。どちらも/productとなっています。お気づきな方もいるかと思いますが、その下にrequirementsという属性としてsf_methodの指定がされています。これにより、symfonyが内部的にメソッドの種類を見てアクションの判別を行います。

さて、何気なくsf_methodという値が出てきました。このsf_methodという値がRESTfulなルーティングを行う上でのキモになってきます。
先ほどはURIとHTTPのメソッドという説明をしましたが、実は現在のHTMLとXHTMLではform要素のmethod属性にはGETとPOST以外は指定できない仕様になっています。(HTML 5では指定できるようになるかもしれません)
そこで登場するのがこのsf_methodです。sf_methodの実態は、なんてことはないsf_methodという名前のリクエストパラメータです。


<form action="<?php echo url_for('product_create') ?>" method="post">
  <input type="hidden" name="sf_method" value="put" />
  <!-- // ... -->
</form>

2行目のinput要素がsf_methodです。symfony 1.2ではPOSTと同時にこのsf_methodを渡すことで、HTTPのメソッドをシミュレートしています。
尚、GETとPOSTに関しては特に記述する必要はありません。また、指定方法は他にもいくつか存在し、自分で記述することはあんまりないと思われます。それについては後述します。

ここまでで、リソース(URI)に対してメソッドを指定することで色々できるようになった、ということはわかっていただけましたでしょうか。

次に、sfObjectRouteというクラスを説明します。実際に開発する時はデータベースを使用すると思います。今までは商品に対するルーティングとして説明してきましたが、上記であればproductという商品のモデルが存在して、実際の画面ではその商品モデルに対する処理をアクションに記述していくことになると思います。
このsfObjectRouteというのは、実際のルーティングにオブジェクトを関連づけることが出来るsfRequestRouteです。symfonyではDoctrineとPropelという2つのORMが標準で採用されており、sfObjectRouteを継承したsfDoctrineRouteとsfPropelRouteというオブジェクトをそれぞれ使用しているORMにあわせて記述していくことになります。


// apps/frontend/config/routing.yml
# 商品詳細
product_show:
  class:    sfDoctrineRoute
  url:      /product/:id
  param:    { module: product, action: show }
  options:  { model: Product, type: object }
  requirements:
    id: \d+
    sf_method: [get]
# 商品編集入力
product_edit:
  class:    sfDoctrineRoute
  url:      /product/:id/edit
  param:    { module: product, action: edit }
  options:  { model: Product, type: object }
  requirements:
    id: \d+
    sf_method: [get]
# 商品編集実行
product_update:
  class:    sfDoctrineRoute
  url:      /product/:id
  param:    { module: product, action: update }
  options:  { model: Product, type: object }
  requirements:
    id: \d+
    sf_method: [put]
# 商品削除
product_delete:
  class:    sfDoctrineRoute
  url:      /product/:id
  param:    { module: product, action: delete }
  options:  { model: Product, type: object }
  requirements:
    id: \d+
    sf_method: [delete]

urlに:idという項目がありますが、:(コロン)で始まる部分は変数として扱われ、この場合だとidというリクエストパラメータが含まれる、ということです。
/product/1/editのようにアクセスしたときに、$request->getParameter('id')とやると1が返ってきます。
requirementsにid: \d+という指定が入っていますが、これは正規表現でidは数値のみ許可という指定を行っています。
ちなみに条件に引っかからない場合は404となります。

sfObjectRouteを使用する際に必ず必要になる項目は、optionsという部分です。ここでmodelとtypeを指定する必要があります。
modelはそのまんま、対応するモデル名です。sfDoctrineRouteでは指定されたモデル名に対し、URIに含まれる変数部分を指定したDoctrine_Queryを動的に作成してオブジェクトを取得する、ということを行ってくれます。その際に単一のオブジェクトが取得できる場合はtypeがobjectとなり、複数のオブジェクトが取得できる場合はtypeがlistとなります。

sfDoctrineRouteで動的にオブジェクトを取得する方法を書いてみます。


<?php
// apps/frontend/modules/product/actions/productActions.class.php
class productActions extends sfActions
{
  public function executeIndex(sfWebRequest $request)
  {
    // type: list の場合
    $this->products = $this->getRoute()->getObjects();
  }
  public function executeShow(sfWebRequest $request)
  {
    // type: object の場合
    $this->product = $this->getRoute()->getObject();
  }
}

上記ではindexがtype:list、showがtype:objectとなっています。それぞれまず、$this->getRoute()を実行し、現在のルーティングに基づいたsfRouteオブジェクトを取得しています。
そのオブジェクトに対し、type:listであればgetObjects()、type:objectであればgetObject()というメソッドを実行してオブジェクトを取得します。type:listの時にgetObject()を実行すると例外が発生するので注意してください。

そろそろ疲れてきましたが、あともう1つ説明させてください。次はsfObjectRouteCollectionです。名前からわかるかと思いますが、sfObjectRouteのコレクションを提供するクラスです。
今までrouting.ymlに記述していた内容を毎回、モデルごとに記述するのは非常に面倒だとは思いませんか?このsfObjectRouteCollectionは、今まで説明してきたCRUDに関するルーティングを一括で指定してくれる素晴らしいクラスです。これもsfObjectRouteと同様、sfDoctrineRouteCollectionとsfPropelRouteCollectionがそれぞれ用意されています。


// apps/frontend/config/routing.yml
# 商品に関するルールを一括指定
product:
  class:    sfDoctrineRouteCollection
  options:  { model: Product }

上記の3行をrouting.ymlに記述するだけで、モデルに対する一連のCRUDのルーティングをまとめて定義してくれます。

ルーティング名 URI sf_method action type
product /product GET index list
product_new /product/new GET new object
product_create /product POST create object
product_show /product/:id GET show object
product_edit /product/:id/edit GET edit object
product_update /product/:id PUT update object
product_delete /product/:id DELETE delete object

さらに、URIのプレフィックス(通常は一番上の項目:上記だとproduct)を変更できたり、アクションの指定や、上記以外のアクションの追加なども簡単にできるようになっています。


//apps/frontend/config/routing.yml
product:
  class:    sfDoctrineRouteCollection
  options:
    module                myproduct
    model:                Product
    prefix_path:          /myproducts
    actions:              [ list, show, edit, update ]
    collection_actions:   { export: get }
    object_actions:       { publish: put }

上記の補足をすると、prefix_pathを上記の用に/myproductと指定した場合は/myproduct/:id/editのようになります。actionsは有効にするアクションで、デフォルトでは上記の表が全て有効になっています。またwith_show: falseと指定するとデフォルトのものからshowだけ除いた指定となります。注意して欲しいのが、indexがlistという名前で扱われていることです。仕様ですがややこしいですね。
collection_actionsとobject_actionsはそれぞれtype:listなルーティングとtype:objectなルーティングを定義する項目です。アクション名=>sf_method形式で指定します。

このようにルーティング側で定義すると、アクション側でメソッドの判別を行ったりオブジェクトの取得を行ったりする手間が省けますのでアクションが簡潔になります。

尚、ルーティングの指定を行う場合はデフォルトのルーティングは削除しましょう。折角sf_methodなどを指定していてもデフォルトのルーティングが有効の状態ではproduct/delete/id/1の用にアクセスされてしまいますし、マッチしたルーティングがsfObjectRouteでない場合はそもそも$this->getRoute()->getObject()などがエラーになってしまいます。

既にお腹いっぱい書きましたが、まだまだ続きます。

◆ FormとURIの指定方法

最初の方で、sf_methodの指定方法について少し触れましたが、今回はそのあたりを交えてURLをどのように作成していくかということを書いていきます。

一番シンプルなものはurl_forヘルパーです。


<?php
// apps/frontend/modules/product/actions/actions.class.php
class productActions extends sfActions
{
  public function executeEdit(sfWebRequest $request)
  {
    $this->form = new ProductForm($this->getRoute()->getObject());
  }
}
?>


// apps/frontend/modules/product/templates/editSuccess.php
<?php echo $form->renderFormTag(url_for('product_update', $this->form->getObject())) ?>
  <!-- // ... -->
</form>

symfony 1.2ではurl_forの第1引数にルーティング名、第2引数にオブジェクトを渡した場合、自動的に必要な変数をオブジェクトから取得するようになっています。ただしこれはtype:objectのルーティングのみ有効です。

さて、上記の例ではproduct_updateにデータを送信しようとしています。この場合はsf_methodをputにしなければいけませんが、その指定は記述していません。
実はこの場合は何も指定しなくても自動的にPUTになります。それは、オブジェクトが既にDB上に存在するデータである場合はsfFormDoctrineもしくはsfFormPropelが自動的にメソッドをPUTにするようになっているためです。それ以外でも、


<?php
form_tag('#', array('method' => 'put'));
$form->renderFormTag('#', array('method' => 'delete'))

form_tagやsfForm::renderFormTagの引数にmethodを指定することでも、sf_methodを変更できます。


<?php
link_to('DELETE!', 'product_delete', $product, array('method' => 'delete', 'confirm' => 'Are you sure?')) ?>

次はlink_toを使った場合の例です。こちらも先ほどのurl_forと同様の使い方が出来ます。またDELETEなので第4引数でmethodを指定しています。

symfony 1.1までは、URIのクエリ形式でパラメータを指定していたと思いますが、こちらの方法も使用できます。
ただしこの場合は、module/actionのような形式で指定するか、@route_nameのようにアットマークをつける必要があります。


<?php
url_for('@product_edit?id=' . $product->getId());
url_for('product/show?id=' . $product->getId());
// 以下はエラー
url_for('product_edit?id=' . $product->getId());
url_for('@product_show', $product);

button_toヘルパーも同じように出来るかと思ったのですが、button_toに関してはオブジェクトを渡す方法は対応していませんでした。link_to_ifなんかも対応してなかったと思います。その場合はsymfony 1.1までの文字列形式で指定しましょう。
button_toやlink_to_ifは、url_forやlink_toのようにそのまま引数にオブジェクトを渡す方法では動作しません。


<?php
button_to('Edit', array('sf_route' => 'product_edit', 'sf_subject' => $product));
link_to_if($condition, 'Edit', 'product_edit', array('sf_subject' => $product));

上記のように、配列にsf_subjectというキーで指定する必要があります。
(※コメントにて、button_toやlink_to_ifにオブジェクトを渡す方法についてご指摘いただいたので修正しました。)

この他にも、アクション内で指定する方法もあります。


<?php
// apps/frontend/modules/product/actions/actions.class.php
class productActions extends sfActions
{
  public function executeCreate(sfWebRequest $request)
  {
    $this->form = new ProductForm();
    if ($this->form->bindAndSave()) {
      $this->redirect($this->generateUrl('product_show', $this->form->getObject()));
    } else {
      $this->setTemplate('edit');
    }
  }
}

generateUrlメソッドがそれにあたります。が、このメソッドはsymfony 1.2の名前つきルーティングのみ受け付ける仕様となっているようです。
やはりヘルパーを使うのが一番楽なように感じます。


<?php
sfContext::getInstance()->getConfiguration()->loadHelpers('Url');
url_for(...);

上記のようにsfContextからloadHelpersをする方法ならどこででも使えるので、ViewやAction以外から使う場合は上記の方法を使いましょう。

◆ ルーティングに関連するオブジェクトを取得

ルーティングはそれぞれがsfRouteオブジェクトという説明をしてきましたが、symfony内部ではこのsfRouteをまとめたsfRoutingというオブジェクトが存在しています。
sfRoutingは、routing.ymlにあるルーティング内容を全て保持しており、また現在のルーティング名なども保持しています。


<?php
// sfRoutingを取得
$routing = sfContext::getInstance()->getRouting();
// 現在のルーティング名を取得
$routing->getCurrentRouteName();
// 全てのsfRouteの配列を取得
$routing->getRoutes();
// 現在のsfRouteを取得
sfContext::getInstance()->getRequest()->getAttribute('sf_route');

◆ sf_formatの指定

sfDoctrineRouteCollectionで定義した場合に、例えばindexだと実際には以下のように定義されています。


product:
  url:      /product.:sf_format
  class:    sfDoctrineRoute
  options:  { model: product, type: list }
  param:    { module: product, action: index, sf_format: html }
  requirements: { sf_method: get }

sf_formatという変数パラメータが定義されており、デフォルトでhtmlが指定されています。これは実際に実行してみるとすぐわかるのですが、/product.htmlとアクセスした場合、/productとアクセスした場合と全く同じ表示がされるようになっています。例えば/product.jsとしてアクセスしてみると、JavaScriptのコメントアウトされた状態でエラーメッセージが返ってくるかと思います。

このように、出力のフォーマットの切り替えを行うのがsf_formatです。ここからはルーティングからは離れるので細かく説明はしませんが、indexSuccess.js.phpというファイルを作ればsf_formatにjsを指定した場合に自動的にそちらが読み込まれるようになっています。レスポンスヘッダも自動で変えてくれるようです。

◆ ルーティングの一覧を取得

ルーティングの一覧を取得するコマンドが用意されています。


$ symfony app:routes AppName

◆ 複数のアプリケーションをまたいだリンク

これで最後です。先日symfonyの公式ブログにCross Application Linksというブログがポストされました。これをざっくり紹介しようと思ったのですが、日本語訳が今日コミットされていたのでそちらへのリンクを張るだけにしようかと思います。

アプリケーション間のリンクを作る方法

駆け足な上にまとまりがなくなってしまいましたが、以上になります。
僕はsfDoctrineRouteCollectionを継承して自前のRouteCollectionを作成したりしていたのですが、オブジェクトになったことで非常に色々出来るようになったなあと感じました。
あと注意したい点としては、実際にURIがどのルーティングとマッチするかを検索するのにループで検索しているため、アクセスが多い部分をrouting.ymlの上の方へ持って行くと良いかなと思います。
おかしい部分などありましたらツッコミお待ちしております。