- http://code.google.com/hosting/ 風の「/p/プロジェクト名」でアクセスできるルーティングを作ります。
- 「p」は固定の接頭語です。なくともいいけど、その場合は、予め使用する単語を予約する必要があります。
- 「プロジェクト名」:プロジェクトを識別する文字列です。ここでは、[_0-9a-z]+ の文字を想定しています。
参考
- CookBook 3.4.5.9 Custom Route classes
- API Router Class
- 第5回CakePHP勉強会@Tokyoに参加・発表してきました。
- http://d.hatena.ne.jp/hiromi2424/20100603/1275558884
- スライドの25ページ目あたり
走査するテーブルをつくる
-- -- テーブルの構造 `projects` -- CREATE TABLE IF NOT EXISTS `projects` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(200) COLLATE utf8_unicode_ci NOT NULL, `description` text COLLATE utf8_unicode_ci NOT NULL, `type` varchar(200) COLLATE utf8_unicode_ci NOT NULL, `license` varchar(200) COLLATE utf8_unicode_ci NOT NULL, `user_id` int(11) NOT NULL, `created` datetime NOT NULL, `modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=6 ; -- -- テーブルのデータをダンプしています `projects` -- INSERT INTO `projects` (`id`, `name`, `description`, `type`, `license`, `user_id`, `created`, `modified`) VALUES (5, 'whisk', '1', '1', '1', 32, '0000-00-00 00:00:00', '0000-00-00 00:00:00');
ルーティングのおさらい
app/config/routes.php に記述のおさらいです。
結果からですが、
ルーティングの結果で以下のようなパラメーターが Router クラスで生成されます。
「/p/whisk」を例とした場合
array
'project' => string 'whisk' (length=5)
'named' =>
array
empty
'pass' =>
array
empty
'controller' => string 'tickets' (length=7)
'action' => string 'index' (length=5)
'plugin' => null
<?php Router::connect('/', array('controller' => 'projects', 'action' => 'index'));
- 第一引数 '/'
- マッチさせたい url を記述します。
- ここでは、http://example.com/ にアクセスしたときの処理を記述します。
- 第二引数 array('controller' => 'projects', 'action' => 'index')
- 第一引数 にマッチした際に呼び出す、コントローラーやアクションを記述します。
- ここでは、http://example.com/ にアクセスしたとき、上記のパラメーターに、'controller' => 'projects', 'action' => 'index' がセットされ、projects コントローラー の index アクションを呼び出すということを指定しています。
<?php Router::connect( '/:controller', array(), array('controller' => 'projects|users|pages') );
- 第一引数 '/:controller'
- 「:controller」のように「:」が先頭についているものが登場します。
- 「:」が先頭についていると、「/users」にアクセスした際、'controller' => 'users' がセットされ、 users コントローラー を呼び出せるというように、一般的な記述ができます。
- 「:controller」の他に、アクションを呼び出せる「:action」があります。
- その他にも「:project」のように、「:」をつけることで、任意の値をセットできるようになります。
- アクションを省略すると、「'action' => 'index'」がデフォルトでセットされます。
- 第二引数 array()
- ここでは、array() が指定されています。Router::connect の第三引数に値をセットしたいため、デフォルト値を与えています。
- array() は、第二引数が省略された時に、デフォルトでセットされる値です。
- 第三引数 array('controller' => 'projects|users|pages')
- 第一引数でセットされる値に対するバリエーションを定義します。
- 「:controller」で任意のコントローラーを呼び出せるようになりますが、呼び出せるコントローラーを制限したいときに、「'controller' => 'projects|users|pages'」と指定します。
- この設定では、「/users」はOK、「/posts」はNGとなります。
- 注意点として、「/users/add」や「/users/edit/123」などはマッチしません。
<?php Router::connect( '/:controller/:action/*', array(), array('controller' => 'projects|users|pages') );
- 上記のルーティングに加え、「/:controller」にパラメーターが付いている場合にもマッチするようになります。
- 「/users/add」や「/users/edit/123」がマッチします。
<?php App::import('Lib', 'routes/ProjectRoute'); Router::connect( '/p/:project', array( 'controller' => 'tickets' ), array( 'project' => '[_a-z0-9]{3,}', 'routeClass' => 'ProjectRoute' ) );
- 第一引数 '/p/:project'
- http://example.com/p/<プロジェクト名> でアクセスしたときのルーティングを設定します。
- 'project' => '<プロジェクト名>' がセットされます。
- 第二引数 array('controller' => 'tickets')
- http://example.com/p/<プロジェクト名> にアクセスしたとき、tickets コントローラー(アクションは index )を実行する設定です。
- 第三引数 配列で、 'project' => '[_a-z0-9]{3,}', 'routeClass' => 'ProjectRoute'
- 「'project' => '[_a-z0-9]{3,}'」について、'project' の値が、正規表現「[_a-z0-9]{3,}」(半角英数字とアンダーバー「_」が3文字以上) にマッチするものだけ、このルーティングにヒットするようになります。
- 'routeClass' => 'ProjectRoute'について、cakephp1.3系で新たに導入された機能で、データベースを使ったルーティングなどより複雑なルーティングを実現させる機能です。
- ここでは、ProjectRoute というクラスに処理が移ります。
- 事前に、App::import により、扱うクラスを読み込みます。
新機能 routeClass
cake の規則
(app|plugin)/libs/routes 以下に、<任意の名前(単数小文字)>_route.php を作成します。
ここでは、project_route.php を作成します。
また、命名規則(先頭大文字単数 + Route)により、CakeRoute を継承した ProjectRoute クラスをつくります。
<?php class ProjectRoute extends CakeRoute // 先頭大文字単数 + Route { // 略 }
<?php App::import('Lib', 'routes/ProjectRoute');
を使って読み込むことで、スマートに読み込みができます。
与えられた url を処理する parse($url)
routeClass で処理するクラスを指定すると、そのクラスの parse メソッドが呼ばれます。
このメソッド内で、与えられた url にマッチしているかどうかを判定します。
マッチしていない場合は、false を返します。
マッチした場合は、配列でパースしたパラメータを返します。
例として、
「/p/whisk」にアクセスしたときの処理をコメントで解説しています。
<?php /** * (non-PHPdoc) * * @param string $url The url to attempt to parse. * * @see cake/libs/CakeRoute#parse($url) * * @return mixed Boolean false on failure, otherwise an array or parameters */ function parse($url) // 「/p/whisk」 にアクセスすると、/p/whisk がセットさせる。 { // 親メソッドを呼び出すと、Router::connect に記載された設定で、$url を分解します。 // // $url = '/p/whisk' で、parent::parse($url) した結果。 // // array // 'project' => string 'whisk' // 「:project」の部分がセットされます。 // 'named' => // array // empty // 'pass' => // array // empty // 'controller' => string 'tickets' //第二引数の設定がセットされます。 // 'action' => string 'index' // 省略されたので、デフォルトの 'index' がセットされます。 // 'plugin' => null $params = parent::parse($url); // Router::connect に記載された設定にマッチしない場合、 // parent::parse($url) は「false」を返します。 // 「false」が返ったら、ルーティングを失敗させます。 if ($params === false) return false; // モデルを読み込み、$params['project'] にセットされた値が // データベースにあるかどうかをチェックします。 // {{{!初期化 // この部分は、人によりけり。ハードコーディングでもいいかも。 // 読み込むモデルの小文字単数 $name = 'project'; // 読み込むモデルを cake の命名規則に合わせる処理(先頭を大文字) // 'project' -> 'Project' $modelName = Inflector::classify($name); // 取得するレコードのフィールド(id)をセット : 'Project.id' $fieldId = $modelName . '.id'; // 取得するレコードのフィールド(name)をセット : 'Project.name' $fieldName = $modelName . '.name'; // }}}!初期化 // 'Project'モデルを読み込む。返り値は、'Project'モデルオブジェクト $model = ClassRegistry::init($modelName); // データベースを検索する。 // 生成されるSQL(Mysql) // SELECT `Project`.`id` , `Project`.`name` // FROM `projects` AS `Project` // WHERE `Project`.`name` = 'whisk' // LIMIT 1 // // 'conditions' 検索する条件 'Project.name' が 'whisk' // 'fields' 取得するカラム 'Project.id' と 'Project.name' // 'recursive' アソシエーションを抑制する // [結果] // array // 'Project' => // array // 'id' => string '5' (length=1) // 'name' => string 'whisk' (length=5) $result = $model->find('first', array( 'conditions' => array($fieldName => $params[$name]), 'fields' => array($fieldId, $fieldName), 'recursive' => -1 )); // 「false」は、find メソッドに異常があった場合、返される。 // 「null」 は、レコードが見つからなかった場合、返される。 // プロジェクトが無い場合、ルーティングを失敗させます。 if ($result === false || $result === null) { // false -> fail find(), null -> not found data return false; } // 配列から、値を取得します。Set::extract('Project.id', $result); // 値がなければ、「null」を返します。 // $projectId = isset($result['Project']['id']) ? $result['Project']['id'] : null; // と同じですが、すっきりと書けます。 // 詳しくは、以下が参考になります。 // ・http://book.cakephp.org/ja/view/671/extract // ・cake/tests/cases/libs/set.test.php にある testExtract() // ・[CakePHP] Setクラスを使ってコード量を減らす // http://c-brains.jp/blog/wsg/09/09/15-213238.php // // Set クラスは高機能で、色々なことができます。 // (投げられるチケットも多かったので、テストも膨大です。) // Set クラスは高機能過ぎて使い方をすぐに忘れてしまう (´・ω・`) $projectId = Set::extract($fieldId, $result); $projectName = Set::extract($fieldName, $result); // 値がなければ、「null」を返すので、「null」の場合は、ルーティングを失敗させます。 if ($projectId !== null && $projectName !== null) { // ここらは、後で使いまわす予定なので、ここでセットしておきます。 Configure::write('projectId', $projectId); Configure::write('projectName', $projectName); // ルーティングが成功したときは、パースした配列を返します。 return $params; } // ルーティングが失敗したときは、「false」を返します。 return false; }
逆ルーティングをする match($url)
parse メソッドで分解されたパラメータ(array)から、url(string) を生成します。
ややこしいですが、実行されるのは、
Router::url が呼ばれたとき、(ヘルパーでは、url() から呼ばれる)
match メソッドが呼ばれます。
成功した場合は、url(string) を返します。
失敗した場合は、false を返します。
例では、
「whisk」というプロジェクト内で、
tickets コントローラー 、 index アクション を呼び、
index.ctp 内で
<?php foreach ($tickets as $ticket) { echo $html->link( $ticket['Ticket']['title'], array('action' => 'view', $ticket['Ticket']['id']) ); }
が記述されているとして、コメントをつけています。
また、
HtmlHelper::link の 第二引数( array('action' => 'view', $ticket['Ticket']['id']) )は、
HtmlHelper::url (実質的には Helper::url )に渡され、
Router::url が呼ばれ、match メソッドに繋がります。
<?php /** * (non-PHPdoc) * * @param array $url An array of parameters to check matching with. * * @see cake/libs/CakeRoute#match($url) * * @return mixed Either a string url for the parameters if they match or false. */ function match($url) { // $url は 配列です。 // ここでは、以下のような値です。 // array // 'action' => string 'view' (length=4) // 0 => string '155' (length=3) // 'controller' => string 'tickets' // コントローラーを省略したので、コントローラーがセットされる。 // 'plugin' => null // 記録した 'projectName' を読み込みます。 // 記録しなかった場合、「null」が返ります。 // index など、一覧のページでは、かなりの回数が呼ばれます。 // デフォルトのファイルキャッシュでは、オーバーヘッドが大きいので、変数キャッシュの方がマシです。 $projectName = Configure::read('projectName'); // 'projectName' がない場合、この処理を失敗させます。 if ($projectName === null) return false; // 「:project」の一部である「project」に値をセットします。 $url['project'] = $projectName; // あとは、親を呼んで、任せます。 // 「/p/whisk/tickets/view/155」が返ります。 return parent::match($url); }
routes.php
細かい部分はお好みで。
<?php App::import('Lib', 'routes/ProjectRoute'); if (!defined('WHISK_USER_URL')) { define('WHISK_USER_URL' , 'u'); } if (!defined('WHISK_PROJECT_URL')) { define('WHISK_PROJECT_URL' , 'p'); } Router::connect('/', array('controller' => 'projects', 'action' => 'index')); /* Genral Routes */ Router::connect( '/:controller', array(), array('controller' => 'projects|users|pages') ); Router::connect( '/:controller/:action/*', array(), array('controller' => 'projects|users|pages') ); /* Project Routes */ Router::connect( '/' . WHISK_PROJECT_URL . '/:project', array( 'controller' => 'tickets' ), array( 'project' => '[_a-z0-9]{3,}', 'routeClass' => 'ProjectRoute', 'controller' => 'tickets|comments|settings|states', ) ); Router::connect( '/' . WHISK_PROJECT_URL . '/:project/:controller', array(), array( 'project' => '[_a-z0-9]{3,}', 'routeClass' => 'ProjectRoute', 'controller' => 'tickets|comments|settings|states', ) ); Router::connect( '/' . WHISK_PROJECT_URL . '/:project/:controller/:action/*', array(), array( 'project' => '[_a-zA-Z0-9]{3,}', 'routeClass' => 'ProjectRoute', 'controller' => 'tickets|comments|settings|states', ) );