7.5. 標準のルータ: Zend_Controller_Router_Rewrite

7.5.1. 導入

Zend_Controller_Router_Rewrite は、標準のルータです。 ルーティングとは、URI (ベース URL から取得した URI の一部) を展開し、どのコントローラのどのアクションが リクエストを処理するのかを決める処理のことです。 モジュールやコントローラ、アクション、そしてその他のパラメータが Zend_Controller_Request_Http オブジェクトにまとめられます。 このオブジェクトを処理するのが Zend_Controller_Dispatcher_Standard です。 ルーティングが行われるのは一度だけ、すなわちリクエストを最初に受け取ってから 最初のコントローラに処理が渡される際だけです。

Zend_Controller_Router_Rewrite は、mod_rewrite 風の機能を PHP だけで実現できるように設計されています。 この処理は Ruby on Rails のルーティングを多少参考にしており、 ウェブサーバの URL 書き換えに関する前提知識を必要としません。 以下の単純な mod_rewrite ルール (のいずれか) で動作するように設計されています。

RewriteEngine on
RewriteRule !\.(js|ico|gif|jpg|png|css)$ index.php

あるいは

RewriteEngine on
RewriteCond %{SCRIPT_FILENAME} !-f
RewriteCond %{SCRIPT_FILENAME} !-d
RewriteRule ^(.*)$ index.php/$1 

Rewrite ルータを IIS ウェブサーバで使用するには Isapi_Rewrite を Isapi 拡張モジュールとしてインストールします。そして次のようなルールを記述します。

RewriteRule ^[\w/\%]*(?:\.(?!(?:js|ico|gif|jpg|png|css)$)[\w\%]*$)? /index.php [I]
[注意] IIS Isapi_Rewrite

IIS を使用すると、$_SERVER['REQUEST_URI'] が存在しないか空の文字列に設定されます。このような場合、 Zend_Controller_Request_Http$_SERVER['HTTP_X_REWRITE_URL'] の値を使用します。これは Isapi_Rewrite 拡張モジュールが設定します。

Lighttpd の場合は、次のようなルールを使用します。

url.rewrite-once = ( ".*\.(js|ico|gif|jpg|png|css)$" => "$0", "" => "/index.php")

7.5.2. ルータの使用法

Rewrite ルータを適切に使用するには、まずそのインスタンスを作成し、 次にユーザ定義のルーティングを追加し、それをコントローラに注入しなければなりません。 以下にコードの例を示します。

<?php
/* ルータを作成します */

$router = $ctrl->getRouter(); // デフォルトで rewrite ルータを返します
$router->addRoute(
    'user',
    new Zend_Controller_Router_Route('user/:username', array('controller' => 'user', 'action' => 'info'))
);

7.5.3. 基本的な RewriteRouter の操作法

RewriteRouter で最も重要なのが、ユーザ定義のルーティングです。 これは、RewriteRouter の addRoute メソッドをコールして追加します。 このメソッドに、Zend_Controller_Router_Route_Interface を実装したクラスの新しいインスタンスを渡します。

<?php
$router->addRoute('user', new Zend_Controller_Router_Route('user/:username'));

Rewrite ルータには、4 種類の基本的なルーティング方式があります (そのうちのひとつは特別なものです)。

これらのルーティングは、チェインやユーザ定義のルーティング方式を作成する際に何度も使用します。 任意の設定でお好みの数のルーティングを使用することができますが、 Module ルートだけは例外です。これを使用するのは一度だけで、 もっとも汎用的なルート (デフォルト) として使用します。 個々のルーティング方式については、後ほど詳細に説明します。

addRoute への最初のパラメータはルートの名前です。 これを使用して、ルータがルートを処理します。 たとえば URL の生成などに使用します。 二番目のパラメータはルート自身となります。

[注意] 注意

ルート名のもっとも一般的な使用例は、 Zend_View の url ヘルパーです。

<a href="<?= $this->url('user', array('username' => 'martel')) ?>">Martel</a>

これは user/martel へのリンクとなります。

ルーティング処理は、定義されたすべてのルートから リクエスト URI にマッチする定義を探すことによって行います。 マッチするものが見つかれば、ルートのインスタンスから変数の値が返され、 それを Zend_Controller_Request オブジェクトに注入します。 これを、後にディスパッチャやユーザが作成したコントローラで使用します。 マッチするものが見つからない場合は、チェイン内の次のルートを調べます。

[注意] 定義の順番

一番最後にマッチしたルートが適用されるので、 汎用的なルートは最初に定義するようにしましょう。

[注意] 返される値

ルーティングの結果返される値は、URL パラメータあるいは ユーザ定義のルータのデフォルト値です。これらの値は、後ほど Zend_Controller_Request::getParam() あるいは Zend_Controller_Action::_getParam() メソッドでアクセスできます。

ルートで使用される変数のうち、'module'、'controller' および 'action' の 3 つは特別な扱いとなります。これらの特殊変数は、Zend_Controller_Dispatcher がディスパッチ先のコントローラとアクションを決定するために使用されます。

[注意] 特殊変数

これらの特殊変数の名前を変更することもできます。その場合は Zend_Controller_Request_HttpsetControllerKey メソッドや setActionKey メソッドを使用します。

7.5.4. デフォルトのルート

Zend_Controller_Router_Rewrite がデフォルトのルートとして設定されています。 これは controller/action 形式の URI にマッチします。 さらに、パス要素の最初の部分にモジュール名を指定することができます。つまり module/controller/action のような URI も可能です。 また、URI にパラメータを追加した形式、つまり controller/action/var1/value1/var2/value2 のような URI にもデフォルトで対応しています。

ルータのマッチ処理についての例を示します。

// 以下の設定を前提とします
$ctrl->setControllerDirectory(
    array(
        'default' => '/path/to/default/controllers',
        'news'    => '/path/to/blog/controllers',
        'blog'    => '/path/to/blog/controllers'
    )
);

モジュールのみ
http://example/news
    module == news

無効なモジュール名は、コントローラ名として扱われます
http://example/foo
    controller == foo

モジュール + コントローラ
http://example/blog/archive
    module     == blog
    controller == archive

モジュール + コントローラ + アクション
http://example/blog/archive/list
    module     == blog
    controller == archive
    action     == list

モジュール + コントローラ + アクション + パラメータ
http://example/blog/archive/list/sort/alpha/date/desc
    module     == blog
    controller == archive
    action     == list
    sort       == alpha
    date       == desc

デフォルトのルートは、Zend_Controller_Router_Route_Module オブジェクトを 'default' という名前 (インデックス) で RewriteRouter に保存したものです。 これは、以下のようにして作成します。

<?php
$compat = new Zend_Controller_Router_Route_Module(array(), $dispatcher, $request);
$this->addRoute('default', $compat);

このデフォルトルートが不要な場合は、独自の 'デフォルト' ルートで上書きします (つまり、'default' という名前で保存します)。 あるいは、removeDefaultRoutes() で削除することもできます。

<?php
// すべてのデフォルトルートを削除します
$router->removeDefaultRoutes();

7.5.5. ベース URL およびサブディレクトリ

Rewrite ルータはサブディレクトリ (例. http://domain.com/~user/application-root/>) 内でも使用可能です。この場合、アプリケーションのベース URL (/~user/application-root) の自動検出が Zend_Controller_Request_Http によって行われ、適切に使用されます。

ベース URL の検出に失敗する場合は、 Zend_Controller_Request_Http のメソッド setBaseUrl() を使用してベースパスを上書き指定することができます (??? を参照ください)。

<?php
$request->setBaseUrl(/~user/application-root/);

7.5.6. ルートの型

7.5.6.1. Zend_Controller_Router_Route

Zend_Controller_Router_Route はフレームワークの標準のルートです。 簡単に利用でき、柔軟なルート定義が可能です。各ルートには、まず (静的および動的な) URL のマッピングが含まれ、 そしてデフォルト値および変数についての制限を指定して初期化します。

とある架空のアプリケーションで、コンテンツの作者情報のページが必要になったとしましょう。 ブラウザで http://domain.com/author/martel にアクセスした際に、"martel" とかいう人についての情報を見たいわけです。 この機能を実現するためのルートは、次のようになります。

<?php
$route = new Zend_Controller_Router_Route(
    'author/:username', 
    array(
        'controller' => 'profile', 
        'action'     => 'userinfo'
    )
);

$router->addRoute('user', $route);

Zend_Controller_Router_Route のコンストラクタの最初のパラメータは、ルートの定義です。 これを URL にマッチさせます。ルート定義は静的な部分と動的な部分で構成され、 それをスラッシュ ('/') で連結します。 静的な部分は単なるテキスト (例. author) です。 動的な部分を変数と呼び、変数名の前にコロンをつけて (例. :username) 表します。

[注意] 文字の使用法

現在の実装では、(スラッシュ以外の) 任意の文字を変数名として使用できます。しかし、 PHP の変数名として使用できる文字だけを用いることを強く推奨します。 このようにしておくことで、 将来実装が変更されたときにバグを引き起こす可能性を抑えられます。

この例のルートは、ブラウザで 'http://domain.com/author/martel' を指した際にマッチします。 この場合、すべての変数の値が Zend_Controller_Request オブジェクトに注入され、ProfileController からアクセスできるようになります。 この例が返す変数は、以下のようなキーと値のペアを持つ配列となります。

<?php
$values = array(
    'username'   => 'martel',
    'controller' => 'profile',
    'action'     => 'userinfo'
);

その後、Zend_Controller_Dispatcher は (デフォルトモジュールの) ProfileController クラスにある userinfoAction() メソッドを実行します。変数にアクセスするには、 Zend_Controller_Action::_getParam() あるいは Zend_Controller_Request::getParam() メソッドを使用します。

<?php
public function userinfoAction() 
{
    $request = $this->getRequest();
    $username = $request->getParam('username');

    $username = $this->_getParam('username');
}

ルート定義には、特殊文字 (ワイルドカード) を含めることができます。これは '*' 記号で表します。 これを使用して、Module ルートと同様にパラメータを扱う (変数名 => 値 のペアを URI で定義する) ことができます。 次のルートは、Module ルートの挙動をまねたものです。

<?php
$route = new Zend_Controller_Router_Route(
    ':module/:controller/:action/*', 
    array('module' => 'default')
);
$router->addRoute('default', $route);
7.5.6.1.1. 変数のデフォルト

ルートで使用するすべての変数についてデフォルト値を指定することができます。 これは、 Zend_Controller_Router_Route のコンストラクタの 2 番目のパラメータで指定します。 このパラメータは、変数名をキーとする配列で、 対応する値にそのデフォルト値を指定します。

<?php
$route = new Zend_Controller_Router_Route(
    'archive/:year', 
    array('year' => 2006)
);
$router->addRoute('archive', $route);

上のルートは 'http://domain.com/archive/2005' および 'http://example.com/archive' のような URL にマッチします。後者の場合、変数 year にはデフォルト値である 2006 が設定されます。

この例は、year 変数をリクエストオブジェクトに注入することになります。 そしてルーティング情報が存在しない (コントローラやアクションのパラメータが定義されていない) ので、 アプリケーションはデフォルトのコントローラのデフォルトアクションメソッド (ともに Zend_Controller_Dispatcher_Abstract で定義されています) にディスパッチします。より使いやすくするには、 ルートのデフォルトとしてコントローラとアクションを定義しておく必要があります。

<?php
$route = new Zend_Controller_Router_Route(
    'archive/:year', 
    array(
        'year'       => 2006, 
        'controller' => 'archive',
        'action'     => 'show'
    )
);
$router->addRoute('archive', $route);

このルートは、ArchiveControllershowAction() を実行します。

7.5.6.1.2. 変数の制約

Zend_Controller_Router_Route のコンストラクタの 三番目のパラメータで、変数の制約を指定することができます。 これは、正規表現で指定します。

<?php
$route = new Zend_Controller_Router_Route(
    'archive/:year', 
    array(
        'year'       => 2006, 
        'controller' => 'archive',
        'action'     => 'show'
    ),
    array('year' => '\d+')
);
$router->addRoute('archive', $route);

上の例のルートでは、year 変数の値が数値データである場合にのみ Rewrite ルータにマッチします。つまり http://domain.com/archive/2345 はマッチしますが http://example.com/archive/test はマッチしません。 この場合はチェイン内の次のルートに処理を移します。

7.5.6.2. Zend_Controller_Router_Route_Static

これまでの例では、すべて動的なルートを使用していました。 つまり、特定のパターンにマッチするものについてのルートです。 しかし、時には特定のルートを固定してしまい、 わざわざ正規表現エンジンを動かしたくない場合もあるでしょう。 そんなときには静的なルートを使用します。

<?php
$route = new Zend_Controller_Router_Route_Static(
    'login', 
    array('controller' => 'auth', 'action' => 'login')
);
$router->addRoute('login', $route);

上のルートは http://domain.com/login という URL にマッチし、AuthController::loginAction() にディスパッチされます。

7.5.6.3. Zend_Controller_Router_Route_Regex

デフォルトのルートや静的なルートに加えて、正規表現によるルートも使用可能です。 このルートは他のものに比べてより強力で柔軟なものですが、 多少複雑になってしまいます。そして、より高速になります。

標準のルートと同様、このルートを初期化する際にはルートの定義とデフォルトを指定する必要があります。 サンプルとして、archive ルートを作成してみましょう。 これは先ほど定義したものとほぼ同じですが、今回は Regex ルートを使用しています。

$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)', 
    array(
        'controller' => 'archive',
        'action'     => 'show'
    )
);
$router->addRoute('archive', $route);

定義された正規表現のパターンが、リクエストオブジェクトに注入されます。 上の例では、http://domain.com/archive/2006 がマッチした後の結果の値は次のような配列になります。

$values = array(
    1            => '2006',
    'controller' => 'archive',
    'action'     => 'show'
);
[注意] 注意

ルータとのマッチングを行う前に、URL の先頭と最後のスラッシュは取り除かれます。 結果として、URL http://domain.com/foo/bar/ は正規表現 foo/bar にマッチすることになります。 /foo/bar にはマッチしません。

[注意] 注意

行頭と行末を表す文字 (それぞれ '^' および '$') が、すべての式の前後に自動的に付加されます。 したがって、これらは正規表現で指定する必要はありません。

[注意] 注意

このルートクラスは、区切り文字として # を使用します。 つまり、ルート定義の中にハッシュ文字 ('#') がある場合は、それをエスケープする必要があるということです。 スラッシュ ('/') をエスケープする必要はありません。 '#' (アンカー) は通常はウェブサーバに渡されることはないので、 エスケープが必要になることはまずないでしょう。

定義されたサブパターンの内容は、通常通りの方法で取得できます。

public function showAction() 
{
    $request = $this->getRequest();
    $year    = $request->getParam(1); // $year = '2006';
}
[注意] 注意

このキーは、文字列 ('1') ではなく数値の 1 であることに注意しましょう。

このルートは、標準のルートとまったく同様に動作するわけではありません。 'year' のデフォルトが設定されていないからです。 また、year のデフォルトを設定してこれをオプション扱いにしたとしても、 最後のスラッシュをどうするかという問題が残ります。 これを解決するには、year 部をスラッシュを含めてオプションにし、 その数値部のみを取得するようにします。

$route = new Zend_Controller_Router_Route_Regex(
    'archive(?:/(\d+))?', 
    array(
        'year'       => '2006',
        'controller' => 'archive',
        'action'     => 'show'
    )
);
$router->addRoute('archive', $route);

まだ問題が残っていることにおそらくお気づきでしょう。 パラメータとして数値のキーを使用するのはなかなか難しく、 長い目で見れば問題を引き起こす可能性が高くなります。 そこで三番目のパラメータの登場です。 このパラメータは、正規表現サブパターンとパラメータ名のキーを関連付けます。 簡単な例を見てみましょう。

$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)', 
    array(
        'controller' => 'archive',
        'action' => 'show'
    ),
    array(
        1 => 'year'
    )
);
$router->addRoute('archive', $route);

この結果は次のようになり、これがリクエストオブジェクトに格納されます。

$values = array(
    'year'       => '2006',
    'controller' => 'archive',
    'action'     => 'show'
);

関連付けは両方の方法で定義でき、任意の環境 (例. Zend_Config) で動作します。 キーには変数名あるいはサブパターン番号のいずれかを含めることができます。

$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)', 
    array( ... ),
    array(1 => 'year')
);

// あるいは
        
$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)', 
    array( ... ),
    array('year' => 1)
);       
[注意] 注意

サブパターンのキーは整数値でなければなりません。

リクエストの値から数値キーが消え、代わりに名前がつけられたことに注目しましょう。 もちろん、お望みなら数値での指定と名前での指定を共用することもできます。

$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)/page/(\d+)', 
    array( ... ),
    array('year' => 1)
);       

この結果、リクエスト内には数値キーと名前つきキーが共存することになります。 たとえば、URL http://domain.com/archive/2006/page/10 は次のような値になります。

$values = array(
    'year'       => '2006',
    2            => 10,
    'controller' => 'archive',
    'action'     => 'show'
);

正規表現を簡単に反転させることはできないので、 url ヘルパーやこのクラスのメソッドを使用するには 逆の URL を準備しておく必要があります。 逆方向のパスは sprintf() 形式の文字列で表し、 コンストラクタの四番目のパラメータとして指定します。

$route = new Zend_Controller_Router_Route_Regex(
    'archive/(\d+)', 
    array( ... ),
    array('year' => 1),
    'archive/%s'
);       

これまで説明してきたことは、すべて標準のルートオブジェクトでも可能なことです。 それでは、Regex ルートを使用するメリットはいったい何なのでしょう? これを使用すると、あらゆる形式の URL を制約なしに定義することができます。 仮に、あなたが blog を持っており http://domain.com/blog/archive/01-Using_the_Regex_Router.html のような URL を作成したいと考えたとしましょう。 このパスの最後の要素 01-Using_the_Regex_Router.html から記事の ID とタイトル/説明 を取得するにはどうしたらいいでしょうか? 標準のルートでは不可能でしょう。Regex ルートを使用した場合は、 次のようにすることができます。

$route = new Zend_Controller_Router_Route_Regex(
    'blog/archive/(\d+)-(.+)\.html',
    array(
        'controller' => 'blog', 
        'action'     => 'view'
    ), 
    array(
        1 => 'id', 
        2 => 'description'
    ),
    'blog/archive/%d-%s.html'
);
$router->addRoute('blogArchive', $route);

regex ルートは標準のルートよりはるかに柔軟性があるということが、 ここからもわかります。

7.5.7. RewriteRouter での Zend_Config の使用法

新しいルートを追加する際に、 いちいちコードを書き換えるのではなく設定ファイルの変更で対応できると便利でしょう。 そんなときには addConfig() メソッドを使用します。基本的な使用法は、 まず Zend_Config 互換の設定を作成し、それをコードに読み込み、 そして RewriteRouter に渡すことです。

例として、次のような INI ファイルを考えてみましょう。

[production]
routes.archive.route = "archive/:year/*"
routes.archive.defaults.controller = archive
routes.archive.defaults.action = show
routes.archive.defaults.year = 2000
routes.archive.reqs.year = "\d+"

routes.news.type = "Zend_Controller_Router_Route_Static"
routes.news.route = "news"
routes.news.defaults.controller = "news"
routes.news.defaults.action = "list"

routes.archive.type = "Zend_Controller_Router_Route_Regex"
routes.archive.route = "archive/(\d+)"
routes.archive.defaults.controller = "archive"
routes.archive.defaults.action = "show"
routes.archive.map.1 = "year" 
; あるいは: routes.archive.map.year = 1

上の INI ファイルを、次のようにして Zend_Config オブジェクトに読み込みます。

$config = new Zend_Config_Ini('/path/to/config.ini', 'production');
$router = new Zend_Controller_Router_Rewrite();
$router->addConfig($config, 'routes');

上の例では、INI ファイルの 'routes' セクションを使用してルートを決めるよう、 ルータに指定しています。このセクションの第一レベルのキーがルート名に対応します。 上の例だと 'archive' と 'news' がこれにあたります。 ルートの各エントリには、最低限 'route' エントリとひとつ以上の 'defaults' エントリが必要となります。また、オプションでひとつ以上の 'reqs' ('required' の略) も指定できます。ここで指定したものが、それぞれ Zend_Controller_Router_Route_Interface オブジェクトに対する引数となります。オプションのキー 'type' を使用すると、 特定のルートで使用するルートクラスの型を指定できます。デフォルトでは、これは Zend_Controller_Router_Route となります。上の例では、 'news' ルートで Zend_Controller_Router_Route_Static を使用するようにしています。

7.5.8. ルータのサブクラスの作成

標準の rewrite ルータには、必要となるであろう機能のほとんどが組み込まれています。 もし新しいルータ型を作成する必要があるとすれば、 それは既存のルートに対して新しい機能を追加したり機能を変更したりしたい場合くらいでしょう。

どこかで、既存のものとはまったく異なるルーティング処理が必要となったとしましょう。 そんな場合には Zend_Controller_Router_Interface を使用します。これは、ルータとして最低限必要なひとつのメソッドのみを定義したインターフェイスです。 method.

<?php
interface Zend_Controller_Router_Interface
{
  /**
   * @param  Zend_Controller_Request_Abstract $request
   * @throws Zend_Controller_Router_Exception
   * @return Zend_Controller_Request_Abstract
   */
  public function route(Zend_Controller_Request_Abstract $request);
}
?>

ルーティング処理は、システムが最初にリクエストを受け取った際に一度だけ行われます。 ルータの役割は、リクエストの内容に応じてコントローラやアクションとオプションパラメータを決定し、 それをリクエストに設定することです。 その後、リクエストオブジェクトがディスパッチャに渡されます。 ルートに対応するディスパッチトークンがない場合は、ルータは何も行いません。