D-7 <altijd in beweging>

Day to day life of a Perl/Go/C/C++/whatever hacker. May include anything from tech, food, and family.

タグ:Catalyst


あくまで自分はどうしてるか、って話ですが、最近はCatalystでなんか書くときはこんな感じで使ってます。

my_catalyst_model_setup.png
色んな事がこの図に詰まっているので、箇条書きしてみる:

  • Model::APIがAPIオブジェクトを作成して、使用時にはModel::APIに対して`find()`というメソッドを使って実際のAPIオブジェクトを持ってくる
  • Schema等はMyApp::Schemaに定義し、Model::APIのアトリビュートとして持っている。cacheも同等。これらの初期化引数は設定ファイルのModel::APIから取れるようにしておく
  • Catalyst::Model::DBIC::Schemaは*使ってない*
  • Model::APIではACCEPT_CONTEXTが呼ばれた時点で、もしまだ初期化が行われていなければ、SchemaやAPIの初期化を行っている。Catalyst::Model::Adaptorは*使ってない*
蛇足だけど、Catalyst::Plugin::AuthenticationでStore::DBIx::Classを使うときはどうしてもDBIC::Schema的な形で認証データが入っているモデルを要求されるので、DBIC::Schemaを使いたくなってしまう・・・が、実は単純にその認証データが入っているresultsetが欲しいだけなので、こんな感じの小さいモデルを一個作ってやりすごしている:
    package MyApp::Web::Model::DBIC::Member;
    use Moose;
    use namespace::autoclean;
    BEGIN { extends "Catalyst::Model" }

    has schema => (is => 'rw');

    sub ACCEPT_CONTEXT {
          my ($self, $c) = @_;
          if (! $self->schema) {
              $self->schema( $c->model('API')->schema ); # Model::APIからschemaを盗んでくる
          }
          return $self->schema->resultset('Member');
    }
    __PACKAGE__->meta->make_immutable();
    1;

ちなみに MyApp::CLI::Hogeとかを書くときには、以下のような感じのでやっている:

  • 全部のAPIが必要じゃないことのほうが多いので適時スクリプトの中身によって必要なSchemaやらAPIやらを作成している。(Model::APIに相当するものはない)
  • WithDBICっていうRoleを作って、DBスキーマが必要な場合のSchema生成等のコードを一元化している
  • 引数等はCatalystの設定ファイルを流用するようなことは*してない*。MooseX::Getopt(もしくはMooseX::SimpleConfig)を使ってコマンドラインで --connect_info=dbi:mysql:dbname=hoge と指定できるようにしている
Orochiとか使えたほうがもっと楽な気はするんだけど、とりあえず依存関係を増やすのもあれなのでこんな感じでやっております。
    このエントリーをはてなブックマークに追加 mixiチェック

Pixisががらっと変わろうとしている。最初は単純に継承ではなくRoleでその機能を提供しようと思ってあれこれ考えてたんだけど、その際にCatalyst.pmの中身を見たらsetup_plugins()が・・・
sub setup_plugins { my ( $class, $plugins ) = @_; $class->_plugins( {} ) unless $class->_plugins; $plugins ||= []; my @plugins = Catalyst::Utils::resolve_namespace($class . '::Plugin', 'Catalyst::Plugin', @$plugins); for my $plugin ( reverse @plugins ) { Class::MOP::load_class($plugin); my $meta = find_meta($plugin); next if $meta && $meta->isa('Moose::Meta::Role'); $class->_register_plugin($plugin); } my @roles = map { $_->name } grep { $_ && blessed($_) && $_->isa('Moose::Meta::Role') } map { find_meta($_) } @plugins; Moose::Util::apply_all_roles( $class => @roles ) if @roles; }
おわああ!Roleが適用できるじゃないか!どうもこれは将来的にPlugin機構をすべてRoleに取り替えるための伏線な気がするぞ。しかしともあれ、これがあるということは今までCatalyst Pluginを指定していた所でRoleを指定すればそのまま動くということ。

ってことでpixis/tree/rollerブランチで主立った機能をすべてRoleに移した。今後の使い方はこんな感じ:
package MyApp; use Pixis::Web; __PACKAGE__->config( ... ); __PACKAGE__->setup(); 1;
このファイル一個と、myapp_server.plさえあればいきなりある程度動くCatalystアプリができあがるという寸法。myapp_server.plが作れるようにしないとなあ。
    このエントリーをはてなブックマークに追加 mixiチェック

Catalystはたいへんすばらしいフレームワークですが、新しいプロジェクトを始める、という時にcatalyst.plでスケルトンから作り直していつものプラグインを設定して・・・みたいな面倒な手間がいろいろあります。

Pixisはなるたけ簡単に新しいアプリを作れるようにしたかったので最初からプラグイン機構を念頭に置いて書き始めましたが、それはあくまで機能の追加にしか使えず、JPAサイトのようにPixisというフレームワークを使って、JPAというサイトがPixis機能を乗っ取るというような場合はそれだけではうまく設計ができませんでした。

これについては悶々と考えていたのですがCatalyst 5.8になり、Mooseベースのオブジェクト指向ができるようになったことでひとつひらめきました。たとえばJPA::Webというアプリを作るとして、基本的にPixisがすでに提供しているすべてのコントローラーやモデルをそのまま使いたい、と思うならこれまでは自前でPixisを継承したコントローラーやモデルを作ることが必要でした。ですがMooseが前提ならClass::MOPの機能を使って、イン・メモリでクラス作っちゃえるじゃないですか。

ってことでやってみました。今のPixisは以下のように継承を宣言するだけで、とりあえずさくっと動くようになりました:
package JPA::Web; use Moose; BEGIN { extends 'Pixis::Web' } __PACKAGE__->setup_config(); __PACKAGE__->setup(); 1;

これは便利!

実装方法まだハックっぽい面がありますが、基本的な方向性はこれで正しいと思います。setup_components()とconfig()をオーバーライドして、子クラスはconfig()に引数を渡さずに設定できるように、setup_components()はPixis::Web::名前空間以下のすべてのモジュールをMoose::Meta::Classのcreate()を使ってインメモリで子クラスを作れるようにしてみました。

もうちょいしたらJPAサイトもこの方式に切り替えようと考えています。
    このエントリーをはてなブックマークに追加 mixiチェック

(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では出ない類のエラーが多いので、困る人も多かろう。なんらかの手助けになれば幸い。
    このエントリーをはてなブックマークに追加 mixiチェック

このページのトップヘ