06日目: ルーティング

5日目を完璧にこなしているなら、MVCパターンに慣れてきて、コーディング方法がより自然に感じるようになっていることでしょう。もっと時間をかけて学ぶことで、振り返らないようになるでしょう。

今日は、 Ark のルーティングフレームワークのすばらしい世界に飛び込みましょう。

URL

Jobeet ホームページ上の求人情報をクリックすると、URLは /job/1 のように表示されます。 もし PHP で Web サイトの開発をしたことがあるなら、おそらく /job.php?id=1 というURLを見慣れているでしょう。 Ark はどうやって動作しているのでしょうか? Ark はどうやってこのURLを基本とするアクションを決めているのでしょうか? なぜ求人の id は my ($self, $c, $id) = @_; で取得できるのでしょうか? 今日は、これら全ての問題の答えを見てゆきます。

しかしまず初めに、 URL と URL が正確に指すものについて話します。 Web コンテキスト上で、URL は Web リソースの一意的な名前です。 URL 先へ行くと、ブラウザーに URLに よって分類されているリソースを取得するように頼みます。 そして URL は Web サイトとユーザー間のインターフェイスとして、リソースが参照している意味のある情報を伝えます。しかし旧来の URL は実際にはリソースについての説明をしておらず、アプリケーションの内部構造を公開してしまっています。 ユーザーは Web サイトが Perl で開発されているとか、求人情報が持つデータベースのある識別子というようなことはあまり気にしません。 アプリケーションの内部動作を公開することはセキュリティの観点から見ても、非常にまずいです。 ユーザーが URL 先にアクセスすることなくリソースを予想することができたらどうだろうか? 開発者は適切な方法でアプリをセキュアすべきで、機密情報は隠したほうがよいです。 URL は Ark でフレームワーク全体を管理するのに重要なものです。 これはルーティングフレームワークで管理します。

Arkの基本的的なルーティング

Ark のルーティングは、Symfony や Ruby on Rails のように、一つの設定ファイルにまとめて記述するような方式ではありません。Ark ではコントローラーメソッドの定義がそのままルーティングの役割を果たします。コントローラーのメソッドにアトリビュートを付加することにより、様々なURLを表現することができます。

代表的なアトリビュートは以下の3つです。

Path

Path アトリビュートにマッチさせたいURLを記述します。

パッケージ名が Foo の場合は foo からのパスになります。以下の例では /foo/bar というURLにマッチします。

package SampleApp::Controller::Foo;
use Ark 'Controller';

# /foo/bar
sub bar :Path('bar') {
    my ($self, $c) = @_;
}

このとき、メソッド名は関係ありません。以下も/foo/barにマッチします。

package SampleApp::Controller::Foo;
use Ark 'Controller';

# /foo/bar
sub hoge :Path('bar') {
    my ($self, $c) = @_;
}

単純にメソッド名をURLにマッチさせたい場合は、後述する:Localを使うと良いでしょう。

Pathに何も指定しないとその package のルートになります。これは /foo/ にマッチします。

package SampleApp::Controller::Foo;
use Ark 'Controller';

# /foo/
sub index :Path {
    my ($self, $c) = @_;
}

ControllerとURLの対応はデフォルトだとController名を小文字にしたものになります。CamelCase -> snake_caseの変換も行われません。

ControllerとURLの対応を変更したい場合、namespace を定義することで変更可能です。

例えば RisaMash コントローラーに/risa_mash/ を対応させたい場合は下記の様に namespace を指定します。

# こうしないと risamash になる
has '+namespace' => default => 'risa_mash';

これは /risa_mash/about にマッチします。

package SampleApp::Controller::RisaMash;
use Ark 'Controller';

has '+namespace' => default => 'risa_mash';

# /risa_mash/about
sub about :Path('about') {
    my ($self, $c) = @_;
}

これは /(トップ)にマッチします。

package SampleApp::Controller::Root;
use Ark 'Controller';

has '+namespace' => default => '';

# /
sub index :Path {
    my ($self, $c) = @_;
}

Pathの中を絶対パスで書くと絶対パスでマッチするので package 名も関係なくなります。これは /hoge/fuga にマッチします。

package SampleApp::Controller::Foo;
use Ark 'Controller';

# /hoge/fuga
sub bar :Path('/hoge/fuga') {
    my ($self, $c) = @_;
}

Args

Argsを指定することで URL からパラメータを取得できます。Args に受け取る数を指定します。

これは /foo/* にマッチします。

package SampleApp::Controller::Foo;
use Ark 'Controller';

# /foo/*
sub index :Path :Args(1) {
    my ($self, $c, $args) = @_;
}

Args を指定するとその指定した数だけ URL から引数を受け取ることが出来ます。上の例では $args として受け取っています。

下記の例では /*/*/** にマッチして /* はindex1、/*/*はindex2のメソッドを実行します。

package SampleApp::Controller;
use Ark 'Controller';

# /*
sub index1 :Path :Args(1) {
    my ($self, $c, $args) = @_;
}

# /*/*
sub index2 :Path :Args(2) {
    my ($self, $c, $args1, $args2) = @_;
}

Argsに何も指定しないと/*/*/*/*...のように、すべてにマッチするので、通常Rootコンローラーにこれを置いて404のページに使います。

package SampleApp::Controller;
use Ark 'Controller';

sub default :Path :Args {
    my ($self, $c) = @_;
    # 404
}

また Args アトリビュートを指定しない場合 Args(0) を指定したと見なされます。

Local

Localアトリビュートはメソッド名にマッチします。

sub foo :Path('about') {
}

sub about :Local {
}

は同じ意味になります。単純なルーティングであれば、Localで十分なことがほとんどです。

Global

メソッド名がトップレベルになります。

sub foo :Path('/about') {
}

sub about :Global {
}

は同じになります。

Regex

正規表現でマッチします。パッケージ名やメソッド名は関係ありません。

sub hoge :Regex('^/article//(\d{4})/(\d{2})/(\d{2})') {
    my ($self, $c, $year, $month, $day) = @_;
}

これでRegexに指定したURLにマッチします。正規表現の()の中の値を引数として受け取ることができます。

Chained

/job/{job_id}/edit のような複雑なURLを使いたい場合はChainedを使います。

sub job :Chained('/') :PathPart :CaptureArgs(1) {
    my ($self, $c, $job_id) = @_;
    $c->stash->{job_id} = $job_id;
}

# /job/{job_id}/edit
sub job_edit :Chained('job') :PathPart('edit') :Args(0) {
    my ($self, $c) = @_;
}

このとき、PathPartの値を省略するとメソッド名がPathPartの値になります。例えば、job_editメソッドは以下のように書くのと同じです。

# /job/{job_id}/edit
sub edit :Chained('job') :PathPart :Args(0) {
    my ($self, $c) = @_;
}

最初のアクションには必ず :Chained('/') を付けます。また、中間のアクションには :CaptureArgsをつけます。これにより、当該アクションは :Privte アトリビュートと同様に直接アクセスすることはできなくなります。末端のアクションには :Argsを付けます。

Chainedは慣れてくると非常に便利ですが、初学者に分かりづらい(=数年後にChainedの作法を忘れてしまった時の自分にとっても分かりづらい)という意見もちらほら聞かれます。濫用は禁物です。

Private

URLとは対応しない処理を記述する際に使用します。

sub something :Private {
    my ($self, $c) = @_;
}

他のアクションから $c->forward('something'); などとして利用します。共通処理を括り出すために使用します。

例えば、テンプレートのパーシャルが複数のコントローラメソッドから利用される場合、パーシャル用の処理をPrivateとしてまとめて、そこでレンダリングに必要な情報をstashに入れるなどすれば良いでしょう。

また、Privateアクションには意味を持つ幾つかのメソッド名が存在します。次の項目で説明します。

特殊なPrivateアクション

begin

sub begin :Private {}

名前の通り一番最初に実行される処理を記述します。次に説明するautoだけで事足りることが多く、あまり使うことはありません。

Controllerがネストしている場合、呼び出し先のコントローラに一番近い(深い)コントローラのbeginのみが呼びだされます。

1つだけ呼び出される所がautoとの大きな違いです。

特定のコントローラでRoot#autoより先に挿し込みたい処理がある場合などに使用すると良いでしょう。

auto

sub auto :Private {
    ...
    1;
}

名前の通りメインの処理が始まる前に自動的に呼び出される処理を記述します。

コントローラがネストしている場合、Rootコントローラのautoから始まり、階層の順にautoが呼び出されます。

Root#autoで全てのアクセス処理に共通の初期化処理を記述しておくのが便利です。

autoが真値を返さなかった場合はアクションの処理に入らずそのまま処理が終了します。これは意図せぬ挙動になることが多いため、autoの最後には1;を記述しておくのが定石です。

end

sub end :Private {}

名前の通り最後に実行される処理を記述します。

これは、beginと同様コントローラーに一番近い位置のendが1つだけ呼ばれます。

Root#endに共通の最終処理を記述しておくのが定石ですが、他のコントローラにendを記述した場合には、Root#endは呼び出されません。

RootコントローラじゃないendからRoot#endを呼び出したい場合には$c->detach('/end')のようにして明示的に呼び出します。

default と index

この2つは特殊なPrivateアクションではありません。

ただ、Catalystでは特殊なPrivateアクションとして定義されているため、慣例的に同様の挙動をするように使うことが多いです。

以下のようにアトリビュートを書けば、Catalystと同様の挙動をさせることが可能です。

sub index :Path {
    # Pathが空(=indexページ)の処理
}

sub default :Path :Args {
    # 標準の挙動(マッチするアクションが他になかった場合)の処理
}

forward, detach について

あるアクションの中で他のアクションを呼び出したい場合に、$c->forward('something');とすれば、その処理を呼び出すことが可能です。

実際のところ、これは$self->something($c);と同じ動きになりますが、$c->forward('/other/something');などとして、他のコントローラのメソッドも呼び出す事もできるのが違いです。 また、forwardで呼び出した場合、アクションの流れと処理時間がデバッグコンソールに表示されるので便利です。

forwardはそのアクションが呼び出された後も、残りの処理が実行されますが、別のアクションを呼び出してそこで残りの処理を続行させたくない場合はdetachを使います。

sub hoge :Local {
    my ($self, $c) = @_;

    $c->forward('something');
    warn 1; # ここの処理は実行される

    $c->detach('something');
    warn 2; # ここの処理は実行されない
}

Rootコントローラー

それでは実際にJobeetのコントローラーを書いていきましょう。実はプロジェクトをつくった時点で、トップページと404のアクションはすでに定義されています。

lib/Jobeet/Controller.pm というファイルが自動で作成されています。また昨日 end メソッドを追加しました。現在の内容は以下の用になっています。
(以前のバージョンのArkを使う場合、Controoler/Root.pm というコントローラーを定義して、そのnamespaceを空にすることで Rootコントローラを定義していましたが、今は、Controller.pmを定義することで同様の昨日の実現が可能になっています)

package Jobeet::Controller;
use Ark 'Controller';

# default 404 handler
sub default :Path :Args {
    my ($self, $c) = @_;

    $c->res->status(404);
    $c->res->body('404 Not Found');
}

sub index :Path :Args(0) {
    my ($self, $c) = @_;
    $c->res->body('Ark Default Index');
}

sub end :Private {
    my ($self, $c) = @_;

    unless ($c->res->body or $c->res->status =~ /^3\d\d/) {
        $c->forward( $c->view('MT') );
    }
}

1;

Rootコントローラーのアクションはこれでいいでしょう。

Jobコントローラー

次にJobのコントローラーを作ります。Jobで必要なページは以下のものです。

昨日一覧ページは作成しました。コードは以下のようになっています。

package Jobeet::Controller::Job;
use Ark 'Controller';

use Jobeet::Models;

sub index :Path {
    my ($self, $c) = @_;

    $c->stash->{jobs} = models('Schema::Job');
}

1;

以下のように他のアクションも定義していきましょう。

# /job/{job_id} (詳細)
sub show :Path :Args(1) {
    my ($self, $c, $job_id) = @_;
}

# /job/create (新規作成)
sub create :Local {
    my ($self, $c) = @_;
}

sub job :Chained('/') :PathPart :CaptureArgs(1) {
    my ($self, $c, $job_id) = @_;
    $c->stash->{job_id} = $job_id;
}

# /job/{job_id}/edit (編集)
sub edit :Chained('job') :PathPart :Args(0) {
    my ($self, $c) = @_;
}

# /job/{job_id}/delete (削除)
sub delete :Chained('job') :PathPart :Args(0) {
    my ($self, $c) = @_;
}

1;

確認する

ここまでできたら一度サーバーをデバッグモードで起動してみましょう。

$ plackup -r dev.psgi

そうするとコンソールにルーティングのテーブルが表示されます。

[debug] Loaded Path actions:
.-------------------------------------+--------------------------------------.
| Path                                | Private                              |
+-------------------------------------+--------------------------------------+
| /                                   | /index                               |
| /                                   | /default                             |
| /job                                | /job/show                            |
| /job                                | /job/index                           |
| /job/create                         | /job/create                          |
'-------------------------------------+--------------------------------------'

[debug] Loaded Chained actions:
.-------------------------------------+--------------------------------------.
| Path Spec                           | Private                              |
+-------------------------------------+--------------------------------------+
| /job/*/delete/                      | /job/job (1)                         |
|                                     | => /job/delete                       |
| /job/*/edit/                        | /job/job (1)                         |
|                                     | => /job/edit                         |
'-------------------------------------+--------------------------------------'

これは左側の Path に書かれているパスにリクエストの URL がマッチしたら右のメソッドを呼ぶという意味になります。

また明日

明日は、新しい概念を紹介しませんが、これまでカバーしてきたことをより深く追求することに時間をかけます。