Catalyst入門
基本が大事!ということで、Catalystに再入門してみた。
Catalyst::Manual::Intro - Introduction to Catalyst - metacpan.org
というイントロがあるので、それをまとめながら訳す。訳しながらまとめる。
という作業をやっている途中で
http://www.tcool.org/index.cgi/catalyst/
というすばらしいサイトがあることを知ったのだが、今回の目的は単に訳すことや文章を読むことではなくてもっと個人的な作業なので特に気にしないで続けることができた。というわけでこれはあくまで「まとめ」なので省略してるところがあります。正確さを求める場合は必ず原文を参照すること。あと、個人的には上でリンクした日本語訳と比べてみると結構おもしろかったです。「あー、こうやって訳すのか。すごいな」とか「ほとんど同じ訳だな」とか。
また、英語を訳しながら読むとどうしても速度がゆっくりになるし、ちゃんと理解しないと自分の言葉で書けないので、その点でとてもよかったと思う。
この下から始まり。
-
- -
Catalystがどう動くのかの説明と、簡単なアプリケーションを作って実行する方法を示す。
Catalystとは何か?
Catalystは柔軟性とシンプルさを主眼に置いたウェブアプリケーションフレームワーク。
Ruby on Rails, Spring, Maypoleなんかと似ている。CatalystはMaypoleをもとにしている。
MVC
CatalystはMVCデザインパターンに従う。
CatalystはControllerの役目をする。
ModelはClass::DBIなど、ViewはTemplate Toolkit、Mason、HTML::Templateなどが代表的。
ほかにも使いたいモジュールを好きなようにつかっていい。
柔軟性について
- 複数のモデル、ビュー、コントローラを使えるよ
Catalystを使ってアプリケーションを作るときは、コンポーネントと呼ばれるモジュールを使って各処理をハンドリングする。このコードは大体の場合はシンプルで、ただ単にさっきMVCのところで言ったようなモジュールを呼び出すだけだ。また、Catalystはこれらのコンポーネントをすごく柔軟な方法で扱っているので、たくさんのモデル、ビュー、コントローラ、たくさんのPerlモジュールを使うことができる。
- コンポーネントの再利用
既存のPerlモジュールだけじゃなくCatalystのコンポーネントも再利用できる。
- 無制限のURL-Actioonディスパッチ
どんなURLもアクションにディスパッチできる。正規表現も使える。mod_rewriteは必要ない。URLにクラス名を入れる必要もない。
sub hello : Global { my ( $self, $context ) = @_; $context->response->body('Hello World!'); }
シンプルさ
Catalystはとても簡単な方法でこの柔軟性を実現している。
- ブロックを積み重ねるようなインターフェース
すべてのコンポーネントはお互いにとてもスムーズに動作する。たとえば、Catalystはコンテキストオブジェクトを自動的に作るんだけど、それはすべてのコンポーネントから使える。コンテキストオブジェクトを使うと、リクエストオブジェクトへのアクセス、コンポーネント間のデータ共有、フロー制御ができる。
- コンポーネントの自動検出
コンポーネントはuseしなくていい。Catalystが自動的に見つけてロードしてくれる。
- ポピュラーなモジュール用のプレビルドされたコンポーネント
Class::DBIのためにはCatalyst::Model::CDBIを、Template ToolkitのためにはCatalyst::View::TTを参照のこと。
- ビルトインのテストフレームワーク
Catalystは軽量なHTTPサーバとテストフレームワークを搭載しているので簡単にテストできる。
- ヘルパースクリプト
コンポーネントとユニットテスト用のスタートアップコードを自動生成。Catalust::Helperを参照。
クイックスタート
インストールして、セットアップして、実行する。
セットアップ
$ catalyst.pl MyApp # output omitted $ cd MyApp $ script/myapp_create.pl controller Library::Login
実行
$ script/myapp_server.pl
以下のURLにブラウザでアクセスすればCatalystが動いているのを見れる。
http://localhost:3000/
http://localhost:3000/library/login/
死ぬほど簡単だ。
アプリケーションクラス
モデル、ビュー、コントローラーに加えて、アプリケーション自身を表すクラスが一つある。ここでアプリケーションの設定や、プラグインのロードや、アプリケーション全体に対するアクション定義や、Catalystの拡張をする。
package MyApp; use strict; use Catalyst qw/-Debug/; MyApp->config( name => 'My Application', # You can put anything else you want in here: my_configuration_variable => 'something', ); sub default : Private { my ( $self, $context ) = @_; $context->response->body('Catalyst rocks!'); } 1;
たいていの場合はここで一つの設定値を定義する。
- name
- アプリケーションの名前
オプションでテンプレートのルートや静的なデータを指定できる。省略した場合、Catalystはディレクトリの場所を自動検出しようとする。またプラグイン用など、好きなパラメータを定義できる。アプリケーションのどこからでも$context->config->{$param_name}でアクセスできる。
コンテキスト
Catalystは自動的にコンテキストオブジェクトをブレスする。それはアプリケーションのどこからでも使える。コンテキストを使ってCatalystと直接やり取りし、コンポーネント同士をくっつける。たとえば、Template Toolkitからコンテキストを使いたかったらこうやる。
<h1>Welcome to [% c.config.name %]!</h1>
それは既に存在するのだ。
さっきURL-Actioonディスパッチで示したようにコンテキストは常に第2パラメータだ。ちなみに第1パラメータはコンポーネントオブジェクトのリファレンスかクラス名。あと、さっきはわかりやすいように$contextって言ったけど、ほとんどの開発者は単に$cって言う。
sub hello : Global { my ( $self, $c ) = @_; $c->res->body('Hello World!'); }
Contextには重要なオブジェクトがいくつかある。
- Catalyst::Request
$c->request $c->req # alias
リクエストオブジェクトにはリクエスト固有の情報がすべて入ってる。クエリーパラメータ、クッキー、アップロード(訳注:これなんだろ。ファイルアップロード?)、ヘッダーなどなど。
$c->req->params->{foo}; $c->req->cookies->{sessionid}; $c->req->headers->content_type; $c->req->base;
- Catalyst::Response
$c->response $c->res # alias
レスポンスはリクエストに似ているけど、ただ単にレスポンス固有の情報を持っている。
$c->res->body('Hello World'); $c->res->status(404); $c->res->redirect('http://cook.de');
- Catalyst::Config
$c->config $c->config->root; $c->config->name;
- Catalyst::Log
$c->log $c->log->debug('Something happened'); $c->log->info('Something you should know');
- スタッシュ
$c->stash $c->stash->{foo} = 'bar';
スタッシュはコンポーネント間でデータを共有するためのハッシュだ。たとえばこんな感じ。
sub hello : Global { my ( $self, $c ) = @_; $c->stash->{message} = 'Hello World!'; $c->forward('show_message'); } sub show_message : Private { my ( $self, $c ) = @_; $c->res->body( $c->stash->{message} ); }
stashは内部でやり取りするデータのためだけに使えるってことに注意。つまり新しいリクエストが来たらクリアされるってこと。もっと永続的なデータを保持したかったらセッションを使うこと。
アクション
Catalystのコントローラはそのアクションによって規定される。アクションは特別な属性つきの一つのサブルーチンだ。URLってのは二つのパートから成り立っている。ベースとそのパスだ。たとえばURLがhttp://localhost:3000/foo/barだったらベースはhttp://localhost:3000/でパスはfoo/barになる。ホスト名の後のスラッシュはベースのほうにくっつくことに注意。
Catalystのアクションにはいくつかのタイプがある。
package MyApp::Controller::My::Controller; sub bar : Path('foo/bar') { }
このアクションは現在のネームスペースに対して相対的に働く。上の例はhttp://localhost:3000/my/contoroller/foo/barだけにマッチする。またパスをスラッシュから始めるとルートからのマッチになる。たとえば、
package MyApp::Controller::My::Controller; sub bar : Path('/foo/bar') { }
はhttp://localhost:3000/foo/barだけにマッチする。
package MyApp::Controller::My::Controller; sub bar : Path { }
Pathの部分を空にするとネームスペースのルートにマッチする。上の例ではhttp://localhost:3000/my/contorollerにマッチする。
sub bar : Regex('^item(?d+)/order(?d+)$') { }
正規表現も使える。上の例はたとえばhttp://localhost:3000/item23/order42みたいなURLにマッチする。正規表現をシングルクォーテーションで囲むかどうかはオプション。
Regexはグローバルに作用する。つまり呼び出し元のネームスペースは考慮されないから、MyApp::Controller::Catalog::Order::Processネームスペースのbarメソッドは、正規表現で明示的に指定しない限りbar、Catalog、Order、Processのどれにもマッチしない(訳注:ちょっとわかりにくいけど、たぶん、マッチさせたかったら正規表現にネームスペース全部指定しろってこと)。
これとは違うやり方がLocalRegexだ。
- LocalRegex(正規表現ローカル版)
sub bar : LocalRegex('^widget(?d+)$') { }
LocalRegexはローカルに作用する。上の例のようにすればhttp://localhost:3000/catalog/widget23みたいなURLにマッチする。
正規表現先頭のサーカムフレックス(^)を省略すると、コントローラの階層に関係なくマッチするようになる。下のコードは上のやつと違ってhttp://localhost:3000/catalog/foo/widget23みたいなURLにもマッチする。
package MyApp::Controller::Catalog; sub bar : LocalRegex('widget(?d+)$') { }
LogRegexもRegexも、丸括弧()を使ってマッチした値を記憶した場合、それらの値は$c->req->snippetsで参照できる。上の例で"widget23"がマッチしたとすると$c->req->snippets->[0]には23が入る。URLの末尾で引数を渡したいなら、アクションに正規表現を使わなければならない。詳しくは後述のURL Path Handlingを参照。
- トップレベル
package MyApp; sub foo : Global { }
http://localhost:3000/fooにマッチする。関数名が直接アプリケーションベースにマップされる。
- Namespace-Prefixed(ネームスペースが前にくっつく)
package MyApp::Controller::My::Controller; sub foo : Local { }
http://localhost:3000/my/controller/fooにマッチする。
このアクションタイプではコンポーネントのクラス(パッケージ)名を変形したものがURLの前にくっつく。変形は次のように行われる。
- Catalystであらかじめ意味付けされているパーツ(上の例ではMyApp::Controller)を除去する
- ::を/に置き換る
- 全部小文字にする
Catalystコンポーネントクラス名の事前意味づけについての詳しいことはComponentsを参照。
- Private
sub foo : Private ( )
これはURLにマッチしない。URLへのリクエストで実行することはできない。PrivateアクションはCatalyst内部でのみ使用される。呼び出すためにはforwardメソッドを使う。
$c->forward('foo');
他のコンポーネントから呼ぶときはメソッドへの絶対パスで指定すること。MyApp::Controller::Catalog::Order::Processコントローラのbarメソッドを他の場所からコールするときは$c->forward('/catalog/order/process/bar')とする。詳しくはフローコントロールを参照。
ノート:アクションを正規表現やパスで定義する理由は何か?実際のところすべてのパブリックアクションはプライベートアクションでもあるので、コンポーネントへのアクセス方法がひとつだけになるってことだ。forwardを使って。
ビルトインのプライベートアクション
アプリケーションの決まったときに、Catalystはこれらのアプリケーションクラス内のビルトインプライベートアクションを自動的にコールする。
- default : Private
どのアクションもマッチしないときに呼ばれる。たとえばトップページやエラーページの表示のために使ったりする。
もしdefaultが思ったように動かなかったら、パス文字列なしのリテラルPathアクションを使ってみよう。両者の違いはPathのほうがネームスペースからの相対パスを引数で受け取るのに対して、defaultのほうはコントローラの位置にかかわらずルートからの相対パスを受け取るという点だ。
- index : Private
indexはdefaultによく似ているが、引数をとらないという点とマッチングプロセスよりもすこしだけ高い重み付けがされている点が異なる。これはコントローラへの静的なエントリポイントとして有用だろう。たとえば静的なウェルカムページを持つ場合とか。Pathよりも優先順位が高いという点に注意!
- begin : Private
リクエストの最初、マッチしたどのアクションが呼ばれるよりも早く呼ばれる。
- end : Private
リクエストの最後、マッチしたアクションがすべて呼ばれてから呼ばれる。
コントローラ/自動連鎖内のビルトインアクション
Package MyApp::Controller::Foo; sub begin : Private { } sub default : Private { } sub auto : Private { }
コントローラ内でもビルトインプライベートアクションを定義できる。
そのアクションは、それより詳細度の低いコントローラやアプリケーションクラス内のそれをオーバーライドする。言い換えると、3つのビルトインプライベートアクション(訳注:名前は同じアクション)があったとすると、1回のリクエストではそのうちの1つしか実行されないということだ。したがって、もしMyApp::Controller::Catalog::beginが存在していて、あなたがいまcatalogネームスペースにいるとすると、MyApp::beginの代わりにそれが実行されることになる。で、順々にMyApp::Controller::Catalog::Order::beginがこれをオーバーライドして、というふうになる。
ノーマルなビルトインアクションに加えて、連鎖を構成するための特別なアクションがある。それがautoだ。autoアクションはbeginの後、あなたのアクションの前に実行される。他のビルトインと違って、autoアクションはお互いをオーバーライドしない。それらはアプリケーションクラスから始まって、一番詳細度の高いクラスまで順番に呼ばれる。これはノーマルなビルトインがお互いをオーバーライドする方向とは逆になっている。
ここでいろんな種類のビルトインが呼ばれるときの順番を例示しておこう。
/foo/fooへのリクエスト
MyApp::begin MyApp::auto MyApp::Controller::Foo::default # MyApp::Controller::Foo::Fooがない場合 MyApp::end
/foo/bar/fooへのリクエスト
MyApp::Controller::Foo::Bar::begin MyApp::auto MyApp::Controller::Foo::auto MyApp::Controller::Foo::Bar::auto MyApp::Controller::Foo::Bar::default # MyApp::Controller::Foo::Bar::fooがない場合 MyApp::Controller::Foo::Bar::end
また、autoアクションは0を返すことによってその連鎖から抜け出すことができるという点において優れている。あるautoアクションが0を返すと、endを除いた残りのアクションはみんなスキップされる。したがって上に例示したリクエストにおいて、最初のautoがfalseを返すとこのようになる。
/foo/bar/fooへのリクエストだが最初のautoでfalseを返す
MyApp::Controller::Foo::Bar::begin MyApp::auto MyApp::Controller::Foo::Bar::end
これを使った1つの例が認証アクションだ。認証用のautoアクションはアプリケーションクラスに置く(これが常に最初に呼ばれるから)。で、認証に失敗したら0を返せば、そのリクエストの残りのメソッドはスキップされる。
ノート:視点を変えてみると、処理を続けるためにはautoアクションはtrueを返さなければいけないということだ。また、自動連鎖の中でdieすることもできる。その場合、リクエストはいきなり最終ステージへと向かう。他に何か実行することはない。
URLパスハンドリング
URLの一部を使って変数引数を渡すことができる。この場合、regexアクションを'^'と'$'と一緒に使わなければならない。そして引数はURL中でスラッシュで区切られている必要がある。たとえば、あなたが/foo/$bar/$bazをハンドルしたいとするとこうなる。ここで$barと$bazが変数。
sub foo : Regex('^foo$') { my ($self, $context, $bar, $baz) = @_; }
でも、/foo/booとか/foo/boo/hooなんていうアクションが定義されている場合はどうなるか?
sub boo : Path('foo/boo') { .. } sub hoo : Path('foo/boo/hoo') { .. }
Catalystは詳細度の高いものから低いものへという順番でアクションにマッチする。
/foo/boo/hoo /foo/boo /foo # might be /foo/bar/baz but won't be /foo/boo/hoo
だから、Catalystが最初の二つのURLを'^foo$'アクションにマッチさせることは絶対にない。
パラメータ処理
URLクエリストリングに渡されたパラメータはCatalyst::Requestクラスのメソッドで扱う。そのparamメソッドは機能的にはCGI.pmのparamメソッドと等価で、CGI.pmを必要とするモジュールで使うことができる。
# http://localhost:3000/catalog/view/?category=hardware&page=3 my $category = $c->req->param('category'); my $current_page = $c->req->param('page') || 1; # multiple values for single parameter name my @values = $c->req->param('scrolling_list'); # DFV requires a CGI.pm-like input hash my $results = Data::FormValidator->check($c->req->params, ?%dfv_profile);
フローコントロール
(which accepts the key of an action to executeがわからん)
forwardメソッドを使ってフローをコントロールする。アクションは同じコントローラ内でも他のCatalystコントローラでも、または単なるクラス名(メソッド名が続いてもいい)でもいい。forwardの後、コントロールフローは呼び出し元に戻ってくる。
forwardはメソッドコールに似ている。主要な違いは例外を捕捉できるようにevalしていることだ。また、コンテキストオブジェクト($cまたは$context)を自動的に渡すので、それぞれの呼び出しのプロファイリングを可能にする(訳注:よくわからん)(デバッグを有効にしていればログに表示される)。
sub hello : Global { my ( $self, $c ) = @_; $c->stash->{message} = 'Hello World!'; $c->forward('check_message'); # $c is automatically included } sub check_message : Private { my ( $self, $c ) = @_; return unless $c->stash->{message}; $c->forward('show_message'); } sub show_message : Private { my ( $self, $c ) = @_; $c->res->body( $c->stash->{message} ); }
forwardは新しいリクエストを発行するわけじゃないから、リクエストオブジェクト($c->req)はそのままで変わらない。これがforwardを使うのとリダイレクトするのとの違いだ。
無名配列(訳注:と書いてあるけど下のソースは配列のリファレンスになってる)を使って新しい引数を渡すこともできる。この場合、$c->req->argsはforwardの間だけ変化して戻ってきたら元の値にリセットされる。
sub hello : Global { my ( $self, $c ) = @_; $c->stash->{message} = 'Hello World!'; $c->forward('check_message',[qw/test1/]); # now $c->req->args is back to what it was before } sub check_message : Private { my ( $self, $c ) = @_; my $first_argument = $c->req->args[0]; # now = 'test1' # do something... }
例を見ればわかるが、同じコントローラ内のメソッドを参照する限りはただ単にメソッド名を使えばよい。もし他のコントローラ内のメソッドまたはメインアプリケーションにforwardしたかったらメソッド名を絶対パスで参照する必要がある。
$c->forward('/my/controller/action'); $c->forward('/default'); # calls default in main application
ここにクラスとメソッドへのforwardの仕方をいくつか示す。
sub hello : Global { my ( $self, $c ) = @_; $c->forward(qw/MyApp::Model::Hello say_hello/); } sub bye : Global { my ( $self, $c ) = @_; $c->forward('MyApp::Model::Hello'); # no method: will try 'process' } package MyApp::Model::Hello; sub say_hello { my ( $self, $c ) = @_; $c->res->body('Hello World!'); } sub process { my ( $self, $c ) = @_; $c->res->body('Goodbye World!'); }
forwardが呼び出し元に戻って処理を続けるということに注意すること。もし呼び出し元の処理を続けたくなければ、代わりにdetachを使う。detachはデタッチされたアクションを実行してから呼び出し元に戻らない。また、いずれの場合もメソッド名を省略するとCatalystはprocess()を自動的にコールしようとする。
コンポーネント
Catalystはきわめて柔軟なコンポーネントシステムを備えている。そのため、好きなだけモデル、ビュー、コントローラを定義できる。
すべてのコンポーネントはCatalyst::Baseを継承しなければならない。それはシンプルなクラス構造とconfigやnew(コンストラクタ)のようなクラスメソッドをいくつか提供する。
package MyApp::Controller::Catalog; use strict; use base 'Catalyst::Base'; __PACKAGE__->config( foo => 'bar' ); 1;
モデル、ビュー、コントローラをuseしたり登録したりする必要はない。メインアプリケーションでsetupをコールしたときにCatalystが自動的に検出してインスタンス化してくれる。あなたがやらなきゃいけないことは各コンポーネントタイプのディレクトリに置いておくことだけだ。そのディレクトリにはそれぞれにとても簡単なエイリアスを使えることに注意。
- MyApp/Model/
- MyApp/M/
- MyApp/View/
- MyApp/V/
- MyApp/Controller/
- MyApp/C/
ビュー
ビューの定義方法を示すために、Template Toolkit用に用意されているベースクラス、Catalyst::View::TTを使う。やらなきゃいけないことはこのクラスから継承することだけだ。
package MyApp::View::TT; use strict; use base 'Catalyst::View::TT'; 1;
(ヘルパースクリプトを使って自動生成することもできる。
script/myapp_create.pl view TT TT
ここで最初のTTはビューの名前がTTだということ、二つ目はTemplate Toolkiを使うってことを意味する。)
process()メソッドが継承されたので、あとはただ、テンプレートを描画するために$c->forward('MyApp::View::TT')とすればよい。
sub hello : Global { my ( $self, $c ) = @_; $c->stash->{template} = 'hello.tt'; } sub end : Private { my ( $self, $c ) = @_; $c->forward('MyApp::View::TT'); }
通常はリクエストの最後にテンプレートを描画するだろうから、これはendアクションにぴったりの役目だ。
テンプレートのファイルはちゃんと$c->config->{root}が示すディレクトリに入れておくこと。さもないとステキなデバッグスクリーンとご対面、ということになるだろう。
モデル
モデルの定義方法を示すために、さっきと同様、今度はClass::DBI用のCatalyst::Model::CDBIを使おう。
でも最初に、データベースが必要だ。
myapp.sql CREATE TABLE foo ( id INTEGER PRIMARY KEY, data TEXT ); CREATE TABLE bar ( id INTEGER PRIMARY KEY, foo INTEGER REFERENCES foo, data TEXT ); INSERT INTO foo (data) VALUES ('TEST!'); % sqlite /tmp/myapp.db < myapp.sql
これで、このデータベース用のCDBIコンポーネントを作れる。
package MyApp::Model::CDBI; use strict; use base 'Catalyst::Model::CDBI'; __PACKAGE__->config( dsn => 'dbi:SQLite:/tmp/myapp.db', relationships => 1 ); 1;
Catalystは自動的にテーブルのレイアウトや依存間関係をロードする。テンプレートにデータを渡すにはスタッシュを使う。
package MyApp; use strict; use Catalyst '-Debug'; __PACKAGE__->config( name => 'My Application', root => '/home/joeuser/myapp/root' ); __PACKAGE__->setup; sub end : Private { my ( $self, $c ) = @_; $c->stash->{template} ||= 'index.tt'; $c->forward('MyApp::View::TT'); } sub view : Global { my ( $self, $c, $id ) = @_; $c->stash->{item} = MyApp::Model::CDBI::Foo->retrieve($id); } 1; # Then, in a TT template: The id is [% item.data %]
モデルはCatalystアプリケーションの一部でなくてもよく、モデルの役割を果たす外部モジュールをコールすることもできる。
# in a Controller sub list : Local { my ( $self, $c ) = @_; $c->stash->{template} = 'list.tt'; use Some::Outside::CDBI::Module; my @records = Some::Outside::CDBI::Module->retrieve_all; $c->stash->{records} = ?@records; }
でも、モデルをCatalystアプリケーションの1つにしたほうがいくつかいいことがある。
幸いなことに、みんながCatalystで使いたがるようなモデルクラスは既に存在するだろう。またCatalystのモデルクラスを書くのは簡単だ。
package MyApp::Model::Catalog; use base qw/Catalyst::Base Some::Other::CDBI::Module::Catalog/; 1;
これだけ。これでSome::Other::CDBI::Module::CatalogがMyApp::Model::CatalogとしてCatalystの一部となった。
コントローラ
複数のコントローラってのは、アプリケーションの領域(ドメイン)を論理的に分割するグッドな方法だ。
package MyApp::Controller::Login; sub sign-in : Local { } sub new-password : Local { } sub sign-out : Local { } package MyApp::Controller::Catalog; sub view : Local { } sub list : Local { } package MyApp::Controller::Cart; sub add : Local { } sub update : Local { } sub order : Local { }
テスト
Catalustにはテスト用のビルトインHTTPサーバーがある(そのあとで、製品用の環境としてより強力なサーバーに簡単に移行できる)。
コマンドラインからアプリケーションをスタートする。
script/myapp_server.pl
それからブラウザでhttp://localhost:3000/をみれば出力を確認できるだろう。
また、コマンドラインから全部やることもできる。
script/myapp_test.pl http://localhost/
おわり。