Catalyst::Manual::Cookbook - Catalystでお料理を
ママが昔よく焼いてくれたおいしいコード!
endアクションでdie()を呼び出すと、リクエストの最後にデバッグ画面を強制表示させることができます。
sub end : Private {
my ( $self, $c ) = @_;
die "forced debug";
}
いちいちこれを書いたり消したりするのが面倒なら、endアクションにこんな条件文を加えることもできます。
sub end : Private {
my ( $self, $c ) = @_;
die "forced debug" if $c->req->params->{dump_info};
}
こうしておくと、たとえばクエリストリングに"&dump_info=1"などと書き加えるだけでデバッグ画面を強制できます。
デバッグ画面のあのすばらしい統計情報を表示させたくない場合はアプリケーション・クラスに次の一行を加えてください。
sub Catalyst::Log::info { }
Catalyst を使ったスキャフォールディングは非常に簡単です。
おすすめはCatalyst::Helper::Controller::Scaffoldを使う方法です。
このモジュールをインストールしておけば、次のようにするだけでClass::DBIのモデル・クラスの土台をつくれます。
./script/myapp_create controller <name> Scaffold <CDBI::Class>Scaffolding
Catalystでアップロードを実装するには次のようなHTMLフォームを用意する必要があります。
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="hidden" name="form_submit" value="yes">
<input type="file" name="my_file">
<input type="submit" value="Send">
</form>
フォームには忘れずにenctype="multipart/form-data"を書いておくのがポイントです。
Catalystのコントローラ・モジュールには次のような「upload」アクションを書きます。
sub upload : Global {
my ($self, $c) = @_;
if ( $c->request->parameters->{form_submit} eq 'yes' ) {
if ( my $upload = $c->request->upload('my_file') ) {
my $filename = $upload->filename;
my $target = "/tmp/upload/$filename";
unless ( $upload->link_to($target) || $upload->copy_to($target) ) {
die( "Failed to copy '$filename' to '$target': $!" );
}
}
}
$c->stash->{template} = 'file_upload.html';
}
ひとつのフォームで複数のファイルをアップロードする場合はいくつか修正を加える必要があります。
フォームは基本的にこのような構成になります。
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="hidden" name="form_submit" value="yes">
<input type="file" name="file1" size="50"><br>
<input type="file" name="file2" size="50"><br>
<input type="file" name="file3" size="50"><br>
<input type="submit" value="Send">
</form>
コントローラはこうなります。
sub upload : Local {
my ($self, $c) = @_;
if ( $c->request->parameters->{form_submit} eq 'yes' ) {
for my $field ( $c->req->upload ) {
my $upload = $c->req->upload($field);
my $filename = $upload->filename;
my $target = "/tmp/upload/$filename";
unless ( $upload->link_to($target) || $upload->copy_to($target) ) {
die( "Failed to copy '$filename' to '$target': $!" );
}
}
}
$c->stash->{template} = 'file_upload.html';
}
for my $field ($c->req-upload)ですべてのファイル・インプット・フィールドから自動的に入力ファイル名を取得するようにします。あとは単一ファイルのアップロードのときとよく似た基本的なファイル保存のコードです。
注意:エラーが起こったときにdieするのは望ましい動作ではないかもしれません。上の例のように動作はしますが、むしろ$!の値を$c->stash->{error}に保存して、カスタム・エラー・テンプレートでエラー・メッセージを表示するようにした方がよいでしょう。
アップロードと利用可能なメソッドの詳細はCatalyst::Request::UploadやCatalyst::Requestをご覧ください。
このプラグインを使った認証には(少なくとも)2通りの実装方法があります。1) ユーザ名とパスワードのみを確認する 2) ユーザ名、パスワード、ユーザの権限を確認する
いずれの場合も、MyAppパッケージの中に次のようなコードを書く必要があります。
use Catalyst qw/Session::FastMmap Static Authentication::CDBI/;
MyApp->config( authentication => { user_class => 'MyApp::M::MyApp::Users',
user_field => 'email',
password_field => 'password' });
「user_class」はusersテーブル用のClass::DBIクラスです。「user_field」ではどのフィールドをユーザ名の検索に利用するかを指定します(email、first name、surnameなど)。「password_field」は、まあ、テーブルに含まれるパスワード・フィールドです。デフォルトではパスワードではプレーン・テキストで保存されます。特に設定されていない場合、Authentication::CDBIは「user」フィールドと「password」フィールドを検索します。
PostgreSQLの場合、usersテーブルはこんな感じのものになるでしょう。
CREATE TABLE users ( user_id serial, name varchar(100), surname varchar(100), password varchar(100), email varchar(100), primary key(user_id) );
ここではまず最初のやり方、権限をチェックしない、ユーザ名とパスワードのみのログイン/認証について説明します。
あるユーザとしてログインするには次のようなアクションを使います。
sub login : Local {
my ($self, $c) = @_;
if ($c->req->params->{username}) {
$c->session_login($c->req->params->{username},
$c->req->params->{password} );
if ($c->req->{user}) {
$c->forward('/restricted_area');
}
}
}
このアクションはMyAppクラスに入れないでください……入れてしまうと同名のビルトイン・メソッドと衝突します。そのかわりにコントローラ・クラスに入れてください。
$c->req->params->{username}と$c->req->params->{password}はログイン・フォームから取得したフォーム・パラメータです。ログインが成功した場合、$c->req->{user}には認証に成功したユーザのユーザ名が入ります。
以降のリクエストでもユーザのログイン状態を記憶しておきたい場合は$c->session_loginメソッドを使ってください。CatalystはセッションIDとセッション・クッキーを作った上で、自動的にすべてのURLにセッションIDを付与します。あとは必要なときに$c->req->{user}をすればよしです。
ログアウトさせるときは$c->session_logoutを呼んでください。
では、二番目のやり方を見てみましょう。権限を確認しながらユーザ名とパスワードでログイン/認証します。
権限を利用するには、MyApp->configの「authentication」セクションに以下のパラメータを加える必要があります。
role_class => 'MyApp::M::MyApp::Roles',
user_role_class => 'MyApp::M::MyApp::UserRoles',
user_role_user_field => 'user_id',
user_role_role_field => 'role_id',
対応するテーブルをPostgreSQLで作ると次のようになります。
CREATE TABLE roles ( role_id serial, name varchar(100), primary key(role_id) ); CREATE TABLE user_roles ( user_role_id serial, user_id int, role_id int, primary key(user_role_id), foreign key(user_id) references users(user_id), foreign key(role_id) references roles(role_id) );
「roles」テーブルは権限名の一覧です。「user_roles」テーブルはユーザから権限を検索するときに利用します。
ここで、ログイン済みのユーザが「admin」権限を持つ人にのみ許されたページを見ようとしているとします。コントローラでは次のようなチェックを行えます。
sub add : Local {
my ($self, $c) = @_;
if ($c->roles(qw/admin/)) {
$c->res->output("Your account has the role 'admin.'");
} else {
$c->res->output("You're not allowed to be here.");
}
}
認証されていないユーザが制限領域にアクセスしようとしたときにはログイン・フォームに飛ばす必要があるかもしれません。これをコントローラ全体で行いたい場合は(adminセクションにひとつのコントローラしかない場合は)、「begin」アクションにユーザ・チェックを追加するのが一番です。
sub begin : Private {
my ($self, $c) = @_;
unless ($c->req->{user}) {
$c->req->action(undef); ## ここに注目!!
$c->forward('/user/login');
}
}
$c->req->action(undef) に注目してください。こうする必要があるのは $c->forward の動作の仕方によるものです――つまり、login への forward が呼ばれても、Catalyst はそこからさらに URI が定義しているアクションを実行してしまうためです(たとえば、/add に行こうとしていたとすると、最初に「begin」で「login」に飛んだ後に、「add」が実行されてしまうのです)。そこで、$c->req->action(undef) で呼び出されることになっているすべてのアクションを無効化して、ユーザをしかるべき場所に飛ばせるようにするのです。
でも、必要なのはそれだけです。
ログインやサイレント・コマンドのように、本当の目的とは90度異なる別のリクエストの処理中に発生する一連のアクションをまとめる簡単な方法があります。それぞれのアクションを用意した上で、別の目的で必要とされるときには、たとえば__login のようなフォーム変数に値を入れて、次のような begin サブルーチンを通してください。
sub begin : Private {
my ($self, $c) = @_;
foreach my $action (qw/login docommand foo bar whatever/) {
if ($c->req->params->{"__${action}"}) {
$c->forward($action);
}
}
}
Catalystアプリケーションはmod_perl環境下で実行したときに最高のパフォーマンスを出せるようになっていますが、ときにはmod_perlが選択肢になく、CGIで実行するのは遅すぎる場合もあります。そんなとき、mod_perlの代用品として、リーズナブルなパフォーマンスを出せるものにFastCGIがあります。
http://www.fastcgi.com/から引用すると、「FastCGIとは言語から独立した、スケーラブルなCGI拡張で、特定のサーバAPIに制限されることなく、高いパフォーマンスを提供」するものです。Apache用のウェブ・サーバ・サポートはmod_fastcgiとして、またPerlサポートはFCGIモジュールとして提供されています。CGI用のCatalystアプリケーションをFastCGI用に変換するにはFCGI::Requestオブジェクトを初期化して、Acceptメソッドがゼロを返す間ループする必要があります。以下にそのやり方を示します。また、通常の、一回かぎりのCGIスクリプトとして実行することもできます。
#!/usr/bin/perl
use strict;
use FCGI;
use MyApp;
my $request = FCGI::Request();
while ($request->Accept() >= 0) {
MyApp->run;
}
初期化コードはすべてrequest-acceptループの外に置いてください。
ひとつだけやっかいなことがあります。MyApp->runはステータス行を含む完全なHTTPレスポンス(たとえば「HTTP/1.1 200」)を出力するのですのですが、FastCGIはヘッダ部分のみを要求しますので、サンプル・コードでは出力をキャプチャして先頭行がHTTPステータス行なら削除するようにしています(注意:これは変更されるかもしれません)。
Apacheのmod_fastcgiモジュールは多くのLinuxディストリビューションが提供していますし、ほとんどのUnix風システムで簡単にコンパイルできます。このモジュールはFastCGIスクリプトを管理するプロセス・マネージャを提供します。自作したスクリプトをFastCGI用に設定するには、Apacheの設定ディレクティブをこのように設定してください。
<Location /fcgi-bin>
AddHandler fastcgi-script fcgi
</Location>
または
<Location /fcgi-bin>
SetHandler fastcgi-script
Action fastcgi-script /path/to/fcgi-bin/fcgi-script
</Location>
mod_fastcgiには実行中のFastCGIスクリプトを管理するさまざまなオプションがあります。また、スクリプトに認証や認可、アクセスチェックといったフェーズを扱わせることもできます。
くわしくはFastCGIのドキュメントや、FCGIモジュール、http://www.fastcgi.com/をご覧ください。
Catalystで静的コンテンツをサーブするのはいささかトリッキーな作業になることがあります。このレシピで紹介するのは取りうる解決法のひとつですが、これを使うと、Catalyst経由の静的コンテンツはすべて、開発時にはビルトインのHTTP::Deamonサーバを通してサーブできます。また、本番環境に移行したときには簡単にApacheによるサーブに切り替えられます。
静的コンテンツのサーブはルート・ディレクトリ以下にあるひとつのディレクトリからに限るのが一番です。root/cssとroot/imagesのように複数の異なるディレクトリを使うようにすると管理しなければならないコードが増えます。というのも、これらの静的ディレクトリはひとつひとつ別々に指定する必要がある――たとえば、root/jsディレクトリを追加することに決めたら、それを含めるようコードを修正する必要があるためです。逆に、すべての静的ディレクトリをメインとなるroot/staticディレクトリのサブディレクトリにするとはるかに簡単に管理できます。典型的なルート・ディレクトリ以下の構造はたとえばこうです。
root/
root/content.tt
root/controller/stuff.tt
root/header.tt
root/static/
root/static/css/main.css
root/static/images/logo.jpg
root/static/js/code.js
静的コンテンツはすべてroot/staticの下にあります。それ以外はすべてTemplate Toolkitファイルです。これで、Catalyst内部からstaticへのマッチングを取れば静的コンテンツを指定できます。
静的ファイルをスタンドアロン・サーバでサーブするには、まずStaticプラグインをロードする必要があります。まだの場合はCatalyst::Plugin::Staticをインストールしておいてください。
メイン・アプリケーション・クラス(MyApp.pm)でプラグインをロードします。
use Catalyst qw/-Debug FormValidator Static OtherPlugin/;
また、endメソッドでは静的コンテンツをビューにforward「しない」ようにする必要もあります。こんな感じになるでしょう。
sub end : Private {
my ( $self, $c ) = @_;
$c->forward( 'MyApp::V::TT' )
unless ( $c->res->body || !$c->stash->{template} );
}
こうすると、事前にテンプレートがコントローラで定義されていて、$c->res->bodyにまだデータが入っていない場合にのみビューにforwardするようになります。
次に、コントローラを作成して、/staticパスへのリクエストを処理できるようにします。時間を節約するためにヘルパーを使いましょう。次のコマンドでlib/MyApp/C/Static.pmというスタブ・コントローラが作成されます。
$ script/myapp_create.pl controller Static
できたファイルを編集して、次のメソッドを追加します。
# /static 以下にあるすべてのファイルを静的ファイルとしてサーブします。
sub default : Path('/static') {
my ( $self, $c ) = @_;
# オプションとして、ブラウザがコンテンツをキャッシュできるようにします。
$c->res->headers->header( 'Cache-Control' => 'max-age=86400' );
$c->serve_static; # Catalyst::Plugin::Staticより
}
# また、/favicon.ico へのリクエストも処理します
sub favicon : Path('/favicon.ico') {
my ( $self, $c ) = @_;
$c->serve_static;
}
また、これをHTMLヘッダで使うとfavicon.icoとは別のブラウザ向けアイコンを定義することもできます。
<link rel="icon" href="/static/myapp.ico" type="image/x-icon" />
Staticプラグインはshared-mime-infoパッケージを利用してMIMEタイプを自動判別していますが、このパッケージはインストールが難しいことでも有名です。特にwin32とOSXではそうです。OSXの場合はFinkをインストールして、apt-get install shared-mime-infoするのが一番かもしれません。サーバを再起動すれば万事うまくいくはずです。
最高の結果を得るためにはかならず最新版(≧ 0.16)を使うようにしてください。CSSファイルをサーブするときにエラーが出る、あるいはtext/cssではなくtext/plainとしてサーブされるようなら、shared-mime-infoのバージョンが古いのかもしれません。もっとも、Staticコントローラでこのようなコードを書いておけばいいと考える方もいるかもしれません。
if ($c->req->path =~ /css$/i) {
$c->serve_static( "text/css" );
} else {
$c->serve_static;
}
Apacheを使うときはroot/staticパスへのリクエストをサーバ・レベルでインターセプトすることでCatalystとStaticコントローラをまったく使わずに済ますこともできます。必要なのはDocumentRootを設定して、静的コンテンツに対して独立したLocationブロックを追加するだけのことです。mod_perl 1.x環境下での完全な設定ファイルはこうなります。
<Perl>
use lib qw(/var/www/MyApp/lib);
</Perl>
PerlModule MyApp
<VirtualHost *>
ServerName myapp.example.com
DocumentRoot /var/www/MyApp/root
<Location />
SetHandler perl-script
PerlHandler MyApp
</Location>
<LocationMatch "/(static|favicon.ico)">
SetHandler default-handler
</LocationMatch>
</VirtualHost>
もっと簡単な例です。最初はこれでもいいでしょう。
Alias /static/ "/my/static/files/"
<Location "/static">
SetHandler none
</Location>
時には別のアクションにforwardするときに引数を渡したくなることもあります。バージョン5.30からはforwardを呼ぶときに引数を渡せるようになりましたが、バージョンが古い場合でも、Catalyst Requestオブジェクトに手作業で引数をセットできます。
# バージョン5.30以降
$c->forward('/wherever', [qw/arg1 arg2 arg3/]);
# 5.30以前
$c->req->args([qw/arg1 arg2 arg3/]);
$c->forward('/wherever');
(forward経由で引数を渡す方法の詳細についてはCatalyst::Manual::Introのフロー管理をご覧ください)
アプリケーション・クラスのconfigメソッドを使うとアプリケーションの設定をできます。この設定はハード・コードすることもできますし、独立した設定ファイルを取り込むようにすることもできます。
YAMLは柔軟で読みやすい設定ファイルを作成するための手法です。このすばらしい手法を使うと、Catalystアプリケーションの設定をひとつの理解しやすいファイルにまとめておけます。
アプリケーション・クラス(たとえばlib/MyApp.pm)ではこうします。
use YAML;
# アプリケーションのセットアップ
__PACKAGE__->config( YAML::LoadFile(__PACKAGE__->config->{'home'} . '/myapp.yml') );
__PACKAGE__->setup;
今度はアプリケーションのホーム・ディレクトリにmyapp.ymlを作成します。
--- #YAML:1.0
# インデントやラベルと値の区切りにタブを使わないこと!!!
name: MyApp
# 認証:perldoc Catalyst::Plugin::Authentication::CDBI
authentication:
user_class: 'MyApp::M::MyDB::Customer'
user_field: 'username'
password_field: 'password'
password_hash: 'md5'
role_class: 'MyApp::M::MyDB::Role'
user_role_class: 'MyApp::M::MyDB::PersonRole'
user_role_user_field: 'person'
# セッション:perldoc Catalyst::Plugin::Session::FastMmap
session:
expires: '3600'
rewrite: '0'
storage: '/tmp/myapp.session'
# メール:perldoc Catalyst::Plugin::Email
# これはオプションをひとつの配列として渡します orz
email:
- SMTP
- localhost
これは次のようにするのと同じです。
# ベース・パッケージの設定
__PACKAGE__->config( name => MyApp );
# 認証の設定
__PACKAGE__->config->{authentication} = {
user_class => 'MyApp::M::MyDB::Customer',
...
};
# セッションの設定
__PACKAGE__->config->{session} = {
expires => 3600,
...
};
# メール送信の設定
__PACKAGE__->config->{email} = [qw/SMTP localhost/];
YAMLもご覧ください。
多くの人々がCatalystで既存のモデル・クラスを使いたがるため(あるいは、逆にCatalystの外部で――たとえばcronのジョブとして――も使えるCatalystモデルを書きたがるため)、外部のモデルでも動くシンプルなCatalystコンポーネントも簡単に書けるようになっています。
package MyApp::M::Catalog;
use base qw/Catalyst::Base Some::Other::CDBI::Module::Catalog/;
1;
はい、できた、と! これでSome::Other::CDBI::Module::CatalogがMyApp::M::CatalogとしてCatalystアプリケーションの一部になるのです。
Catalystはアプリケーションの中でエラーが発生すると独自のエラー・ページを表示するのがデフォルトとなっています。このエラー・ページは、-Debugモードで実行しているときはエラー・メッセージや$cコンテキスト・オブジェクトの内容をすべてData::Dumperで出力したものが表示されて便利なのですが、-Debugモードでないときはシンプルに「Please come back later」という画面になってしまいます。
カスタム・エラー・ページを利用するには、特別なendメソッドを用意してエラー処理を短絡させてください。次に一例をあげますが、アプリケーションのニーズにあわせてさらに修正した方がよいかもしれません(たとえば、fillformが呼ばれているときはおそらくすべてこのendメソッドに飛ぶ必要があるでしょう。Catalyst::Plugin::FillInFormをご覧ください)。
sub end : Private {
my ( $self, $c ) = @_;
if ( scalar @{ $c->error } ) {
$c->stash->{errors} = $c->error;
$c->stash->{template} = 'errors.tt';
$c->forward('MyApp::View::TT');
$c->error(0);
}
return 1 if $c->response->status =~ /^3\d\d$/;
return 1 if $c->response->body;
unless ( $c->response->content_type ) {
$c->response->content_type('text/html; charset=utf-8');
}
$c->forward('MyApp::View::TT');
}
このエラー・ページは、コードの中で手動でエラーを設定して呼び出すこともできます。
$c->error( 'You broke me!' );
アプリケーションへのアクセスは登録ユーザのみに限定し、それ以外の人はサイン・インするまで強制的にログイン・ページに飛ばすようにすると便利なことが多いものです。
このような実装をするには、まずusernameとpasswordというフィールドを持つ顧客テーブルと、対応するモデル・クラスを用意してください。続いて、次のような変更を行います。
use Catalyst qw/Session::FastMmap Authentication::CDBI/;
__PACKAGE__->config->{authentication} = {
'user_class' => 'ScratchPad::M::MyDB::Customer',
'user_field' => 'username',
'password_field' => 'password',
'password_hash' => '',
};
sub auto : Private {
my ($self, $c) = @_;
my $login_path = 'user/login';
# 実際にログインページにたどり着けるようにします!
if ($c->req->path eq $login_path) {
return 1;
}
# ユーザが登録されていればOKです
if ( $c->req->user ) {
$c->session->{'authed_user'} =
MyApp::M::MyDB::Customer->retrieve(
'username' => $c->req->user
);
}
# そうでなければログインしていないということ
else {
# force the login screen to be shown
$c->res->redirect($c->req->base . $login_path);
}
# 処理を続行します
return 1;
}
sub login : Path('/user/login') {
my ($self, $c) = @_;
# デフォルトのテンプレート
$c->stash->{'template'} = "user/login.tt";
# デフォルトのフォーム・メッセージ
$c->stash->{'message'} = 'Please enter your username and password';
if ( $c->req->param('username') ) {
# ログインを試みます
$c->session_login(
$c->req->param('username'),
$c->req->param('password'),
);
# ユーザを取得できたらログイン成功です
if ( $c->req->user ) {
$c->res->redirect('/some/page');
}
# そうでなければログイン失敗。もう一度!
else {
$c->stash->{'message'} =
'Unable to authenticate the login details supplied';
}
}
}
sub logout : Path('/user/logout') {
my ($self, $c) = @_;
# ログアウトして、保持していた情報を削除します
$c->session_logout;
delete $c->session->{'authed_user'};
# 「デフォルト」のアクションを実行します
$c->res->redirect($c->req->base);
}
[% INCLUDE header.tt %]
<form action="/user/login" method="POST" name="login_form">
[% message %]<br />
<label for="username">username:</label><br />
<input type="text" id="username" name="username" /><br />
<label for="password">password:</label><br />
<input type="password" id="password" name="password" /><br />
<input type="submit" value="log in" name="form_submit" />
</form>
[% INCLUDE footer.tt %]
Sebastian Riedel, sri@oook.de Danijel Milicevic, me@danijel.de Viljo Marrandi, vilts@yahoo.com Marcus Ramberg, mramberg@cpan.org Jesse Sheidlower, jester@panix.com Andy Grundman, andy@hybridized.org Chisel Wright, pause@herlpacker.co.uk
石垣憲一(ishigaki_at_tcool.org) http://www.tcool.org/
This program is free software, you can redistribute it and/or modify it under the same terms as Perl itself.