近頃の Web + DB なアプリケーションは MVC でモデルは O/R マッピング、みたいなアーキテクチャが主流です。その際 MVC フレームワークを使って作るのはいわずもがなですが、最近 Ruby 界隈(?)では Ruby on Rails、Perl 界隈では Catalyst というのが熱い模様です。Java 界隈では Spring が熱いのかな?
Perl の O/R マッピングのデファクトは多分 Class::DBI で、Class::DBI と相性が良いテンプレートエンジンと言えば Template-Toolkit。という感じで、自然とモデルとビューに何を使うかは決まってきます。そこであとはコントローラ、というわけですが、Catalyst は主にそのコントローラの部分です。CDBI + TT なアプリケーション向けのコントローラですが、モデルやテンプレートは CDBI と TT に限定されてるわけではないので、別の組み合わせでも使えます。
と、いうことでちょっとお勉強がてらいじってみました。以下、そのメモというか解説です。Perl でウェブアプリケーション作りたいけど良いフレームワークはないかなあ、とお嘆きの方は一読していただければこれ幸い。
何はなくともまずは準備から。CPAN から Catalyst モジュールをインストールします。
[naoya@colinux naoya]$ sudo perl -MCPAN -e 'install Bundle::Catalyst' [naoya@colinux naoya]$ sudo perl -MCPAN -e 'install Bundle::Catalyst::Everything'
これで Catalyst に必要なモジュールは全部入る...といいたいところですがデータベースに何を使うかによって、自動では入ってくれないものもいくつかあります。MySQL を使うなら Class::DBI::Loader::mysql、Class::DBI::mysql あたりを明示的にインストールする必要があるかも。(もちろん DBD::mysql も。)
普通ならここでいったんフレームワークのインストール作業は終えて、ウェブサーバーのセットアップとかに入るところなんですが、Catalyst にはデバッグ用の、コマンドラインから実行できる httpd が付属してくるのでその必要はないです。これが結構便利です。
さて、何を作ろうか...というところですがとりあえずお勉強目的なので qootas.org でも取り上げられている TinyURL っぽいものを作ってみました。qootas.org の記事では Catalyst のバージョン 4.34 を使っていますが、最新の 5.10 ではコントローラ周りの API が結構変わってたりするので、同じようなものを作るのでも調べごとが必要そうですし、題材としては良さそだった、ということで。(なので記事の中身は qootas.org のそれとかなり被っています。)
仕様としては
といったところです。本物の TinyURL は短縮URL の識別子にランダムな文字列を使っていますが、ここでは簡単のため http://.../23 といった感じで auto_increment な数値を使うことにしました。
あらかじめ、MySQL 上にこんな感じのテーブルを作成しておきます。データベース名は tinyurl、テーブル名は urlmap です。
mysql> desc urlmap; +-------+------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------+------------------+------+-----+---------+----------------+ | id | int(10) unsigned | | PRI | NULL | auto_increment | | url | varchar(255) | | | | | +-------+------------------+------+-----+---------+----------------+
ここから Catalyst を使った開発に入ります。最初にウェブアプリケーションのパッケージ名を決めます。ここでは NDO::TinyURL としました。パッケージ名を決めたら、catalyst.pl を実行します。すると、各種ディレクトリや Helper スクリプトが生成されます。
[naoya@colinux naoya]$ catalyst.pl NDO::TinyURL created "NDO-TinyURL" created "NDO-TinyURL/script" created "NDO-TinyURL/lib" created "NDO-TinyURL/root" created "NDO-TinyURL/t" created "NDO-TinyURL/t/m" created "NDO-TinyURL/t/v" created "NDO-TinyURL/t/c" created "NDO-TinyURL/lib/NDO/TinyURL" created "NDO-TinyURL/lib/NDO/TinyURL/M" created "NDO-TinyURL/lib/NDO/TinyURL/V" created "NDO-TinyURL/lib/NDO/TinyURL/C" created "NDO-TinyURL/lib/NDO/TinyURL.pm" created "NDO-TinyURL/Build.PL" created "NDO-TinyURL/Makefile.PL" created "NDO-TinyURL/README" created "NDO-TinyURL/Changes" created "NDO-TinyURL/t/01app.t" created "NDO-TinyURL/t/02pod.t" created "NDO-TinyURL/t/03podcoverage.t" created "NDO-TinyURL/script/ndo_tinyurl_cgi.pl" created "NDO-TinyURL/script/ndo_tinyurl_fcgi.pl" created "NDO-TinyURL/script/ndo_tinyurl_server.pl" created "NDO-TinyURL/script/ndo_tinyurl_test.pl" created "NDO-TinyURL/script/ndo_tinyurl_create.pl"
次に、これは結構お約束な作業っぽいですが、Template-Toolkit (TT) をベースにしたビュークラスを作成します。といってもこれも Helper スクリプトを実行するだけ。
[naoya@colinux naoya]$ cd NDO-TinyURL/ [naoya@colinux NDO-TinyURL]$ script/ndo_tinyurl_create.pl view TT TT created "/home/naoya/NDO-TinyURL/script/../lib/NDO/TinyURL/V/TT.pm" created "/home/naoya/NDO-TinyURL/script/../t/v/tt.t"
引数には "view TT TT" としてます。(なんか他の人のを見てても TT TT としてるのが多かったので。) テンプレートエンジンに TT 以外の物を使う場合とかは "view MyViewer" とすると MyViewer.pm がスケルトンとして生成されるのでそこにコードを書いていくといいみたいです。
なお、この Helper で生成された NDO::TinyURL::V::TT クラスは Catalyst::View::TT を継承しているだけのクラスになります。Template-Toolkit で提供されている以上の機能をビューに追加したい場合とかは、このクラスにメソッドを定義していくと良いでしょう。
ビューの下準備はこれでおしまい。以降、ビューの開発は、コントローラを作りながらウェブアプリケーションの各アクションに合わせてテンプレートを書いていく作業になります。
次はモデルの作成です。通常 Class::DBI でモデルを作る場合は、データソースやテーブルのスキーマに合わせて、テーブル一つにたいして Class::DBI のサブクラスを一つ定義していく、ということをやりますが、Catalyst ではやはりここも Helper スクリプトにお任せできます。
[naoya@colinux NDO-TinyURL]$ script/ndo_tinyurl_create.pl model CDBI CDBI DBI:mysql:tinyurl nobody nobody created "/home/naoya/NDO-TinyURL/script/../lib/NDO/TinyURL/M/CDBI.pm" created "/home/naoya/NDO-TinyURL/script/../lib/NDO/TinyURL/M/CDBI" created "/home/naoya/NDO-TinyURL/script/../lib/NDO/TinyURL/M/CDBI/Urlmap.pm" created "/home/naoya/NDO-TinyURL/script/../t/m/cdbi_urlmap.t"
うーん、楽ちん。"model モデルのパッケージ名 モデルに使うクラス DSN [username] [password]" として実行します。まあ、Class::DBI を使うならお約束的に "model CDBI CDBI DBI:mysql:foobar user pass" でしょうか。
これで下準備は完了。作ったモデルを組み合わせてウェブアプリケーションを作成...つまりコントローラを作っていきます。コントローラのクラスは lib/NDO/TinyURL.pm です。スケルトンがあらかじめ生成されているので、それを書き換えていきます。
ちなみに、今回は / と /12345 と /make の3つしかアクションがないアプリケーションで、いずれも / 直下なので NDO/TinyURL.pm を編集していますが、一階層下って /foobar/action, /foobar/action2, /foobar/action3...なんてことをやる場合は
$ script/ndo_tinyurl_create.pl controller FooBar
として /foobar/ 以下のコントローラのスケルトンを生成し、lib/NDO/TinyURL/C/FooBar.pm を編集します。antipop2.0 - Catalyst で作る簡単 Web アプリケーション: Feed2JS 解説 にはその具体例が載ってます。
話がそれました。コントローラのコードはこんな感じになりました。バージョン 5.10 では sub action : Attribute とかしてメソッドを定義したりします。なんか Perl の Syntax 的に風変わりですが。この辺も詳しくは quootas.org : Catalyst入門: Actionの定義とその処理の流れ(前編) で解説されてたりします。
package NDO::TinyURL;
use strict;
use Catalyst qw/-Debug/;
our $VERSION = '0.01';
NDO::TinyURL->config(
name => 'TinyURL',
root => '/home/naoya/NDO-TinyURL/root',
templates => {
index => 'templates/index.tt',
make => 'templates/make.tt',
}
);
NDO::TinyURL->setup;
sub default : Private {
my ( $self, $c ) = @_;
$c->stash->{template} = NDO::TinyURL->config->{templates}->{index};
$c->forward('NDO::TinyURL::V::TT');
}
sub make : Global {
my ($self, $c) = @_;
my $url = $c->req->param('url') or $c->res->redirect('/');
$url = 'http://' . $url unless $url =~ /^http(?s):\/\//i;
my $map = NDO::TinyURL::M::CDBI::Urlmap->find_or_create({ url => $url});
$c->stash->{map} = $map;
$c->stash->{template} = NDO::TinyURL->config->{templates}->{make};
$c->forward('NDO::TinyURL::V::TT');
}
sub redirect : Regex('^(\d+)$') {
my ($self, $c) = @_;
my $map = NDO::TinyURL::M::CDBI::Urlmap->retrieve($c->req->snippets->[0]);
$map ? $c->res->redirect($map->url) : $c->res->redirect('/');
}
まず、/ にリクエストが来た場合を考えます。/ は default アクションになります。特に何もせずフォームのある画面を表示したいので、
sub default : Private {
my ( $self, $c ) = @_;
$c->stash->{template} = NDO::TinyURL->config->{templates}->{index};
$c->forward('NDO::TinyURL::V::TT');
}
として、$c->forward('NDO::TinyURL::V::TT') を実行します。$c は Context オブジェクトで、コントローラでは基本的にこの Context オブジェクトのメソッドを呼び出してほげほげ、ということをします。ここでは先に Helper スクリプトで用意したビュークラスに、処理の転送を行っています。
転送する前に $c->stash->{template} に、このアクション用のテンプレートファイルのパスをセットしています。なんか、テンプレートファイルのパスを stash に保存しておくというのがちょっと気持ちわるい気もしますが、そういうことみたいです。なお、stash や config にセットしておいた値はテンプレート側から自由にアクセス可能です。
/ 用のテンプレートは templates/index.tt と指定したので、それを用意します。実際の場所は root/templates/index.tt です。
<html> <head></head> <body> <h1>[% name %]</h1> <form action="make" method="post"> URL: <input type="text" name="url" size="30"> <input type="submit" value="tinyurl" type="submit"> </body> </html>
フォームがあるだけの画面です。[% name %] は TT のテンプレートタグで、TinyURL.pm の中で config にセットしておいた値を展開してます。
これで / は完了。
次は、URL が入力されたら短縮URLを作って画面に表示する /make です。
sub make : Global {
my ($self, $c) = @_;
my $url = $c->req->param('url') or $c->res->redirect('/');
$url = 'http://' . $url unless $url =~ /^http(?s):\/\//i;
my $map = NDO::TinyURL::M::CDBI::Urlmap->find_or_create({ url => $url});
$c->stash->{map} = $map;
$c->stash->{template} = NDO::TinyURL->config->{templates}->{make};
$c->forward('NDO::TinyURL::V::TT');
}
make : Global と書くと、/make に来たらこのアクションを呼びなさいという指示になります。Global 属性はこんな風に使います。ここでは Class::DBI の find_or_create メソッドで、入力された URL を urlmap テーブルに保存してます。あとは、stash にオブジェクトをセットしつつテンプレートを指定してビューに転送。
なんか url が入力されてなかったときは / に戻すということをやってますが、Catalyst::Plugin::FormValidator あたりを使ったほうが美しいかも。(Catalyst はエラーハンドリングに関する機能はフレームワークに組み込まれてないのかな、誰か教えて。)
root/templates/make.tt はこんな感じです。
<html> <head></head> <body> <h1>[% name %] created</h2> <p><a href="[% map.url %]" target="_blank">[% map.url %]</a> to <a href="/[% map.id %]" target="_blank">[% base %][% map.id %]</a></p> </ul> </body> </html>
最後に、/12345 に来たときにリダイレクトするアクション。
sub redirect : Regex('^(\d+)$') {
my ($self, $c) = @_;
my $map = NDO::TinyURL::M::CDBI::Urlmap->retrieve($c->req->snippets->[0]);
$map ? $c->res->redirect($map->url) : $c->res->redirect('/');
}
ここでは /make のように、アクションの名前は固定じゃなく変数である数値なのですが、このように Regex 属性を使うことでそれを実現できます。正規表現メモリに入れた変数($1) は Request オブジェクトの snippet メソッドで取り出せます。
これでアプリケーションは完成。Catalyst に付属の httpd でテストしましょう。
[naoya@colinux NDO-TinyURL]$ script/ndo_tinyurl_server.pl [Thu May 5 11:18:10 2005] [catalyst] [debug] Debug messages enabled [Thu May 5 11:18:10 2005] [catalyst] [debug] Loaded dispatcher "Catalyst::Dispatcher" [Thu May 5 11:18:10 2005] [catalyst] [debug] Loaded engine "Catalyst::Engine::HTTP" [Thu May 5 11:18:10 2005] [catalyst] [debug] Found home "/home/naoya/NDO-TinyURL/script/.." [Thu May 5 11:18:10 2005] [catalyst] [debug] Loaded tables "urlmap" [Thu May 5 11:18:11 2005] [catalyst] [debug] Loaded components .=----------------------------------------------------------------------------=. | NDO::TinyURL::M::CDBI | | NDO::TinyURL::V::TT | | NDO::TinyURL::M::CDBI::Urlmap | '=----------------------------------------------------------------------------=' [Thu May 5 11:18:11 2005] [catalyst] [debug] Loaded private actions .=-------------------------------------+--------------------------------------=. | Private | Class | |=-------------------------------------+--------------------------------------=| | /make | NDO::TinyURL | | /redirect | NDO::TinyURL | | /default | NDO::TinyURL | '=-------------------------------------+--------------------------------------=' [Thu May 5 11:18:11 2005] [catalyst] [debug] Loaded public actions .=-------------------------------------+--------------------------------------=. | Public | Private | |=-------------------------------------+--------------------------------------=| | /make | /make | '=-------------------------------------+--------------------------------------=' [Thu May 5 11:18:11 2005] [catalyst] [debug] Loaded regex actions .=-------------------------------------+--------------------------------------=. | Regex | Private | |=-------------------------------------+--------------------------------------=| | ^(\d+)$ | /redirect | '=-------------------------------------+--------------------------------------=' [Thu May 5 11:18:11 2005] [catalyst] [info] TinyURL powered by Catalyst 5.10 You can connect to your server at http://colinux:3000/
なにやらディスパッチ先はここがこうなってるとかいうステータスが色々出て、サーバーは localhsot の 3000 番にバインドしました。指示通りアクセスすると、
てな感じで画面が遷移し http://colinux:3000/2 にアクセスしたところちゃんとリダイレクトされました。めでたしめでたし。
まだ触りぐらいしか使ってないのですが、とりあえず Helper スクリプトと httpd が超ベンリ。最初引数がよくわかんなかったりしましたが、その辺はフレームワーク学習のためのコストのうちですね。慣れてくると Helper でセットアップ、コントローラとテンプレートをもりもり書いて、必要に応じてロジックをクラスにまとめて...というのの繰り返していくだけでお手軽にウェブアプリケーションを作ることができそうです。
フレームワークのアーキテクチャとしては、一人もしくは少人数向けのフレームワークかなあという印象です。今回正規表現でいじったように、URLとロジックのマッピングをコントローラでかなり柔軟にいじれて自由度が高いのは便利ですが、ちょっと自由度が高すぎるので、少人数ならよさそうだけど、と思った。大規模開発も可能でしょうけど、一番効果を発揮するのは Hacker な人が一人でどんどん作ってくケースかな。
今回ははてなで使っているはてなフレームワークとの比較のために勉強してみました。(アーキテクチャは Catalyst と結構違うんですが)はてなフレームワークは一人よりも少人数開発にフォーカスしたフレームワークで、もう少しプログラマに対する制約が強い。そのおかげでコードが均一になりやすいというメリットが得られてます。その辺の使用感覚と比べて、Catalyst は一人向けかなあと感じた次第。
プラグインでフレームワークそのものの機能を拡張していけるのが良いですね。Sledge もそうだけど、オープンソースなフレームワークはプラグインアーキテクチャでいかに Hacker を刺激するかがその発展の肝かもしれない。(あとドキュメンテーションw)
ちなみに、プラグインを書くには Catalyst::Plugin::Foobar というクラスを作ってその中にメソッドを定義します。ここで定義されたメソッドは Context オブジェクトのメソッドとして定義されます。使うためには use Catalyst qw /Foobar/ とします。プラグインそのものに機能を実装しても良いし、Catalyst::Plugin::Prototype のようにプラグインの中身が更に汎化できる場合は HTML::Prototype のようにそれを外出しのモジュールにしてしまっても良い、といった感じです。
と、いうことで結構使ってみて楽しかったので、次からプライベートで何か作るときは CGI::Application じゃなく Catalyst を使ってみてもいいかな、と思いました。おしまい。
参考にしたドキュメント