How To Write Helper Script For Catalyst

Catalyst の Helper Script の作成方法について説明していきます。
HelperScriptというと広義において、テストサーバーやテスト実行のスクリプトも含みますが、
今回は、コンポーネントやその他のファイルを生成するヘルパースクリプトを扱います。
このようなスクリプトをgeneratorと呼ぶことにします。
Catalystでは script/myapp_create.pl がそれにあたります。

Catalystでは、このgeneratorを使用してコンポーネントを生成していきます。
生成すると、ファイルの中身は、すでにデフォルトの内容が記述されています。
毎回同じような処理を書く場合、その処理内容をデフォルトとして、コンポーネントを生成するヘルパーを作っておくといいでしょう。
今回は、このヘルパースクリプトの作り方を簡単に解説していきます。

generatorの使い方

あなたが、MyAppというアプリケーションのスケルトンをCatalystで作成した場合、
script/myapp_create.pl というファイルが生成されます。
このスクリプトを使用して、ファイルを生成していきます。

script/myapp_create.pl controller Hoge

上記のように、第一引数にコンポーネントタイプを指定します。
コンポーネントタイプは model, view, controller の中から選択します。
第二引数にコンポーネント名を指定します。
デフォルトのHelperによる、コンポーネントの生成が実行されます。
上記の例では、MyApp::C::Hoge というパッケージのファイル、及び
そのパッケージのためのテストスクリプトが自動生成されます。

script/myapp_create.pl view MyTT TT

この例では、コンポーネントタイプにview、コンポーネント名がMyTTですので
MyApp::V::MyTT が生成されます。
先ほどの例と違うのは第三引数があることです。
第三引数はヘルパー名となります。
この場合、Catalyst::Helper::View::TT を使用して、
MyApp::V::MyTT を生成する、という命令になります。

script/myapp_create.pl model MyCDBI CDBI dbi:mysql:database user pass

コンポーネントタイプがmodelで、コンポーネント名がMyCDBIですので
MyApp::M::MyCDBIが生成されます。
ヘルパー名がCDBIですので、Catalyst::Helper::Model::CDBI を使用して、ファイルを生成することになります。
ヘルパー名より後ろの値は引数としてヘルパーに渡されます。

generator用のhelper scriptを作成する

今回は、コンポーネント生成用のHelperScriptを作成してみます。
コンポーネントと無関係のファイル生成用のHelperもありますが、
今回は言及しません。Catalyst::Helper::Prototypeなどがそれにあたりますので、
興味のある人は処理内容を見てみるといいでしょう。

名前の付け方は上の説明で理解できたと思います。
Catalyst::Helper::コンポーネントタイプ::ヘルパー名という形になります。
今回は controller を 生成するスクリプトを書きます。
ヘルパー名はMyScaffoldとしますので、名前は
Catalyst::Helper::Controller::MyScaffoldとなります。

mk_compclassを定義する

コンポーネントを生成するタイプのhelperを作成する場合、
まずはじめに、mk_compclass というメソッドを定義しなければいけません。

このメソッド内に、ファイル生成の処理を書いていきます。
package Catalyst::Helper::Controller::MyScaffold;
use strict;
use warnings;

sub mk_compclass {
    my ( $self, $helper, @args ) = @_;
    my $filepath = $helper->{file};
    $helper->render_file('hoge', $filepath);    
}

1;
__DATA__
__hoge__
package [% class %];
use strict
use base 'Catalyst::Base';

sub list : Local {
    ...
}

sub add : Local {
    ...
}

1;

$helper

mk_compclassに引数として渡されます。
このヘルパーオブジェクトを使用してファイルを生成していくことになります。
使用するのは主に、render_file メソッドです。

$helper->render_file(テンプレート, ファイルパス);

render_fileは、Template-Toolkitを使用して、指定されたパスに
ファイルを書き出すメソッドです。

テンプレートはヘルパークラス内に書きます。
まず、テンプレートに名前を付けます。
今回は hoge という名前にします。
ヘルパークラスの最後、__DATA__ 以降に、 __hoge__という行を足し
それ以降にTemplate-Toolkit用のテンプレートを書いていきます。

package Catalyst::Helper::Controller::MyScaffold;
use strict;
use warnings;

sub mk_compclass {
    ...
}

1;
__DATA__
__hoge__
#ここからテンプレート
package [% class %];
use strict
use base 'Catalyst::Base';

# Template-Toolkit 用のテンプレートを書く

1;

この場合、$helper->render_file('hoge', $filepath) とすると、
__hoge__からファイル末端までを、一つのテンプレートとして処理し、
生成されたデータを、指定された $filepath に出力します。

テンプレートには $helper オブジェクト自身のハッシュが渡されますので
$helperオブジェクトが持つデータをテンプレートから呼び出せます。
$helperは次のような値を持っています。

$helper->{app}

アプリケーション名です。例題の場合は、MyAppが入ります。

$helper->{base}

アプリケーションのルートディレクトリのパスが入ります。

$helper->{class}

生成するコンポーネントのクラス名が入ります。
script/myapp_create.pl controller Hoge
として実行された場合には、
MyApp::C::Hoge という文字列が入ります。

$helper->{file}

生成するコンポーネントのファイルパスが入ります。
上記例題では、このファイルパスをそのままrender_fileに渡しています。
これらの値を

[% class %] [% file %]  [% app %]

というような形で
テンプレートから呼び出すことが出来ます。
これら以外の値をテンプレート内で使用したい場合、

sub mk_compclass {
    my ($self, $helper, @args) = @_;
    $helper->{foobar} = "foobar";
    $helper->render_file('hoge', $helper->{file});
}

のように、$helperのハッシュに値を追加してからrender_fileを呼びます。
テンプレート内からは [% foobar %] で値を呼び出せるようになります。

Scaffoldを実装する

Webアプリケーションを作成するとき、データベースの各テーブルに関するCRUDな処理を毎回書いていると思います。
例えば、User テーブルがあった場合に、
userのリストを表示する機能、userを追加する機能、編集する機能、削除する機能などです。
こういった処理は、特に管理システムのようなアプリケーションで頻出します。
別のテーブル、例えばCompanyテーブルがあった場合にも、
そのテーブルに関するCRUDな処理を書く場合、
Userテーブルと共通箇所の多い処理を書くことがほとんどです。
このように毎回書かなければいけない処理を、デフォルトとして既にファイルに記述された状態で
スクリプトを生成するHelperをScaffoldといって、Ruby On Rails や、 Catalyst の Plugin で実装されています。
これらの Scaffold はコントローラーのスクリプトだけでなく、
それから必要とされる、テンプレートファイルまでも生成します。
Scaffold とは 「足場」という意味です。

例えば、毎回次のような処理を書いているとします。

package MyApp::C::User;
use strict;
use warnings;
use base qw/Catalyst::Base/;

sub list : Local {
    my ( $self, $c ) = @_;
    my @records = MyApp::M::User->retrieve_all;
    $c->stash->{records}  = \@records;
    $c->stash->{template} = "User/list.tt";
}

sub add : Local {
    my ( $self, $c ) = @_;
    $c->stash->{template} = "User/add.tt";
}

sub do_add : Local {
    my ( $self, $c ) = @_;
    $c->form(
        # validation
    );
    my $result = $c->form;
    if ( $result->has_invalid or $result->has_missing ) {
        $c->stash->{result} = $result;
        $c->detach('add');
    }
    MyApp::M::User->create_from_form($result);
    $c->stash->{template} = "User/do_add.tt";
}

sub edit : Local {
    my ( $self, $c, $id ) = @_;
    my $record = MyApp::M::User->retrieve($id);
    $c->stash->{record}   = $record;
    $c->stash->{template} = "User/edit.tt";
}

sub do_edit : Local {
    my ( $self, $c, $id ) = @_;
    $c->form(
        # validation
    );
    my $result = $c->form;
    if ( $result->has_invalid or $result->has_missing ) {
        $c->stash->{result} = $result;
        $c->detach('edit');
    }
    MyApp::M::User->retrieve($id)->update_from_form($resutl);
    $c->stash->{template} = "User/do_edit.tt";
}

sub delete : Local {
    my ( $self, $c, $id ) = @_;
    my $record = MyApp::M::User->retrieve($id);
    $record->delete;
    $c->stash->{template} = "User/delete.tt";
}

1;

これはUserテーブルを扱う場合の処理ですが、
Companyテーブルを扱う場合も、一部を変更するだけで、ほとんどの箇所を使いまわせます。
毎回書くのは面倒なので、これを簡単に生成するヘルパーを作ります

package Catalyst::Helper::Controller::MyScaffold;
use strict;
use warnings;

sub mk_compclass {
    my ($self, $helper, $table) = @_;
    my $filepath = $helper->{file};
    $helper->{table} = $table;
    $helper->render_file('compclass', $filepath);
}

1;
__DATA__
__compclass__
package [% class %];
use strict;
use base 'Catalyst::Base';

sub list : Local {
    my ( $self, $c ) = @_;
    my @records = [% app %]::M::[% table %]->retrieve_all;
    $c->stash->{records}  = \@records;
    $c->stash->{template} = "[% table %]/list.tt";
}

sub add : Local {
    my ( $self, $c ) = @_;
    $c->stash->{template} = "[% table %]/add.tt";
}

sub do_add : Local {
    my ( $self, $c ) = @_;
    $c->form(
        # validation
    );
    my $result = $c->form;
    if ( $result->has_invalid or $result->has_missing ) {
        $c->stash->{result} = $result;
        $c->detach('add');
    }
    [% app %]::M::[% table %]->create_from_form($result);
    $c->stash->{template} = "[% table %]/do_add.tt";
}

sub edit : Local {
    my ( $self, $c, $id ) = @_;
    my $record = [% app %]::M::[% table %]->retrieve($id);
    $c->stash->{record}   = $record;
    $c->stash->{template} = "[% table %]/edit.tt";
}

sub do_edit : Local {
    my ( $self, $c, $id ) = @_;
    $c->form(
        # validation
    );
    my $result = $c->form;
    if ( $result->has_invalid or $result->has_missing ) {
        $c->stash->{result} = $result;
        $c->detach('edit');
    }
    [% app %]::M::[% table %]->retrieve($id)->update_from_form($resutl);
    $c->stash->{template} = "[% table %]/do_edit.tt";
}

sub delete : Local {
    my ( $self, $c, $id ) = @_;
    my $record = [% app %]::M::[% table %]->retrieve($id);
    $record->delete;
    $c->stash->{template} = "[% table %]/delete.tt";
}
1;

このヘルパーを使用して、CRUD用のControllerを生成することによって、
毎回書いている処理を書く手間がはぶけます。

次のように使います。

script/create.pl controller User MyScaffold User

コンポーネント以外のファイルを生成することも可能です。
list.ttも一緒に生成してみます。

package Catalyst::Helper::Controller::MyScaffold;
use strict;
use warnings;
use File::Spec;

sub mk_compclass {
    my ($self, $helper, $table) = @_;
    my $filepath = $helper->{file};
    $helper->{table} = $table;
    $helper->render_file('compclass', $filepath);

    my $base = $helper->{base};
    my $dir  = File::Spec->catdir($base, 'root', $table);

    $helper->mk_dir($dir);

    $helper->render_file('list', File::Spec->catfile($dir, 'list.tt'));
}

1;
__DATA__
__compclass__
package [% class %];
use strict;
use base 'Catalyst::Base';

sub list : Local {
    my ( $self, $c ) = @_;
    my @records = [% app %]::M::[% table %]->retrieve_all;
    $c->stash->{template} = \@records;
    $c->stash->{template} = "[% table %]/list.tt";
}

# 以下略

__list__
[% TAGS star -%]
<html>
<head><title>[* app *]</title></head>
<body>

<h1>[* table *]</h1>

<table>
[% FOREACH record IN records %]
<tr>

</tr>
[% END %]
</table>

</body>
</html>

上の例では、 __compclass__というテンプレートの指定の後に、さらに
__list__ で区切ってテンプレートを書いています。このように、一つのヘルパー内に、
複数のテンプレートを記述することが可能です。

ただし、コンポーネント以外のファイルを出力する場合は、
出力前に $helper->mk_dir($dir) でディレクトリを作成する必要があります。
また、今回は Template-Toolkit用のテンプレートファイル を Template-Toolkitで出力することになりますので、タグのエスケープが必要です。
[% TAGS star %] で タグを [* *] に変換することによって対処しています。

このように、コンポーネントだけで無く、使用するテンプレートまで作ってしまう事が出来ます。

是非、自分好みのScaffoldを用意してみて下さい。

その他

Catalyst::Helperには、今回言及しなかった機能がまだいくつかあります。
興味を持たれたらCatalyst::Helperや、既存のHelperスクリプトを調べてみるといいでしょう。
Catalyst::Helper::Controller::Scaffoldや、Catalyst::View::TTSiteが参考になると思います。