Phalconルーティングまとめ(2)ルーティングを使いこなす

「最速」PHPフレームワークPhalconのルーティングについて、基本事項をまとめます(公式ドキュメントの翻訳+αです)。記事執筆時のPhalconのバージョンは1.3.1です。

なお、ルーティングの定義方法等の基本事項については、(1)を参照してください。

ルートのグループ化

ルートが共通のパスを持っている場合、それらをひとまとめにすることができます。

<?php

$router = new \Phalcon\Mvc\Router();

// 共通のモジュールとコントローラーを持つグループを作成
$blog = new \Phalcon\Mvc\Router\Group(array(
    'module'     => 'blog',
    'controller' => 'index'
));

// 全てのルートが /blog で始まると設定する
$blog->setPrefix('/blog');

// グループにルートを追加
$blog->add('/save', array(
    'action' => 'save'
));

// 別のルートを追加
$blog->add('/edit/{id}', array(
    'action' => 'edit'
));

// デフォルトとは異なるルートの追加
$blog->add('/blog', array(
    'controller' => 'blog',
    'action' => 'index'
));

// グループをルーターに追加
$router->mount($blog);

ルートグループを別ファイルに分けることもできます。

<?php

class BlogRoutes extends Phalcon\Mvc\Router\Group
{
    public function initialize()
    {
        // デフォルトのパス
        $this->setPaths(array(
            'module'    => 'blog',
            'namespace' => 'Blog\Controllers'
        ));

        // ルートは全て /blog から始まる
        $this->setPrefix('/blog');

        // ルートを追加
        $this->add('/save', array(
            'action' => 'save'
        ));

        // 別のルートを追加
        $this->add('/edit/{id}', array(
            'action' => 'edit'
        ));

        // デフォルトとは異なるルートの追加
        $this->add('/blog', array(
            'controller' => 'blog',
            'action' => 'index'
        ));

    }
}

あとは、このグループをルーターにmount()するだけです。

$router->mount(new BlogRoutes());

mod_rewrite

ルーターに有効なURLが渡されると、ルーターは与えられたURLがルートにマッチするかチェックします。デフォルトでは、mod_rewriteによって $GET['url'] という値が作られるようになっており、この値がルーターに渡されます。Phalconで一般的に使用されるmod_rewrite設定は以下のようになります。

RewriteEngine On
RewriteCond   %{REQUEST_FILENAME} !-d
RewriteCond   %{REQUEST_FILENAME} !-f
RewriteRule   ^(.*)$ index.php?_url=/$1 [QSA,L]

名前付きルート

ルーターに追加されたルートは、Phalcon\Mvc\Router\Route オブジェクトとして保持されます。このクラスは、それぞれのルートの詳細をカプセル化します。したがって、以下のような使い方もできます。

$route = $router->add("/posts/{year}/{title}", "Posts::show"); // add()の返り値はRouteオブジェクト
$route->setName("show-posts"); // Routeオブジェクトに名前を設定

// 上記処理をメソッドチェインでまとめる
$router->add("/posts/{year}/{title}", "Posts::show")->setName("show-posts");

名前付きルートは以下のように利用します(Phalcon\Mvc\Urlを使用)。

// /posts/2012/phalcon-1-0-released と表示
echo $url->get(array(
    "for" => "show-posts",
    "year" => "2012",
    "title" => "phalcon-1-0-released"
));

使用例

以下は、ルーティングのカスタマイズ例です。

// matches "/system/admin/a/edit/7001"
$router->add(
    "/system/:controller/a/:action/:params",
    array(
        "controller" => 1,
        "action"     => 2,
        "params"     => 3
    )
);

// matches "/es/news"
$router->add(
    "/([a-z]{2})/:controller",
    array(
        "controller" => 2,
        "action"     => "index",
        "language"   => 1
    )
);

// matches "/es/news"
$router->add(
    "/{language:[a-z]{2}}/:controller",
    array(
        "controller" => 2,
        "action"     => "index"
    )
);

// matches "/admin/posts/edit/100"
$router->add(
    "/admin/:controller/:action/:int",
    array(
        "controller" => 1,
        "action"     => 2,
        "id"         => 3
    )
);

// matches "/posts/2010/02/some-cool-content"
$router->add(
    "/posts/([0-9]{4})/([0-9]{2})/([a-z\-]+)",
    array(
        "controller" => "posts",
        "action"     => "show",
        "year"       => 1,
        "month"      => 2,
        "title"      => 4
    )
);

// matches "/manual/en/translate.adapter.html"
$router->add(
    "/manual/([a-z]{2})/([a-z\.]+)\.html",
    array(
        "controller" => "manual",
        "action"     => "show",
        "language"   => 1,
        "file"       => 2
    )
);

// matches /feed/fr/le-robots-hot-news.atom
$router->add(
    "/feed/{lang:[a-z]+}/{blog:[a-z\-]+}\.{type:[a-z\-]+}",
    "Feed::get"
);

// matches /api/v1/users/peter.json
$router->add('/api/(v1|v2)/{method:[a-z]+}/{param:[a-z]+}\.(json|xml)',
    array(
        'controller' => 'api',
        'version' => 1,
        'format' => 4
    )
);

コントローラーと名前空間として許可する文字列の正規表現には、注意する必要があります。これらの文字列はファイルを探す際に使われるため、許可しないファイルへのアクセスを許してしまう恐れがあります。安全な正規表現は「 /([a-zA-Z0-9_-]+)」です。

デフォルトルーティング

Phalcon\Mvc\Router にはデフォルトのルーティングが設定されています。そのパターンは、「/:controller/:action/:params」です。

例えば、http://phalconphp.com/documentation/show/about.html というURLは以下のように変換されます。

Controller documentation
Action show
Parameter about.html

このルートを使いたくない場合、ルーターの初期化時にfalseを渡します。

$router = new \Phalcon\Mvc\Router(false);

デフォルトルートの設定

アプリケーションがいずれのルートにもマッチしないURLでアクセスされた場合、「/」ルートが使用されます。

$router->add("/", array(
    'controller' => 'index',
    'action' => 'index'
));

デフォルトパスの設定

モジュールや名前空間のデフォルトをあらかじめ設定しておくこともできます。

$router->setDefaultModule('backend');
$router->setDefaultNamespace('Backend\Controllers');
$router->setDefaultController('index');
$router->setDefaultAction('index');

// 配列で設定する場合
$router->setDefaults(array(
    'controller' => 'index',
    'action' => 'index'
));

Not Found設定

いずれのルートにもマッチしなかった場合のルーティングを設定することができます。

$router->notFound(array(
    "controller" => "index",
    "action" => "route404"
));

余分なスラッシュの扱い

余分なスラッシュを伴ってアクセスされた場合("/login"に対して"/login/"でアクセス等)、デフォルトではルートはマッチせず、Not Foundになります。余分なスラッシュを無視したい場合は、以下のメソッドを使用します。

$router->removeExtraSlashes(true);

あるいは、自前で末尾スラッシュを無視するルートを定義することもできます。

$router->add(
    '/{language:[a-z]{2}}/:controller[/]{0,1}',
    array(
        'controller' => 2,
        'action'     => 'index'
    )
);

マッチのコールバック関数

beforMatchメソッドにコールバック関数を渡すことで、より複雑な条件によるマッチングを行えます。

$router->add('/login', array(
    'module' => 'admin',
    'controller' => 'session'
))->beforeMatch(function($uri, $route) {
    // リクエストがAjaxかチェック
    if ($_SERVER['X_REQUESTED_WITH'] === 'xmlhttprequest') {
        return false;
    }
    return true;
});

マッチング条件のコールバックは、クラスにすることで再利用できるようになります。

<?php

class AjaxFilter
{
    public function check()
    {
        return $_SERVER['X_REQUESTED_WITH'] == 'xmlhttprequest';
    }
}

この場合、無名関数の代わりにフィルタークラスのインスタンスと、実行したいメソッド名を渡します。

$router->add('/get/info/{id}', array(
    'controller' => 'products',
    'action' => 'info'
))->beforeMatch(array(new AjaxFilter(), 'check'));

ホストネーム制約

ルートに、ホストネームによる制約を付けることができます。

$router->add('/login', array(
    'module' => 'admin',
    'controller' => 'session',
    'action' => 'login'
))->setHostName('admin.company.com');

ホストネーム制約では、正規表現も使えます。

$router->add('/login', array(
    'module' => 'admin',
    'controller' => 'session',
    'action' => 'login'
))->setHostName('([a-z+]).company.com');

特定のグループに対してホストネーム制約を設定することもできます。

// 共通のモジュールとコントローラーをもつグループを作成
$blog = new \Phalcon\Mvc\Router\Group(array(
    'module' => 'blog',
    'controller' => 'posts'
));

// ホストネーム制約
$blog->setHostName('blog.mycompany.com');

$blog->setPrefix('/blog');

$blog->add('/', array(
    'action' => 'index'
));

$blog->add('/save', array(
    'action' => 'save'
));

$router->mount($blog);

URIの取得方法を変更する

Phalconのデフォルトでは、ルーターに渡されるURLは $GET['url'] から取得します。Router::setUriSource()にRouter::URI_SOURCE_SERVER_REQUEST_URI を渡すことで、 $GET['url']からではなく、$_SERVER['REQUEST_URI']からURLを取得するように変更できます。

// $router->setUriSource(Router::URI_SOURCE_GET_URL); // $_GET['_url'] を使う(デフォルト)
$router->setUriSource(Router::URI_SOURCE_SERVER_REQUEST_URI); // $_SERVER['REQUEST_URI'] を使う

ルーティングの動作テスト

以下のスクリプトで、ルーティングの動作テストを行えます。

<?php

// テスト用URL
$testRoutes = array(
    '/',
    '/index',
    '/index/index',
    '/index/test',
    '/products',
    '/products/index/',
    '/products/show/101',
);

$router = new Phalcon\Mvc\Router();

// ルートを定義

// テスト用URLを1つずつテスト
foreach ($testRoutes as $testRoute) {

    $router->handle($testRoute);

    echo 'Testing ', $testRoute, PHP_EOL;

    // ルートがマッチしたかテスト
    if ($router->wasMatched()) {
        echo 'Controller: ', $router->getControllerName(), PHP_EOL;
        echo 'Action: ', $router->getActionName(), PHP_EOL;
    } else {
        echo 'ルートはマッチしませんでした', PHP_EOL;
    }
    echo PHP_EOL;

}

ルーターインスタンスを登録する

DIコンテナルーターインスタンスを登録することで、ルーターが利用可能になります。

<?php

$router = new \Phalcon\Mvc\Router();

$router->add("/login", array(
    'controller' => 'login',
    'action' => 'index',
));

$router->add("/products/:action", array(
    'controller' => 'products',
    'action' => 1,
));

return $router;

上記phpファイルの末尾では、ルーターインスタンスをreturnしています。このphpファイルをrequireすると、returnされた値をrequireの返り値として受け取ることができます。

<?php

/**
* ルーティングを利用できるようにする
*/
$di->set('router', function(){
    $router = require __DIR__.'/../app/config/routes.php';
    return $router;
});

独自のルーターを実装する

Phalcon\Mvc\RouterInterfaceを実装したルーターを作れば、自前のルーターを用意することができます。

まとめ

今回は、Testing your routesまでを紹介しました。ここまでで、Phalconのルーターに関しては、ほとんどの事項を学び終えたことになります。

残りは、Symfony風のAnnotations Routerのような若干マニアックな機能であったり、APIリファレンスだったりなので、記事にするかは未定です。

APIリファレンスについては、Phalcon\Mvc\RouterPhalcon\Mvc\Routeあたりは見ておくと良いと思います。グループ機能を使うなら、Phalcon\Mvc\Route\Groupも。