5日目: ルーティング
4日目を完璧にこなしているなら、MVC パターンに慣れてきて、コーディング方法がより自然に感じるようになっていることでしょう。もっと時間をかけて学ぶことで、振り返らないようになるでしょう。昨日のチュートリアルで、Jobeet のページデザインや処理のカスタマイズをし、レイアウトやヘルパー、スロットといった symfony のコンセプトについても見直しました。
今日は、symfony のルーティングフレームワークのすばらしい世界に飛び込みましょう。
~URL~
Jobeet ホームページ上の求人情報をクリックすると、URL は /job/show/id/1
のように表示されます。もし PHP で Web サイトの開発をしたことがあるなら、おそらく /job.php?id=1
という URL を見慣れているでしょう。symfony はどうやって動作しているのでしょうか?symfony はどうやってこの URL を基本とするアクションを決めているのでしょうか?なぜ求人の id
は $request->getParameter('id')
で得ることができるのでしょうか?今日は、これらすべての問題の答えを見てゆきます。
しかしまず初めに、URL と URL が正確に指すものについて話します。Web コンテキスト上で、URL は Web リソースの一意性のある名前です。URL の先へ行くと、ブラウザに URL によって分類されているリソースを取得するように頼みます。そして URL は Web サイトとユーザー間のインターフェイスとして、リソースが参照している意味のある情報を伝えます。しかし旧来の URL は実際にはリソースについての説明をしておらず、アプリケーションの内部構造を公開してしまっています。ユーザーは Web サイトが PHP で開発されているとか、求人情報がもつデータベースのある識別子というようなことはあまり気にしません。アプリケーションの内部動作を公開することは~セキュリティ~の観点から見ても、非常にまずいことです。ユーザーが URL 先にアクセスすることなくリソースを予想することができたらどうだろうか?開発者は適切な方法でアプリをセキュアすべきで、機密情報は隠したほうがよいです。 URL は symfony でフレームワーク全体を管理するのに重要なものです。これは~ルーティング~ (routing) フレームワークで管理します。ルーティングは内部 URI と外部 URL を管理します。リクエストを受け取るとき、ルーティングは URL を解析して内部URIに変換します。
求人ページの内部 URI はすでに indexSuccess.php
テンプレートで見ています:
'job/show?id='.$job->getId()
~url_for()
ヘルパー~はこの内部 URI を適切な URL に変換します:
/job/show/id/1
内部 URI はいくつかのパーツから構成されます。job
はモジュール名で、show
はアクション名、その後にアクションに渡すパラメータをクエリ文字列として追加します。内部 URI の一般的なパターンを下記に示します:
MODULE/ACTION?key=value&key_1=value_1&...
symfony のルーティングは2つの処理方法があるので、技術的実装を変更することなく URL を変換することができます。このことは Front Controller デザインパターンの主な利点の1つです。
ルーティングコンフィギュレーション
内部 URI と外部 URL 間のマッピングは ~routing.yml
~ ファイルで行われます:
[yml]
# apps/frontend/config/routing.yml
homepage:
url: /
param: { module: default, action: index }
default_index:
url: /:module
param: { action: index }
default:
url: /:module/:action/*
routing.yml
はルートについて記述されています。ルートは名前 (homepage
)、パターン (/:module/:action/*
) といくつかのパラメータ (param
キー下の値) をもちます。
リクエストが来るとき、URL から得られるパターンにマッチするかを試します。routing.yml
のなかの最初にマッチするルートが重要となります。このルーティングの動作を理解するためにもっとたくさんの例を見ることにしましょう。
/job
の URL をもつ Jobeet ホームページにリクエストをすると、マッチする最初のルートは default_index
です。パターンのなかでは、コロン (:
) を~プレフィックス~とする単語が変数であり、/:module
パターンは「/
の後にマッチする何か」ということを意味します。この例のなかでは、module
変数は値として job
をもちます。この値は $request->getParameter('module')
で取得することができます。このルートは action
変数にはデフォルト値が定義されます。
よってこのルートにマッチするすべてのの URL のリクエストは action
パラメータには index
という値をもつようになります。
もし /job/show/id/1
ページにリクエストするなら、symfony は最後のパターンである (/:modules/:action/*
) にマッチします。
パターン内ではスター (*
) はスラッシュ (/
) で区切られる変数と値のペアの一群にマッチします:
| リクエストパラメータ | 値 | | --------------------- | ---- | | module | job | | action | show | | id | 1 |
NOTE ~
module
~、~action
~ 変数は実行するアクションを決めるため、symfony によって使われる特別なものです。
URL の /job/show/id/1
は下記で使われている url_for()
ヘルパーによってテンプレートから作られます:
[php]
url_for('job/show?id='.$job->getId())
@
をプレフィックスとするルート名も使えます:
[php]
url_for('@default?module=job&action=show&id='.$job->getId())
上記2つは同じものですが、後者の方がすべてのルートを解析することなくベストなマッチングをするため速く動作しますし、実装する上でもより少ないコードになります (モジュール名、アクション名を内部 URI 内に含まないので)。
ルートのカスタマイズ
今のところ、ブラウザで URL の /
にリクエストすると、symfony のデフォルトの初期ページになります。その理由はこのURLが homepage
~ルート~にマッチするからです。しかし Jobeet のホームページとしての役割を果たすように変更します。変更するには、homepage
ルートの module
変数の値を job
に変更します:
[php]
# apps/frontend/config/routing.yml
homepage:
url: /
param: { module: job, action: index }
homepage
ルートを使うように、レイアウトのなかの Jobeet ロゴのリンクを変更します:
[php]
<!-- apps/frontend/templates/layout.php -->
<h1>
<a href="<?php echo url_for('homepage') ?>">
<img src="/images/logo.jpg" alt="Jobeet Job Board" />
</a>
</h1>
簡単でした!
TIP ルーティングコンフィギュレーションを更新するとき、開発環境では変更は即座に反映されます。しかし運用環境でもこれらを動かすには、キャッシュをクリアする必要があります。
もう少し内容を含めるために、求人ページの URL をより意味のある文字列に変更してみましょう:
/job/sensio-labs/paris-france/1/web-developer
Jobeet についての知識がなくても、ページをみなくても、URL から Sensio Labs がフランスのパリで Web 開発者を探しているということがわかります。
NOTE わかりやすい URL はユーザーに情報を伝える上で重要となります。メールのなかで URL をコピペしたり、検索エンジンに合わせて自分の Web サイトを最適化するのに役立ちます。
URL を下記のようなパターンにマッチさせます:
/job/:company/:location/:id/:position
routing.yml
ファイルを編集し、ファイルの冒頭に job_show_user
ルートを追加します:
[yml]
job_show_user:
url: /job/:company/:location/:id/:position
param: { module: job, action: show }
Jobeet ホームページをリフレッシュしても、求人情報へのリンクは変更されません。ルートを生成するなら、必要な変数をすべて渡すことが必要となります。ですので、indexSuccess.php
のなかで呼び出される url_for()
を変更する必要があります。
[php]
url_for('job/show?id='.$job->getId().'&company='.$job->getCompany().
'&location='.$job->getLocation().'&position='.$job->getPosition())
~内部 URI~ は配列として表すこともできます:
[php]
url_for(array(
'module' => 'job',
'action' => 'show',
'id' => $job->getId(),
'company' => $job->getCompany(),
'location' => $job->getLocation(),
'position' => $job->getPosition(),
))
要件
初日のチュートリアルのあいだ、よい結果をもたらすバリデーションとエラーハンドリングについて話しました。ルーティングシステムは組み込みの~バリデーション~機能をもちます。各パターンの変数は~ルート~定義のなかの ~requirements
|要件~ エントリを使って正規表現によるバリデーションができます。
[yml]
job_show_user:
url: /job/:company/:location/:id/:position
param: { module: job, action: show }
requirements:
id: \d+
上記の requirements
エントリは id
が数値であることを強制しています。もし数値でなければルートにはマッチしません。
ルートクラス
~routing.yml
~ で定義されている各ルートは内部で sfRoute
オブジェクトに変換されます。このクラスはルート定義の class
エントリで定義することで変更可能です。~HTTP~ プロトコルをよく知っているのなら、~GET|GET (HTTP メソッド)~
、~POST|POST (HTTP メソッド)~
、~HEAD|HEAD (HTTP メソッド)~
、~DELETE|DELETE (HTTP メソッド)~
と ~PUT|PUT (HTTP メソッド)~
のようなメソッドを定義することもできます。
最初の3つ (GET
、POST
、HEAD
) はすべてのブラウザでサポートされますが、それ以外の2つ (DELETE
、PUT
) はサポートされていません。
特定のリクエストメソッドだけにマッチするようルートを制限するには、sfRequestRoute
クラスを使うようにルートクラスを変更して、sf_method
変数を requirements
エントリに追加できます:
[yml]
job_show_user:
url: /job/:company/:location/:id/:position
class: sfRequestRoute
param: { module: job, action: show }
requirements:
id: \d+
sf_method: [get]
NOTE ~HTTP メソッド~にのみマッチするルートを要求することは、アクションで
sfWebRequest::isMethod()
を使うこととは全体的に同じではありません。メソッドが要求されたルートにマッチしない場合、ルーティングはマッチするルートを探し続けるからです。
オブジェクトルートクラス
求人用の新しい内部 URI はとても長くて書くのが退屈ですが (url_for('job/show?id='.$job->getId().'&company='.$job->getCompany().'&location='.$job->getLocation().'&position='.$job->getPosition())
)、前の節で学んだように、ルートクラスは変更できます。job_show_user
ルートに関しては、~sfPropelRoute
~ を使うほうがよいです。このクラスが ##ORM## オブジェクトもしくは ##ORM## オブジェクトのコレクションを表すルートに合わせて最適化されているからです:
[yml]
job_show_user:
url: /job/:company/:location/:id/:position
class: sfPropelRoute
options: { model: JobeetJob, type: object }
param: { module: job, action: show }
requirements:
id: \d+
sf_method: [get]
options
エントリはルートのふるまいをカスタマイズします。ここでは、model
オプションはルートに関係する ##ORM## モデルクラス (JobeetJob
) を定義し、type
オプションでは、このルートに関係するオブジェクトを定義します (オブジェクトの一群を示すなら list
も使えます)。
job_show_user
ルートは JobeetJob
オブジェクトの関係を知らないので、~url_for()
ヘルパー~で呼び出すのは簡単です:
[php]
url_for(array('sf_route' => 'job_show_user', 'sf_subject' => $job))
もしくは単に次のように記述します:
[php]
url_for('job_show_user', $job)
NOTE オブジェクト以外に複数の引数を渡すことが必要な際に最初の例が役に立ちます。
ルートのなかのすべての変数は JobeetJob
クラスのアクセサと対応して動きます (たとえば、company
ルートの変数は getCampany()
の値に置き換わります)。
生成された URL を見ると、これらはまだ完全にほしい URL にはなっていません:
http://jobeet.localhost/frontend_dev.php/job/Sensio+Labs/Paris%2C+France/1/Web+Developer
すべての ASCII ではない文字をハイフン (-
) に置き換えることで、カラム値の値を 「~slugify|スラッグ~」処理 する必要があります。JobeetJob
ファイルを開き、次のメソッドをクラスに追加してください:
[php]
public function getPositionSlug()
{
return Jobeet::slugify($this->getPosition());
}
public function getLocationSlug()
{
return Jobeet::slugify($this->getLocation());
}
それから、lib/Jobeet.class.php
ファイルを作り、slugify
メソッドを追加します:
[php]
// lib/Jobeet.class.php
class Jobeet
{
static public function slugify($text)
{
// 文字ではないもしくは数値ではないものすべてを - に置き換える
$text = preg_replace('/\W+/', '-', $text);
// トリムして小文字に変換する
$text = strtolower(trim($text, '-'));
return $text;
}
}
NOTE このチュートリアルでは、スペースを最適化してツリーを節約するために純粋な PHP コードのみを含むコードの例では開きの
<?php
ステートメントを示しません。新しい PHP ファイルを作るとき、このステートメントを必ず追加することを覚えておいてください。
「バーチャルな」3つの新しいアクセサ: getCompanySlug()
、getPositionSlug()
、と getLocationSlug()
を定義しました。これらは slugify()
メソッドに適用した後で対応するカラムの値を返します。job_show_user
ルートでこれらのバーチャルアクセサで実際のカラムの名前を置き換えることができます:
[yml]
job_show_user:
url: /job/:company_slug/:location_slug/:id/:position_slug
class: sfPropelRoute
options: { model: JobeetJob, type: object }
param: { module: job, action: show }
requirements:
id: \d+
sf_method: [get]
これで期待する URL が利用できるようになります:
http://jobeet.localhost/frontend_dev.php/job/sensio-labs/paris-france/1/web-developer
しかしこれは話の半分です。ルートはオブジェクトに基づいて URL を生成できますが、渡された URL に関連するオブジェクトを見つけることもできます。関連オブジェクトはルートオブジェクトの getObject()
メソッドで読み取ることができます。やってくるリクエストを解析する際に、ルーティングはアクションで使うためにマッチするルートオブジェクトを保存します。Jobeet
オブジェクトを読み取るためにルートオブジェクトを使う executeShow()
メソッドを変更します:
[php]
class jobActions extends sfActions
{
public function executeShow(sfWebRequest $request)
{
$this->job = $this->getRoute()->getObject();
$this->forward404Unless($this->job);
}
// ...
}
もし未知の id
の求人情報を取得しようとするなら~404エラー~ページが表示されますが、エラーメッセージが変更されているでしょう:
この理由は404エラーが getRoute()
メソッドによって自動的に投げられるからです。ですので executeShow()
メソッドはもっと単純にできます:
[php]
class jobActions extends sfActions
{
public function executeShow(sfWebRequest $request)
{
$this->job = $this->getRoute()->getObject();
}
// ...
}
TIP もしルートで404エラーを作りたくなければ、
allow_empty
ルーティングオプションをtrue
にセットできます。
-
NOTE ルートに関連するオブジェクトは遅延ロードされます。
getRoute()
メソッドを呼び出す場合、データベースからのみ読み取られます。
アクションとテンプレートにおけるルーティング
テンプレートでは url_for()
ヘルパーは内部 URI を外部 URL に変換します。その他の symfony ヘルパーのなかにも、引数として内部 URI をもつものがあります。<a>
タグを生成する ~link_to()
ヘルパー~がその1つです:
[php]
<?php echo link_to($job->getPosition(), 'job_show_user', $job) ?>
下記のような HTML コードを生成します:
[php]
<a href="/job/sensio-labs/paris-france/1/web-developer">Web Developer</a>
url_for()
と link_to()
の両方とも絶対パスでも URL を生成できます:
[php]
url_for('job_show_user', $job, true);
link_to($job->getPosition(), 'job_show_user', $job, true);
アクションから URL を生成したいなら、generateUrl()
メソッドを使います:
[php]
$this->redirect($this->generateUrl('job_show_user', $job));
SIDEBAR 「~redirect|リダイレクト~」 メソッドファミリ
昨日のチュートリアルでは、「forward」メソッドを話しました。これらのメソッドはブラウザでの往復なしに現在のリクエストを別のアクションに転送します。
「redirect」メソッドはユーザーを別の URL に転送します。転送に関しては、
redirect()
メソッド、もしくはredirectIf()
とredirectUnless()
ショートカットメソッドを利用できます。
コレクションルートクラス
job
モジュールに関して、show
アクションのルートはすでにカスタマイズしていますが、その他のメソッド (index
、 new
、edit
、create
、update
、delete
) の URL はまだ default
ルートで管理されています:
[yml]
default:
url: /:module/:action/*
default
ルートは多くのルートを定義しなくても、コーディングを始められるすばらしい方法です。しかし「すべてのアクションをキャッチ」してしまうので固有の設定が必要でも設定できません。
すべての job
アクションは JobeetJob
モデルクラスに関連しており、show
アクションに対してすべて行っているので、それぞれに対してカスタムの ~sfPropelRoute
~ ルートを簡単に定義できます。
job
モジュールはモデルのために古典的な7つのアクションを定義するので、~sfPropelRouteCollection
~ クラスも使えます。
routing.yml
ファイルを開き、次のように修正します:
[yml]
# apps/frontend/config/routing.yml
job:
class: sfPropelRouteCollection
options: { model: JobeetJob }
job_show_user:
url: /job/:company_slug/:location_slug/:id/:position_slug
class: sfPropelRoute
options: { model: JobeetJob, type: object }
param: { module: job, action: show }
requirements:
id: \d+
sf_method: [get]
# default rules
homepage:
url: /
param: { module: job, action: index }
default_index:
url: /:module
param: { action: index }
default:
url: /:module/:action/*
上記の job
ルートは実際には下記に示す7つの sfPropelRoute
ルートを自動的に生成します:
[yml]
job:
url: /job.:sf_format
class: sfPropelRoute
options: { model: JobeetJob, type: list }
param: { module: job, action: index, sf_format: html }
requirements: { sf_method: get }
job_new:
url: /job/new.:sf_format
class: sfPropelRoute
options: { model: JobeetJob, type: object }
param: { module: job, action: new, sf_format: html }
requirements: { sf_method: get }
job_create:
url: /job.:sf_format
class: sfPropelRoute
options: { model: JobeetJob, type: object }
param: { module: job, action: create, sf_format: html }
requirements: { sf_method: post }
job_edit:
url: /job/:id/edit.:sf_format
class: sfPropelRoute
options: { model: JobeetJob, type: object }
param: { module: job, action: edit, sf_format: html }
requirements: { sf_method: get }
job_update:
url: /job/:id.:sf_format
class: sfPropelRoute
options: { model: JobeetJob, type: object }
param: { module: job, action: update, sf_format: html }
requirements: { sf_method: put }
job_delete:
url: /job/:id.:sf_format
class: sfPropelRoute
options: { model: JobeetJob, type: object }
param: { module: job, action: delete, sf_format: html }
requirements: { sf_method: delete }
job_show:
url: /job/:id.:sf_format
class: sfPropelRoute
options: { model: JobeetJob, type: object }
param: { module: job, action: show, sf_format: html }
requirements: { sf_method: get }
NOTE
sfPropelRouteCollection
で生成されるいくつかのルートは同じ ~URL~ をもちます。それらはリクエストされる ~HTTP メソッド~がすべて異なるので、使うことができます。
job_delete
と job_update
ルートが必要とする ~HTTP メソッド~はブラウザでサポートされていません (~DELETE|DELETE (HTTP メソッド)~
と ~PUT|PUT (HTTP メソッド)~
)。この動作は symfony がシミュレートしているので動きます。具体例を見るために _form.php
テンプレートを開いてください:
[php]
// apps/frontend/modules/job/templates/_form.php
<form action="..." ...>
<?php if (!$form->getObject()->isNew()): ?>
<input type="hidden" name="sf_method" value="PUT" />
<?php endif; ?>
<?php echo link_to(
'Delete',
'job/delete?id='.$form->getObject()->getId(),
array('method' => 'delete', 'confirm' => 'Are you sure?')
) ?>
特別な sf_method
パラメータを渡すことで、すべての symfony ヘルパーに望む HTTP メソッドをシミュレートするように指示できます。
NOTE これ以外にも symfony は
sf_method
のようなsf_
を~プレフィックス~とする固有のパラメータをもちます。上記のルート生成の中で、別のパラメータが見れます。これはsf_format
であり、次の日に説明します。
ルートのデバッグ
コレクションルートを使うなら、生成されるルートの一覧の表示が役に立つことがあります。app:routes
タスクはアプリケーションから得られたすべてのルートを出力します:
$ php symfony app:routes frontend
引数にルート名を追加することで、指定したルートに関するたくさんの~デバッグ~情報を取得できます:
$ php symfony app:routes frontend job_edit
デフォルトルート
すべての URL に対して~ルート~を定義するのはよい習慣です。job
ルートは Jobeet アプリケーションを記述するために必要なすべてのルールを定義するので、routing.yml
設定ファイルからデフォルトのルートを削除もしくはコメントアウトします:
[yml]
# apps/frontend/config/routing.yml
#default_index:
# url: /:module
# param: { action: index }
#
#default:
# url: /:module/:action/*
Jobeet アプリケーションは以前と同じように動作します。
また明日
今日はたくさんの情報を詰め込みました。symfony のルーティングフレームワークの使い方と URL を技術的な実装から分離する方法を学びました。
明日は、新しい概念を紹介しませんが、これまでカバーしてきたことをより深く追求することに時間をかけます。
ORM