モダンPerlの世界へようこそ
第7回 Catalyst::DispatchType::Chained:チェーンドアクションはむずかしい?
5.7系列の目玉だったチェーンドアクション
3年前に登場したCatalyst 5.7系列で導入された機能のひとつに,チェーンドアクションと呼ばれるものがあります。これは慣れると非常に便利な機能なのですが,それまでのURLとクラスの対応を根底から覆してしまう大転換だったわりにドキュメントが不足していたため,活用の仕方がわからないという声もありました。
今回はCatalyst 5.8系列で導入された新しいツールを使いながら,このチェーンドアクションの使い方を紹介していきます。スペースの都合でCatalystの基本はある程度理解しているものとして話を進めますので,わからないことがあったらCatalyst本体のドキュメントやCatalyst::Manualなどを適宜参照してください。
まずは要件を整理しよう
どのようなアプリケーションであっても事前にある程度要件を定義しておくのは大事なことですが,チェーンドアクションを使う場合は特にどのようなURLを利用するか,あらかじめよく考えておく必要があります。今回は以下のようなURLを持つ簡単なブログもどきをつくるつもりで話を進めていきます。
- トップページ:最新記事一覧
- http://example.com/
- 個別の記事表示(<id>は任意の英数字)
- http://example.com/entry/<id>
- 個別の記事作成(更新・削除)
- http://example.com/entry/<id>/(create|update|delete)
- 記事の表示以外は事前にログインが必要
- http://example.com/login
先にテストを書いておくと安心
URLの要件定義が済んだら,これをテストに落とし込みます。コマンドラインから「catalyst MyApp」を実行してひな形をつくったら,まずはt/home.tとして,このようなテストを書いてください。
use strict;
use warnings;
use Catalyst::Test 'MyApp';
use Test::More tests => 2;
use HTTP::Status;
my ($res, $c) = ctx_request('/');
ok $res->code == RC_OK;
ok $c->stash->{template} eq 'home';
このctx_requestというのは,Catalyst 5.8系列で新しく加わった便利な機能のひとつ。従来のrequestではレスポンスしか取れませんでしたが,ctx_requestによってコンテキストオブジェクト($c)もあわせて取れるようになりましたので,実際に出力された内容を見なくても,正しいテンプレートが使われているか,セッション情報がどうなっているかなどが簡単に確認できるようになりました(※1)。
- ※1
5.7系列でも2009年3月リリースの5.71001からサポートが入っていますが,それ以前のバージョンを使っている方はctx_requestのかわりにrequestを使って,テンプレートのテストを削ってください。
t/entry.tの方はセッションの扱いが混じりますのでもう少し複雑になります。
use strict;
use warnings;
use Catalyst::Test 'MyApp';
use Test::More tests => 5;
use HTTP::Status;
use HTTP::Request;
my ($res, $c) = ctx_request('/entry/1');
ok $res->code == RC_OK;
ok $c->stash->{template} eq 'entry';
my $req = HTTP::Request->new(GET => '/entry/1/create');
($res, $c) = ctx_request($req);
ok $res->code == RC_UNAUTHORIZED;
$req->header('Cookie' => 'session=foo');
($res, $c) = ctx_request($req);
ok $res->code == RC_OK;
ok $c->stash->{template} eq 'create';
ここでは省略しますが,余力のある方はぜひ更新や削除,ログイン,あるいはエラーになるURLのテストも追加しておいてください。このテストが充実していればいるほど,あとの苦労が少なくなります。
クラスとURLの関係を断ち切ろう
テストができたらコントローラの実装にかかりましょう。まずはこのようなlib/MyApp/Controller/Home.pmを用意してください(※2)。
package MyApp::Controller::Home;
use Moose;
BEGIN { extends 'Catalyst::Controller'; }
sub latest_entries : Chained('/') PathPart('') Args(0) {
my ($self, $c) = @_;
$c->stash->{entries} = $c->model('DB')->latest_entries(5);
$c->stash->{template} = 'home';
}
1;
- ※2
クラスが大きくなって処理速度が気になるようになったら,最後の「1;」のかわりに「__PACKAGE__->meta->make_immutable;」を使うと高速化できます。詳しくはMooseのドキュメントをご覧ください。
このhomeというメソッド(アクション)についている3つのアトリビュートの意味はそれぞれ次の通りです。
Chained('/') は,このアクションの起点となる「アクションのパス」です。具体的な例はまたあとで見ますが,ここでは「Chained('/')」はこのlatest_entriesというアクションの前に実行されるアクションはない,という意味だと覚えておいてください。
PathPart('') は,それまで実行されてきたアクションの「URLのパス」のあとに続くパスです。この場合は最初のアクションですから,始点はルート(「/」)。引数は「''」ですから,追加するパスはなし。つまりここではルートそのものを指していることになります。
Args(0) は,このアクションがチェーンの終点であることを意味しています。引数の「0」は,そのあとに続く(「/」で区切られた)パスの構成要素(セグメント)を引数にとらない,という意味。引数をとる例はあとで紹介します。
要するに,これだけの文字数を使って,やっていることはふだんMyApp::Controller::Rootに登録されている「sub index : Path」というアクションの再実装に過ぎないのですが,PathやLocalで指定するアクションではクラスとURLが密接に結びついているのに対して,チェーンドアクションを利用するとURLとは無関係にコントローラやメソッド名を整理できるようになりますので,コントローラの再利用性が高まりますし,スタンドアロンサーバをデバッグモードで立ち上げたときに表示されるアクションの遷移もよりわかりやすいものにできます(この例ではチェーンが短いのであまり恩恵が感じられませんが,indexというパス名ではなく,latest_entriesのように意味をもった名前をつけられるようになるので,具体的な処理の流れを追いやすくなります)。
モダンPerlの世界へようこそ
- 第27回 Test::Most:Test::Moreでは物足りなくなってきたら
- 第26回 ShipIt:モジュールのリリースをもっと手軽に
- 第25回 Module::Starter:モジュールを書くためのテンプレート
- 第24回 CPAN:Perl界の水先案内人
- 第23回 Module::Build:MakeMakerの後継者を目指して
- 第22回 Mojolicious::Lite:本当に簡単なウェブアプリがあればいいときは
- 第21回 KiokuDB:マッピングが複雑すぎると感じたら
- 第20回 Email::Sender:メールを送信する
- 第19回 Who's Who on IRC:Perl界の紳士録(IRC編)
- 第18回 local::lib:ふだんと違う環境でPerlを使う


