(5/13 追記): メソッドモディファイヤーの件、Catalyst開発チームにテストパッチを送ろうと思って色々書いたら理由がわかったので、追記しました

最初からMooseベースでアプリケーションを作るというのは、実務ではなかなか難しいのはわかります。なので使うとしたら既存のシステムにMooseを組み込む事のほうが多いとは思います。

それについての一般論は JPA #02で話すのでおいておきますが参加申し込みは今日5/12までですよ!)、5.8 からMoose化したCatalystにポートを行う際に実際にあった問題・注意点をちょっと書き出してみます。

1. use Catalyst

Catalyst::Upgradingを読んでいると
package MyApp; use Moose; extends 'Catalyst'; __PACKAGE__->setup(qw/ ConfigLoader /);
という表記が見られるが、これは気をつけないと駄目。

自分が直面した問題は、path_to()等を使った時に起こった。path_to() は現アプリのルートディレクトリからのパスを指定したい時に使う。例えばTTテンプレートのコンパイルされた物をMyApp/tt2 以下に格納したければ、
my $path = MyApp->path_to("tt2");
というようにする。このpath_to() というのは $c->config->{home}という値を使用するのだが、この値は通常Catalystによって自動的にMyApp/ 以下に設定されるので、デフォルト状態のファイルレイアウトでCatalystを使用する分には特に問題にならない。だが、上記のextends記法を使っているとなぜか home変数が設定されず、path_to()の挙動がおかしくなる。

これを回避するには、home変数を設定する必要があるのだが、homeの自動設定はCatalyst::import()で行われており、通常extends() (use baseでも一緒)を使うとimport()は実行はされないのだ(importが自動的に実行されるのは、「use Module」とした時だけ)。それはすなわちhomeの初期化が行われない、ということである。

ということで、実際はこうする必要がある:
package MyApp; use Moose; use Catalyst; extends 'Catalyst';

2. :Index, :Private, :Local, :Chained等

CatalystでコントローラーメソッドをURIパスに結びつけるのが:Indexや:Chained等のメソッドアトリビュートと呼ばれる物だが、これらはPerlのコンパイルフェーズで処理される。いわゆるBEGIN {} ブロックが走るタイミングと考えて良い。

コンパイルフェーズというのは特殊で、use() 宣言やBEGIN{}ブロック等、Perlのネイティブレベルで定義されたものだけが(注:割愛するが、実はそれ以外の黒魔術的なやりかたもある)、それ以外のコードを走らせる前に全部動く。本当のコードが走る前の一種の初期化フェーズと考えてもいいかもしれない。

通常Mooseマニュアルには継承を行う際はextendsを使えと書いており、同じノリでMyApp::Controller::RootをCatalyst::Controllerから継承する、というようにMooseで書こうとすると以下のようになる:
package MyApp::Controller::Root; use Moose; extends 'Catalyst::Controller'; sub index :Index { .... }
が、ここが落とし穴。これだと :Indexを見た瞬間にPerlがエラーを吐く。なぜか。

:IndexをPerlが最初に見るのはコンパイルフェーズだ。前述の通り、use宣言やBEGINブロック以外はその段階では実行されない。:Indexを理解するためにはMyApp::Controller::Rootはその時点ですでにCatalyst::Controllerを継承していなければならないのだが、継承を指定するextends宣言は普通の関数なので、コンパイルフェーズでは走らない。よって、コンパイルフェーズでは MyApp::Controller::Root->isa('Catalyst::Controller')にはなっておらず、メソッドアトリビュートも理解されないのだ。

これを回避するのには単純にBEGINブロックで囲ってやればいいのだが、正直一瞬意味がわからなくて困る
package MyApp::Controller::Root; use Moose; BEGIN { extends 'Catalyst::Controller' } sub index :Index { .... }

3. プラグインが実行されない

(5/13 追記) 以下の理由がわかった。単純。Catalyst->setupはMooseのメソッドモディファイヤーを使用する前に呼び出されなければならない。
package MyApp; .... before finalize => sub { ... }; __PACKAGE__->setup(...); # これは駄目
package MyApp; .... __PACKAGE__->setup(...); # これならOK before finalize => sub { ... };
Catalystは過去の経緯から、setup()時にクラスの継承関係を変更するのだが、それをやる前にメソッドモディファイヤーを使ってしまうと親クラスをきちんと認識してくれない、ということっぽい。setup()の中で親クラスのリセットを行ってくれればそれでいいんじゃねと思うが、それまでは単純にMyApp.pmの上のほうでCatalyst関係の初期化を行えば良い、ってことですな。

プラグインの多くは継承のメカニズムを使ってCatalyst内のメソッドの前か後に実行されるように書かれている。

Catalyst 5.8からはMooseなので、プラグインを書くほどでもないちょっとした事ならMyApp内でメソッドモディファイヤー(afterやbefore)を使ってちょこちょこと自前のロジックを足したりもしたいのだが、それをすると突然プラグインが動かなくなったりする。

自分がはまったのはCatalyst::Plugin::Unicode。それまで動いていたのに、以下のようにbeforeメソッドモディファイヤーで自前のエラー処理をつけようとしたら、"Wide character in syswrite"というエラーが出始めた。
package MyApp; ... before finalize => sub { my $c = shift; $c->handle_exception if @{ $c->error }; };
色々と試してみたところ、これはメソッドモディファイヤーが適用されるタイミングに左右されることがわかった。モディファイヤーがCatalystの継承によるディスパッチを邪魔して、Catalyst.pm以外のメソッドを呼び出してくれない(ちなみにこれでより一層Moose::Roleが素晴らしい、ということがわかった。継承を意識しながらメソッドディスパッチとかうざすぎる)

ちょっとまだ一般解まではわかってないのだが、とにかく、上記のようにMyApp内からfinalizeにフックしたいなら、overrideとnext::methodを組み合わせるのが吉
package MyApp; ... override finalize => sub { my $c = shift; $c->handle_exception if @{ $c->error }; $c->next::method(@_); };
このようにすると正しくプラグインへのディスパッチを行いつつ、フックもできる。

4. エラーがよくUnknown Errorになる。

Perl 5.10.x ではUnknown Errorがよく出る、というのは聞いていたが、5.8.9でもなぜかなる。あんまりちゃんと調べてないので、そもそもこれは既知の事だったらあれなのだが、わかっているのはこれはとにかくコンパイルフェーズやその他セットアップしている時に起こるということ。

なので、このエラーが出たら、まずモジュール類がちゃんとコンパイルすることから調べ始めるのを薦める。それとちょうど良い機会なので、エラーを吐きそうなブロックは明示的に囲ったりして自前でエラーを出力するようにすればよい。warn && confessみたいな単純な処理でも以外と役立つ。
eval { Class::MOP::load_class($pkg); # 個人的にはこういうのとか、DBICのconnect()とかでよくなった }; if ($@) { warn && confess; }
いずれにしろ、エラーケースはちゃんと自前で処理したほうが良いので、これを機に怪しいところは全部やっつけるといいのではないだろうか。




とりあえず、今回Catalyst 5.7 -> 5.8に移行した際に直面した問題はだいたい以上のようなもの。それまでのCatalystでは出ない類のエラーが多いので、困る人も多かろう。なんらかの手助けになれば幸い。