Catalyst::Manual::Cookbook

Catalyst::Manual::Cookbook - Cooking with Catalyst - metacpan.org

Basics

Catalystを使う人が知っておいた方がいいこと。

Delivering a Custom Error Page

アプリケーションでエラーが発生したときはCatalystは独自のエラーページを表示する。-Debugモードのときはエラーメッセージとコンテキストオブジェクト($c)のData::Dumpの出力を表示する。-Debugモードじゃないときは"Please come back later"が表示される。
エラーページを変更するにはendメソッドにエラー処理を書けばいい。例を示す。

    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!' );
Disable statistics

Just add this line to your application class if you don't want those nifty statistics in your debug messages.

    sub Catalyst::Log::info { }

これはよくわからなかった。アプリケーションクラスにこれ追加してみたけど再定義してるって怒られた上気の利いたデバッグページは相変わらず表示された。

Enable debug status in the environment

ふつうは-Debugフラグをuser Catalystに追加してデバッグ情報を有効にする。けど、環境変数を使ってもできるのでアプリケーションを変更しなくてもデバッグ情報を有効にできる。CATALYST_DEBUGか_DEBUGをtrueにすればいい。

Sessions

Catalystにはセッションを使うのに2つのプラグインを使う。

  • State

Stateモジュールはブラウザとアプリケーションの間で状態を追跡するのに使用する。

  • Store

StoreモジュールはユーザーIDやショッピングカートの商品など、セッションに関連するデータを保存するのに使用する。データはメモリ(FastMmap)、ファイル、データベースに格納可能。

  • Authentication magic

アプリケーションでセッションモジュールを使用していると、Authenticationモジュールはセッションを使ってユーザー情報の出し入れをするようになってる。

  • Using a session

セッションモジュールをロードすると$c->sessionを使ってデータを書いたり読んだりできる。データはハッシュのリファレンス。

  • EXAMPLE
  use Catalyst qw/
                 Session
                 Session::Store::FastMmap
                 Session::State::Cookie
                 /;


  ## Write data into the session

  sub add_item : Local {
     my ( $self, $c ) = @_;

     my $item_id = $c->req->param("item");

     push @{ $c->session->{items} }, $item_id;

  }

  ## A page later we retrieve the data from the session:

  sub get_items : Local {
     my ( $self, $c ) = @_;

     $c->stash->{items_to_display} = $c->session->{items};

  }
  • More information

http://search.cpan.org/dist/Catalyst-Plugin-Session
http://search.cpan.org/dist/Catalyst-Plugin-Session-State-Cookie
http://search.cpan.org/dist/Catalyst-Plugin-Session-State-URI
http://search.cpan.org/dist/Catalyst-Plugin-Session-Store-FastMmap
http://search.cpan.org/dist/Catalyst-Plugin-Session-Store-File
http://search.cpan.org/dist/Catalyst-Plugin-Session-Store-DBI

Configure your application

アプリケーションの設定はアプリケーションクラスのconfigメソッドで行う。

YAMLを使って設定を書ける。
アプリケーションクラスで(例えばlib/MyApp.pm)。

  use YAML;
  # application setup
  __PACKAGE__->config( YAML::LoadFile(__PACKAGE__->config->{'home'} . '/myapp.yml') );
  __PACKAGE__->setup;

アプリケーションのhomeにmyapp.ymlを作成する。

  --- #YAML:1.0
  # DO NOT USE TABS FOR INDENTATION OR label/value SEPARATION!!!
  name:     MyApp

  # session; perldoc Catalyst::Plugin::Session::FastMmap
  session:
    expires:        '3600'
    rewrite:        '0'
    storage:        '/tmp/myapp.session'

  # emails; perldoc Catalyst::Plugin::Email
  # this passes options as an array :(
  email:
    - SMTP
    - localhost

これは以下と等価。
(YAMLにはauthenticationの設定がないから違う気がするけど・・・)

  # configure base package
  __PACKAGE__->config( name => MyApp );
  # configure authentication
  __PACKAGE__->config->{authentication} = {
    user_class => 'MyApp::Model::MyDB::Customer',
    ...
  };
  # configure sessions
  __PACKAGE__->config->{session} = {
    expires => 3600,
    ...
  };
  # configure email sending
  __PACKAGE__->config->{email} = [qw/SMTP localhost/];

Skipping your VCS's directories

Catalystはモデル、ビュー、コントローラをロードするのにModule::Pluggableを使う。Module::Pluggableは全ディレクトリを調べて見つかったモジュールをロードする。それを回避したい場合、たとえばバージョンコントロールシステムが自動で作成するディレクトリとか。CatalystsubversionCVSディレクトリはすでにスキップするようになってるけど、他のソースコントロールシステムもあるので。スキップさせるためにはconfigを使う。

  # Configure the application
  __PACKAGE__->config(
      name => 'MyApp',
      setup_components => { except => qr/SCCS/ },
  );

Users and Access Control

ユーザー認証とロール定義がしたい場合のレシピ。

Pass-through login (and other actions)

ログインをスルーしたい場合ってことかな。
__loginみたいなパラメータがある場合にアクションする場合?

    sub begin : Private {
      my ($self, $c) = @_;
      foreach my $action (qw/login docommand foo bar whatever/) {
        if ($c->req->params->{"__${action}"}) {
          $c->forward($action);
        }
      }
    }
Role-based Authorization

ロールベースの権限分けをしたい場合。
login/logoutメソッドとテンプレートは前に出た例と同じ(Authenticationと同じってことかな)。
ロールを実装するにはCatalyst::Plugin::Authorization::Rolesプラグインが必要。

 use Catalyst qw/
    Authentication
    Authentication::Credential::Password
    Authentication::Store::Htpasswd
    Authorization::Roles
  /;

ロールはCatalyst::Authentication::Store::Htpasswdを使うと自動的に実装される。

  # no additional role configuration required
  __PACKAGE__->config->{authentication}{htpasswd} = "passwdfile";

またはCatalyst::Authentication::Store::DBICを使って手動でやる。

  # Authorization using a many-to-many role relationship
  __PACKAGE__->config->{authorization}{dbic} = {
    'role_class'           => 'My::Model::DBIC::Role',
    'role_field'           => 'name',
    'user_role_user_field' => 'user',

    # DBIx::Class only (omit if using Class::DBI)
    'role_rel'             => 'user_role',

    # Class::DBI only, (omit if using DBIx::Class)
    'user_role_class'      => 'My::Model::CDBI::UserRole'
    'user_role_role_field' => 'role',
  };

アクセス制限するためにはcheck_user_roleメソッドを使用する。

  sub restricted : Local {
     my ( $self, $c ) = @_;

     $c->detach("unauthorized")
       unless $c->check_user_roles( "admin" );

     # do something restricted here
  }

assert_user_rolesメソッドも使用可能。このメソッドはユーザーがロールを持ってなかった場合にエラーにする。

  sub also_restricted : Global {
    my ( $self, $c ) = @_;
    $c->assert_user_roles( qw/ user admin / );
  }
Authentication/Authorization

いくつかのステップから構成される。

  • Verification

ユーザーとアプリケーションだけが知っている情報を使ってユーザーを識別する。これはcredential verificationと呼ばれる。

  • Authorization

アクセスさせたいものだけにアクセスできるようにする。これはユーザーが内部で管理しているグループに属しているかや、あるページに対してユーザーを許可することで行う。

  • Modules

最大限の柔軟性を得るために、Catalystの認証システムは多数のモジュールが関連し合ってなりなっている。

  • Credential verifiers

Credentialモジュールはユーザー入力を受け取ってStoreまたは確認のために他のシステムに渡す。たいていはこのモジュールかStoreによってユーザーオブジェクトが作られ、$c->userでアクセス可能になる。
例。

Password - Simple username/password checking.
HTTPD - Checks using basic HTTP auth.
TypeKey - Check using the typekey system.

  • Storage backends

Storageバックエンドはユーザー情報を持ってる。ユーザー情報はcredential verifiersから問い合わせを受ける。情報の更新はこのシステムで行わないので、自分でやる必要がある。
例。

DBIC - Storage using a database.
Minimal - Storage using a simple hash (for testing).

  • User objects

ユーザーオブジェクトはstorage backendかcredential verifierによって作成され、ユーザー情報が設定される。
例。

Hash - A simple hash of keys and values.

  • ACL authorization

ACLAccess Control Listのこと。ACLプラグインはベースパスとか許可ユーザーとかロールとかを使ってパスへのアクセス制限をできるようにする。

  • Roles authorization

ロールによる権限分けでは、ACLに割り当てられたり必要に応じてチェックされるグループにユーザーを所属させる。

  • Logging in

モジュールを選んだら、あとやることは$c->loginをコールするだけ。パラメータが省略されたらそれっぽいパラメータ(username, passwordなど)を探そうとする。

  • Checking roles

ロールのチェックは$c->check_user_rolesで行う。チェックはログイン済みユーザーに対して行う。チェックするロール名を渡して、ユーザーがロールのメンバーだったらtrueを返す。

  • EXAMPLE
 use Catalyst qw/Authentication
                 Authentication::Credential::Password
                 Authentication::Store::Htpasswd
                 Authorization::Roles/;

 __PACKAGE__->config->{authentication}{htpasswd} = "passwdfile";

  sub login : Local {
     my ($self, $c) = @_;

     if (    my $user = $c->req->param("user")
         and my $password = $c->req->param("password") )
     {
         if ( $c->login( $user, $password ) ) {
              $c->res->body( "hello " . $c->user->name );
         } else {
            # login incorrect
         }
     }
     else {
         # invalid form input
     }
  }

  sub restricted : Local {
     my ( $self, $c ) = @_;

     $c->detach("unauthorized")
       unless $c->check_user_roles( "admin" );

     # do something restricted here
  }
  • Using authentication in a testing environment

理想的には、authentication/authorizationのテストを書いて、テスト用のデータベースをセットアップして、Test::WWW::Mechanize::Catalystでログインをシミュレートするべきなんだろうけど、認証フレームワークは柔軟さが重要だからこの方法はちょっとやりにくい。
テスト用のデータベースを使用する代わりに、テスト環境で扱いやすい保存先を使用することができる。この方法だとテスト用のデータベースを変更する必要がないっていう利点もある。
例。

  use Catalyst::Plugin::Authentication::Store::Minimal::Backend;

  # Sets up the user `test_user' with password `test_pass'
  MyApp->default_auth_store(
    Catalyst::Plugin::Authentication::Store::Minimal::Backend->new({
      test_user => { password => 'test_pass' },
    })
  );

これで、テストコードでは$c->login('test_user', 'test_pass')でログインできる。データベースは変更なしで。

  • More information

http://search.cpan.org/perldoc?Catalyst::Plugin::Authentication
にもっと長い説明がある。

Authorization
  • Introduction

権限分け(authorization)は認証(authentication)のあとでやる。認証はユーザーの存在確認を行い、権限分けはユーザーができることを決める。

ロールベースのアクセスコントロールではそれぞれのユーザーにロールを与えることで権限を与える。例えば、動物園では飼育係だけがおりの中に入れる。
例。

    package Zoo::Controller::MooseCage;

    sub feed_moose : Local {
        my ( $self, $c ) = @_;

        $c->model( "Moose" )->eat( $c->req->param("food") );
    }

このアクションだと誰でも檻に入ってえさをあげれる。すげー危険。だからこのアクションを制限する必要がある。
Authentication::Rolesプラグインを使う。

    use Catalyst qw/
        Authentication # yadda yadda
        Authorization::Roles
    /;

アクションはこうなる。

    sub feed_moose : Local {
        my ( $self, $c ) = @_;

        if ( $c->check_roles( "moose_feeder" ) ) {
            $c->model( "Moose" )->eat( $c->req->param("food") );
        } else {
            $c->stash->{error} = "unauthorized";
        }
    }

$c->userをチェックして許可されてればtrueが返る。
check_rolesはassert_rolesって仲間がいて、これだとロールがないときに例外を投げる。

ロールをチェックするのは退屈だし、エラーの原因になる。
Authorization::ACLプラグインは自動的にチェックできるようにする。
例えば、moose_feeder以外はMooseCageコントローラにアクセスできないようにする場合。

Zoo->deny_access_unless( "/moose_cage", [qw/moose_feeder/] );

Rolseプラグインを使ってこうやることも可能。

    Zoo->deny_access_unless( "/moose_cage", sub {
        my $c = shift;
        $c->check_roles( "moose_trainer" ) || $c->check_roles( "moose_feeder" );
    });

moose feedersはfeed_mooseだけできて、moose trainersは何でも出来る場合。

    Zoo->deny_access_unless( "/moose_cage", [qw/moose_trainer/] );
    Zoo->allow_access_if( "/moose_cage/feed_moose", [qw/moose_feeder/]);

同じパスに定義されたロールは追加された順番にチェックされる。
最後に、否定形のアクションをハンドリングするにはaccess_deniedアクションを作成する。

    sub access_denied : Private {
        my ( $self, $c, $action ) = @_;      
    }

このアクションはautoみたいに働く。ネームスペースにまたがって継承される。
このアクションが存在しない場合はエラーが投げられ、代わりにendアクションで後始末可能。
また、"/"へのアクセスを制限するとend, defaultなども制限されるってことに注意。

MyApp->acl_allow_root_internals;

は、root of your app(他のコントローラではなく)のend, begin, autoへのアクセスを許可するルールを作成する。

Models

モデルはアプリケーションのデータが属する場所。Catalystはどんなモデルを使うか自由。

Using existing DBIC (etc.) classes with Catalyst

たいていの場合、使いたいモデルはCatalystに入ってると思う。コードは単純。

    package MyApp::Model::DB;
    use base qw/Catalyst::Model::DBIC::Schema/;
    __PACKAGE__->config(
        schema_class => 'Some::DBIC::Schema',
        connect_info => ['dbi:SQLite:foo.db', '', '', {AutoCommit=>1}];
    );
    1;

これだけ。

XMLRPC

XMLRPCはSOAPと違って単純なウェブサービスプロトコルです。
リクエス

    POST /api HTTP/1.1
    TE: deflate,gzip;q=0.3
    Connection: TE, close
    Accept: text/xml
    Accept: multipart/*
    Host: 127.0.0.1:3000
    User-Agent: SOAP::Lite/Perl/0.60
    Content-Length: 192
    Content-Type: text/xml

    <?xml version="1.0" encoding="UTF-8"?>
    <methodCall>
        <methodName>add</methodName>
        <params>
            <param><value><int>1</int></value></param>
            <param><value><int>2</int></value></param>
        </params>
    </methodCall>

レスポンス

    Connection: close
    Date: Tue, 20 Dec 2005 07:45:55 GMT
    Content-Length: 133
    Content-Type: text/xml
    Status: 200
    X-Catalyst: 5.70

    <?xml version="1.0" encoding="us-ascii"?>
    <methodResponse>
        <params>
            <param><value><int>3</int></value></param>
        </params>
    </methodResponse>

アプリケーションを実装するためのステップ。
1. Install Catalyst (5.61 or later), Catalyst::Plugin::XMLRPC (0.06 or later) and SOAP::Lite (for XMLRPCsh.pl).
2. Create an application framework:

    % catalyst.pl MyApp
    ...
    % cd MyApp

3. Add the XMLRPC plugin to MyApp.pm

use Catalyst qw/-Debug Static::Simple XMLRPC/;

4. Add an API controller

% ./script/myapp_create.pl controller API

5. Add a XMLRPC redispatch method and an add method with Remote attribute to lib/MyApp/Controller/API.pm

    sub default : Private {
        my ( $self, $c ) = @_;
        $c->xmlrpc;
    }

    sub add : Remote {
        my ( $self, $c, $a, $b ) = @_;
        return $a + $b;
    }

defaultアクションはXMLRPCリクエストのエントリポイントになる。そこでは同じクラスのRemote属性の付いたメソッドにディスパッチし直す。
addメソッドは通常のアクションと違ってprivateやpublicパスを持たない。XMLRPCディスパッチャだけがその存在を知っている。
6. That's it! You have built your first web service. Let's test it with XMLRPCsh.pl (part of SOAP::Lite):

    % ./script/myapp_server.pl
    ...
    % XMLRPCsh.pl http://127.0.0.1:3000/api
    Usage: method[(parameters)]
    > add( 1, 2 )
    --- XMLRPC RESULT ---
    '3'

すげーなー。

  • Tip

戻り値のデータ型は自動判別されるけど、強制的に指定することも可能。

    sub add : Remote {
        my ( $self, $c, $a, $b ) = @_;
        return RPC::XML::int->new( $a + $b );
    }

Views

Viewはアプリケーションの表示を担う。

Catalyst::View::TT

Catalystはデータをどう表示するかには感知しない。HTML, PDFなどなんでもよい。
ほとんどの場合、HTMLを表示するのにテンプレートシステムを使用する。いろんなテンプレートシステムがあるけど、Template Toolkitが一番人気がある。
Catalyst::View::TTがTemplate Tookitへのインターフェースでヘルパーがいろいろやってくれる。

  • Creating your View

Catalyst::View::TTはTTとTTSiteの二つのヘルパーを提供している。

    • TT

ヘルパースクリプトで基本的なビューを作成する。

script/myapp_create.pl view TT TT

これはlib/MyApp/View/MyView.pmを作成する。最初は空っぽだけど、始めるのに必要なものはすべて提供してくれる。どのテンプレートを使用するか決めてビューにフォワードする。例えば、

    sub hello : Local {
        my ( $self, $c ) = @_;

        $c->stash->{template} = 'hello.tt';

        $c->forward( $c->view('TT') );
    }

手動でやってもいいけどCatalyst::Action::RenderViewを使った方がいい。

    • TTSite

TTヘルパーは十分動作するものを作ってくれるけど、新しいアプリケーションを作るたびに同じようなテンプレートファイルを作ったり、同じようなオプションを変更していることに気がつくだろう。TTSiteは基本的なテンプレートを作り、オプションを設定して時間節約に役立つ。
もう一度ヘルパースクリプトを使おう。

script/myapp_create.pl view TT TTSite

今度は、ヘルパーがいくつかのオプションをセットしてくれる。

    __PACKAGE__->config({
        CATALYST_VAR => 'Catalyst',
        INCLUDE_PATH => [
            MyApp->path_to( 'root', 'src' ),
            MyApp->path_to( 'root', 'lib' )
        ],
        PRE_PROCESS  => 'config/main',
        WRAPPER      => 'site/wrapper',
        ERROR        => 'error.tt2',
        TIMER        => 0
    });

INCLUDE_PATHはテンプレートファイルを探すディレクトリを指定する。
PRE_PROCESSはすべてのテンプレートファイルで共通の設定を実行するのに使う。
WRAPPERは各テンプレートで使用するファイルだ。ヘッダーとかフッターに使う。
これらのオプションに加えて、TTSiteはテンプレートと設定ファイルも作ってくれる。'root'ディレクトリにsrcとlibってディレクトリができてるだろう。
root/lib/configにある設定ファイルはPRE_PROCESSで呼ばれる。
root/lib/siteのファイルはWRAPPERで呼ばれ、HTMLの枠組みを表示してレイアウトを決め、ヘッダーやフッターを提供する。ページを標準化して変更を容易にする。
あなたが作るテンプレートファイルはroot/srcに置き、やについては考えず、ただ内容だけ置けばいい。WRAPPERが残りを処理してくれる。

  • $c->stash

テンプレートで使用するデータはstashに入れる。例えば、
コントローラで、

    sub hello : Local {
        my ( $self, $c ) = @_;

        $c->stash->{name} = 'Adam';

        $c->stash->{template} = 'hello.tt';

        $c->forward( $c->view('TT') );
    }

hello.ttで、

<strong>Hello, [% name %]!</strong>

これは簡単な例だけどTemplate Toolkitはもっと強力だ。これでプレゼンテーションロジックをアプリケーションから分離できる。

  • $c->uri_for()

これを使えばアプリケーションの配置パスを変えた時にテンプレートで困ることがない。
テンプレートでこんな風にできる。

<a href="[% c.uri_for('/login') %]">Login Here</a>

パラメータはスラッシュで始まってるけど、これはWebサーバーのルートじゃなくてアプリケーションのルートを示す。
または、

<a href="[% c.uri_for('2005','10', '24') %]">October, 24 2005</a>

これは最初のパラメータがスラッシュで始まってないので、現在のネームスペースからの相対になる。アプリケーションがhttp://www.domain.com/Calendarにあり、テンプレートがMyApp::Controller::Displayから呼ばれたのなら、このリンクはhttp://www.domain.com/Calendar/Display/2005/10/24になる。
uri_forがリンクを生成してくれるおかげで、異なるコントトーラから共通のテンプレートが使用できる。サイトに共通の要素がある場合に役立つ。
以下も参考に。
http://search.cpan.org/perldoc?Catalyst
http://search.cpan.org/perldoc?Catalyst%3A%3AView%3A%3ATT
http://search.cpan.org/perldoc?Template

Adding RSS feeds

アプリケーションにRSSフィードを追加するのは簡単だ。ここでは二通りのやり方を示す。両方とも最初にオブジェクトを取得するために普通のViewアクションにフォワードして出力を違うやり方で扱う。

  • Using TT templates

Agave (http://dev.rawmode.org/)のやりかた。

    sub rss : Local {
        my ($self,$c) = @_;
        $c->forward('view');
        $c->stash->{template}='rss.tt';
    }

で、テンプレートを用意する。ここではAgaveから持ってくる。

    <?xml version="1.0" encoding="UTF-8"?>
    <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
      <channel>
        <title>[ [% blog.name || c.config.name || "Agave" %] ] RSS Feed</title>
        <link>[% base %]</link>
        <description>Recent posts</description>
        <language>en-us</language>
        <ttl>40</ttl>
     [% WHILE (post = posts.next) %]
      <item>
        <title>[% post.title %]</title>
        <description>[% post.formatted_teaser|html%]</description>    
        <pubDate>[% post.pub_date %]</pubDate>
        <guid>[% post.full_uri %]</guid>
        <link>[% post.full_uri %]</link>
        <dc:creator>[% post.author.screenname %]</dc:creator>
      </item>
    [% END %]
      </channel>
    </rss> 
  • Using XML::Feed

もっとrobustなやり方はXML::Feedを使う。これはCatalyst Advent Calendarで使ってる。DBIx::Classイテレータを使って'entries'を作るviewアクションがあったとするとこうなる。

    sub rss : Local {
        my ($self,$c) = @_;
        $c->forward('view'); # get the entries

        my $feed = XML::Feed->new('RSS');
        $feed->title( $c->config->{name} . ' RSS Feed' );
        $feed->link( $c->req->base ); # link to the site.
        $feed->description('Catalyst advent calendar'); Some description

        # Process the entries
        while( my $entry = $c->stash->{entries}->next ) {
            my $feed_entry = XML::Feed::Entry->new('RSS');
            $feed_entry->title($entry->title);
            $feed_entry->link( $c->uri_for($entry->link) );
            $feed_entry->issued( DateTime->from_epoch(epoch => $entry->created) );
            $feed->add_entry($feed_entry);
        }
        $c->res->body( $feed->as_xml );
<<|
両方ともコンテントタイプをセットする。
>||
$c->res->content_type('application/rss+xml');
  • Final words

同じやり方でAtomも作れる。

Forcing the browser to download content

ダウンロード用のコンテンツを作りたいとき。たとえばCVSを生成してユーザーにダウンロードさせ、スプレッドシートプログラムにインポートするような場合。

    sub export : Local Args(0) {
        my ( $self, $c ) = @_;

        # In a real application, you'd generate this from the database
        my $csv = "1,5.99?n2,29.99?n3,3.99?n";

        $c->res->content_type('text/comma-separated-values');
        $c->res->body($csv);
    }

ブラウザはふつう、URLの最後のパートをファイル名に使う。この場合だとexportって名前でファイルを保存するか聞いてくるだろう。
Content-Dispositionヘッダを使うとファイル名を指定できる。

    my $filename = 'Important Orders.csv';
    $c->res->header('Content-Disposition', qq[attachment; filename="$filename"]);

ブラウザにファイル名中のスペースを認識させるためにクォートしている。
これを$c->res->bodyの前に置けばいい。
この方法はJPEGとかHTMLとかでも同じように使える。

Controllers

ControllerはWebサーバーとアプリケーションとやり取りする所。

Extending RenderView (formerly DefaultEnd)

endアクションで推奨する方法はCatalyst::Action::RenderViewを使うことで、たいていこれが必要だろう(ここはCatalyst::Plugin::DefaultEndが取り扱う場所だ)。コードをいくつか足す必要があるだろうけど、自分でendメソッドを作ってはいけない。
拡張するにはこうする。
レンダリングの前にendアクションでなにかするには単にコードを追加すればいい。

    sub end : ActionClass('RenderView') {
      my ( $self, $c ) = @_;
      # do stuff here; the RenderView action is called afterwards
    }

レンダリングのあとでなにかするにはこうする。

    sub render : ActionClass('RenderView') { }

    sub end : Private { 
      my ( $self, $c ) = @_;
      $c->forward('render');
      # do stuff here
    }
Action Types
  • Introduction

Catalystアプリケーションは1つ以上のコントトーラモジュールを使って処理を進める。Catalystはコントローラのどのメソッドを呼ぶか決める方法をいくつか持ってる。コントローラのメソッドはアクションと呼ばれることがある。なぜなら、それらはURLに対して何をするかを決めるものだからだ。アプリケーションが起動すると、Catalystは全アクションを見てURLをどこにマップするか決定する。

  • Type attributes

アクションは属性が指定される以外は普通のメソッドだ。
Contollerモジュールが以下のパッケージ定義で始まっていて、

package MyApp::Controller::Buckets;

localhostのポート3000で起動しているとする。
PATH
Path属性は相対か絶対パスを引数に取る。相対はコントローラの名前空間からのパス、絶対パスはURLにマッチする。

 sub my_handles : Path('handles') { .. }

はこうなり、

 http://localhost:3000/buckets/handles
 sub my_handles : Path('/handles') { .. }

はこうなる。

 http://localhost:3000/handles

Local
Local属性には引数がなく、アクション名がURLにマッチする。コントローラのパッケージ名が作り出す名前空間が常にURLの一部になる。

sub my_handler : Local { .. }

はこうなる。

http://localhost:3000/buckets/my_handles

Global
Global属性はLocalに似てるけどコントトーラの名前空間は無視され、ルートからマッチする。

sub my_handler : Global { .. }

はこうなる。

http://localhost:3000/my_handlers

Regex
Regex属性は正規表現を1つ取り、ルートからのパスにマッチする。そのため複数のURLにマッチさせることが可能。

sub my_handles : Regex('^handlers') { .. }

はこれにもマッチするし、

http://localhost:3000/handles

これにもマッチするし、

http://localhost:3000/handler_and_other_parts

他にもいろいろマッチする。

LocalRegex
LocalRegexはRegexに似てるけどコントトーラの名前空間でマッチする。

sub my_handles : LocalRegex('^handles') { .. }

はこれや、

http://localhost:3000/buckets/handles

これなんかにマッチする。

http://localhost:3000/buckets/handles_and_other_parts

Private
Privateはフォワードされる内部アクションを作成する。URLにはマッチしない。

sub my_handles : Private { .. }

は何もマッチしない。
Catalystは特別なPrivateアクションを定義している。これらはオーバーライド可能。
default
defaultアクションはマッチするアクションがなかったときに呼ばれる。ユーザーが以降としてたURLは$c->req->pathで取得できる。

sub default : Private { .. }

はマッチしないURLすべてに対して働く。

index
indexアクションはコントトーラの名前空間そのものにマッチする。もしdefaultやPathアクションが定義されていてもindexアクションが呼ばれる。

sub index : Private { .. }

はこれにマッチする。

http://localhost:3000/buckets

begin
beginアクションはこの名前空間へリクエストが来たとき、マッチしたアクションの前に呼ばれる。変数などのデータを用意するのに使用できる。現在の名前空間に最も近い所にあるbeginアクションが1つだけ呼ばれる。

sub begin : Private { .. }

はここに来たときに一度だけ呼ばれる。

http://localhost:3000/buckets/{anything})?

end
beginと同じようにこの名前空間へリクエストが来たとき、マッチしたアクションの後に呼ばれる。Viewにフォワードするのによく使われる。現在の名前空間に最も近い所にあるbeginアクションが1つだけ呼ばれる。

auto
autoアクションはパスをさかのぼってすべての名前空間で呼ばれる。begin/end/defaulが一回だけ呼ばれるのと対称的だ。

package MyApp.pm
sub auto : Private { .. }
sub auto : Private { .. }

このURLに来たら二つとも呼ばれる。

http://localhost:3000/bucket/(anything)?
  • A word of warning

Pluginでの名前空間の衝突の可能性があるので、MyApp.pmには事前定義されたPrivateアクションだけを置き、他のものはコントトーラモジュールに置いた方がいい。

  • More Information

http://search.cpan.org/author/SRI/Catalyst-5.61/lib/Catalyst/Manual/Intro.pod
http://dev.catalyst.perl.org/wiki/FlowChart

Component-based Subrequests

See Catalyst::Plugin::SubRequest.

File uploads

ファイルアップロードを実装するにはまずこんな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を参照。

Forwarding with arguments

引数と一緒に他のアクションにフォワードしたい時。5.30以降ではforwardで渡せるけどそれ以前の場合はリクエストオブジェクトに入れる必要がある。

  # version 5.30 and later:
  $c->forward('/wherever', [qw/arg1 arg2 arg3/]);

  # pre-5.30
  $c->req->args([qw/arg1 arg2 arg3/]);
  $c->forward('/wherever');

Catalyst::Manual::IntroのFlow_Controlセクションを参照)