5.7系列の目玉だったチェーンドアクション
3年前に登場したCatalyst 5.
今回はCatalyst 5.
まずは要件を整理しよう
どのようなアプリケーションであっても事前にある程度要件を定義しておくのは大事なことですが、
- トップページ:最新記事一覧
- http://
example. com/ - 個別の記事表示
(<id>は任意の英数字) - http://
example. com/ entry/<id> - 個別の記事作成
(更新・ 削除) - http://
example. com/ entry/<id>/(create|update|delete) - 記事の表示以外は事前にログインが必要
- http://
example. com/ login
先にテストを書いておくと安心
URLの要件定義が済んだら、
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_
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の関係を断ち切ろう
テストができたらコントローラの実装にかかりましょう。まずはこのようなlib/package MyApp::Controller::Home;
use Moose;
BEGIN { extends 'Catalyst::Controller'; }
sub latest_
このhomeというメソッド
Chained('/') は、
このアクションの起点となる 「アクションのパス」 です。具体的な例はまたあとで見ますが、 ここでは 「Chained('/')」 はこのlatest_ entriesというアクションの前に実行されるアクションはない、 という意味だと覚えておいてください。 PathPart('') は、
それまで実行されてきたアクションの 「URLのパス」 のあとに続くパスです。この場合は最初のアクションですから、 始点はルート (「/」)。引数は 「''」 ですから、 追加するパスはなし。つまりここではルートそのものを指していることになります。 Args(0) は、
このアクションがチェーンの終点であることを意味しています。引数の 「0」 は、 そのあとに続く (「/」 で区切られた) パスの構成要素 (セグメント) を引数にとらない、 という意味。引数をとる例はあとで紹介します。
要するに、
チェーンドアクションの優先度
さて、
ただし、
package MyApp::Controller::Root;
use Moose;
BEGIN { extends 'Catalyst::Controller'; }
__PACKAGE__->config->{namespace} = '';
sub default : Private {
my ( $self, $c ) = @_;
$c->forward('/error/not_found');
}
sub end : ActionClass('RenderView') {}
1;
プライベートアクションも活用しよう
defaultアクションについては、
package MyApp::Controller::Error;
use Moose;
BEGIN { extends 'Catalyst::Controller'; }
use HTTP::Status;
sub not_found : Private {
my ($self, $c) = @_;
$c->res->body(HTTP::Status::status_message(RC_NOT_FOUND));
$c->res->status(RC_NOT_FOUND);
$c->detach;
}
1;
最後の$c->detachは、
これでt/
順を追って実装していく
どのエントリを表示
package MyApp::Controller::Entry;
use Moose;
BEGIN { extends 'Catalyst::Controller'; }
sub entry : Chained('/') PathPart CaptureArgs(1) {
my ($self, $c, $id) = @_;
$c->stash->{entry} = $c->model('DB')->entry($id);
}
最初のChained('/')についてはすでに説明しました。PathPartに引数がない場合は、
このように可変のパスに対応できるというのがチェーンドアクションのひとつの長所なのですが、
この例の場合、
終点の再利用
MyApp::Controller::Entryの続きはこのような感じになります。
sub default_read : Chained('/entry/entry') PathPart('') Args(0) {
my ($self, $c) = @_;
$c->forward('/entry/read');
}
sub read : Chained('entry') PathPart Args(0) {
my ($self, $c) = @_;
$c->stash->{template} = 'entry';
}
sub create : Chained('entry') PathPart Args(0) {
my ($self, $c) = @_;
$c->forward('/error/unauthorized') unless $c->user_exists;
if (uc $c->req->method eq 'POST') {
$c->stash->{entry}->create($c->req->params);
$c->res->redirect($c->uri_for('/'));
$c->detach;
}
$c->stash->{template} = 'create';
}
1;
Chainedの引数は、
単純にチェーンのつなぎ方の問題だけ考えると、
sub default_read : Chained('/') PathPart('entry') Args(1) {
my ($self, $c, $id) = @_;
$c->stash->{entry} = $c->model('DB')->entry($id);
$c->stash->{template} = 'entry';
}
ただし、
だから、
package MyApp::Base::Controller::CRUD;
use Moose;
BEGIN { extends 'Catalyst::Controller'; }
sub default_read : Chained('entry') PathPart('') Args(0) { ... }
sub read : Chained('entry') PathPart Args(0) { ... }
sub create : Chained('entry') PathPart Args(0) { ... }
1;
同様のCRUD処理が必要なコントローラを追加したくなったときは、
package MyApp::Controller::Wiki;
use Moose;
BEGIN { extends 'MyApp::Base::Controller::CRUD'; }
sub entry : Chained('/') PathPart CaptureArgs(1) { ... }
# あとのCRUD処理はMyApp::Base::Controller::CRUDにおまかせ
1;
サイト全体に関する処理をしたい場合
このように、
セッション管理や認証まわり、
たとえば、
sub root : Chained('/') PathPart('') CaptureArgs(0) {
my ($self, $c) = @_;
# セッション・認証管理のように、サイト全体に関係する処理を行う
# $c->forward('/auth/basic');
# $c->forward('/auth/requires_ssl');
# $c->forward('/session/check'); などなど
}
もちろん場合によってはrootという名前ではなく、
チェーンドアクションとuri_for_action
チェーンドアクションを使う上でもうひとつ忘れてはならないのは、
使い方は特にむずかしいところはありません。今回の例であればこの2通りの書き方さえ知っておけば十分でしょう
# http://example.com/
my $uri = $c->uri_for_action('/home/latest_entries');
# http://example.com/entry/1/create
my $uri = $c->uri_for_action('/entry/create', [1]);
ちなみに、
なんでもかんでもチェーンドアクションにする必要はありません
チェーンドアクションは便利な反面、
従来のLocalやPathを多用するやり方でも、
次回はまた別の観点からCatalyst 5.