日向夏特殊応援部隊

俺様向けメモ

Catalyst Source Code Walking #03

はじめに

前回のエントリはこちら。

d:id:ZIGOROu:20061007:1160169000

今回はComponentだけにフォーカスを当てます。

Componentとは何か

基本的にmyapp_create.plで作成するモジュールはComponentです。
ここで作られるComponentはCatalyst::Controller, Catalyst::Model, Catalyst::Viewクラスか
あるいはそれらの派生クラスであるモジュールを親クラスとしたモジュールとなります。

従って、Catalystの開発は、

  • Pluginによる$cの拡張
  • Componentによる具体化

がメインになると言えます。

もう一度Componentの処理をおさらい

と言う訳で再度Catalyst->setup_components()を見てみます。

sub setup_components {
    my $class = shift;

    my @paths   = qw( ::Controller ::C ::Model ::M ::View ::V );
    my $config  = $class->config->{ setup_components };
    my $extra   = delete $config->{ search_extra } || [];
    
    push @paths, @$extra;
        
    my $locator = Module::Pluggable::Object->new(
        search_path => [ map { s/^(?=::)/$class/; $_; } @paths ],
        %$config
    );

まずはconfig->{setup_components}->{search_extra}がある場合は、デフォルトの@pathsにsearch_extraを加えた上でPluggableなモジュールの検索を行います。
この際に特にconfigを指定しない場合は、問答無用で検索が行われます。

従ってsearch_extraを指定しない場合はMVCなpackageを持つ物をcomponentとして扱うとみなせますが、search_extraの指定がでたらめだと、無用なモジュールもcomponentとして処理しようとします。

for my $component ( sort { length $a <=> length $b } $locator->plugins ) {
    Catalyst::Utils::ensure_class_loaded( $component, { ignore_loaded => 1 } );

    my $module  = $class->setup_component( $component );
    my %modules = (
        $component => $module,
        map {
            $_ => $class->setup_component( $_ )
        } Devel::InnerPackage::list_packages( $component )
    );
       
    for my $key ( keys %modules ) {
        $class->components->{ $key } = $modules{ $key };
    }
}

ここでCatalyst->setup_component()でcomponentのロードを行います。
ちなみにDevel::InnerPackageモジュールにより、同じファイルに記述した内部packageで定義された物もきちんとcomponent扱いにしてくれます。

Catalyst->setup_component()も再び見てみましょう。

sub setup_component {
    my( $class, $component ) = @_;

    unless ( $component->can( 'COMPONENT' ) ) {
        return $component;
    }

ちなみにCOMPONENTメソッドですけど、Catalyst::Componentモジュールで既に定義されています。
helper経由でmodelを作った場合、基本的にはCatalyst::Modelを継承したクラスが出来上がります。

最もこの上の処理が示す通り、COMPONENTメソッドが実装されていないモジュールでもコンポーネントとして扱う事が可能ですが、setup_components()の中でインスタンス化はされません。*1

実際のCOMPONENTメソッドは後で読み直すとして続きを見てみると、

    my $suffix = Catalyst::Utils::class2classsuffix( $component );
    my $config = $class->config->{ $suffix } || {};

    my $instance = eval { $component->COMPONENT( $class, $config ); };

    # snip ...

    return $instance;
}

$component->COMPONENT()によってインスタンス化をしているのが分かります。
この時に$suffixがキーとなる設定値があれば、それをCOMPONENTに渡せます。

さてCatalyst::Component->COMPONENT()メソッドを見てみましょう。

sub COMPONENT {
    my ( $self, $c ) = @_;

    # Temporary fix, some components does not pass context to constructor
    my $arguments = ( ref( $_[-1] ) eq 'HASH' ) ? $_[-1] : {};

これ、前回勘違いして読んでました。
これって幾つかの既存のComponentがCatalystのcontextを受け付けないから仕方なしにこうした処理と言うかvalidateしてるだけっすね。。。

    if ( my $new = $self->NEXT::COMPONENT( $c, $arguments ) ) {
        return $new;
    }
    else {
        if ( my $new = $self->new( $c, $arguments ) ) {
            return $new;
        }
        else {
            my $class = ref $self || $self;
            my $new   = $self->merge_config_hashes( $self->config, $arguments );
            return bless $new, $class;
        }
    }
}

COMPONENTメソッドのNEXT chainまたはnewメソッドでインスタンス化するのが普通の挙動のようです。

と言う訳でComponentとは、search_extraを指定してしまった場合は明確な定義が存在しません。
MVCまたはsearch_extraで指定されたpackageツリー以下のモジュールを全てcomponentとして扱う事が出来ます。

Componentの呼び出しについて

Catalystモジュールの中に下記のメソッドがあります。

  1. Catalyst->model()
  2. Catalyst->controller()
  3. Catalyst->view()
  4. Catalyst->component()

いずれもcomponentとして登録されたモジュールの呼び出しを行います。
そのうちmodel(), controller(), view()はやってる事はほぼ同じです。MVCの違いだけです。
またcomponent()に関してはprefixが存在しないだけでやはりやってる事は同じです。

sub model {
    my ( $c, $name, @args ) = @_;
    return $c->_filter_component( $c->_comp_prefixes( $name, qw/Model M/ ),
        @args )
      if $name;
    return $c->component( $c->config->{default_model} )
      if $c->config->{default_model};
    return $c->_filter_component( $c->_comp_singular(qw/Model M/), @args );

}
  1. name指定がある場合
  2. default_modelがconfigで指定されている場合
  3. それ以外

って感じで処理が進みます。Catalyst->_filter_component()を見てみます。

sub _filter_component {
    my ( $c, $comp, @args ) = @_;
    if ( eval { $comp->can('ACCEPT_CONTEXT'); } ) {
        return $comp->ACCEPT_CONTEXT( $c, @args );
    }
    else { return $comp }
}

ComponentがACCEPT_CONTEXT()メソッドを実装していると、
カレントのContextを渡す事が出来ます。後はACCEPT_CONTEXT()メソッドはインスタンス自体を返すように作らないと、上手く動作しないと思われ。

よってMVCなクラスにこっそり$cを渡したい場合は、ACCEPT_CONTEXT()メソッドを実装すると$cが渡せて、例えばModelなクラスでもlogを取れたりします。

context依存なmodelってどうよって話もなきにしろあらずなので、
特にmodelなんかはACCEPT_CONTEXT経由で呼ばれた場合のみ、contextに依存する処理が使えるとかそんな記述にしとかないといけないと思います。

そもそもCatalyst->setup_components()でも確かにcontextクラスを渡してるはずなんだけど、
setup中はcontextクラスはインスタンス化されてないので、事実上アプリケーション名が渡されるだけです。

ちなみに$nameが無い場合の処理は登録済みのcomponentの中からデフォルトとして設定されたmodel componentを取得するか、最も最初に当たるcomponentが返ってきます。無ければundefになる。
defaultはともかく、最後の処理は下世話な気がする。


ともかく、ComponentをComponentとして呼び出したい場合は、上記のようにmodel(), controller(), view(), component()メソッドを通じて呼び出します。


Controllerに関して言うとその性質上、contextだとかrequestに依存するコードが増えてしまうかも。
但し同じControllerでも、依存しない物に切り分けて書ければ、切り離し可能なモジュールとして扱えると思う。


と言うのもControllerを除けば、Component(Catalyst::Model, Catalyst::Viewの派生クラス)は基本的にCatalyst::Componentを親としていて、
Catalyst::ComponentはClass::Accessor::Fast、Class::Data::Inheritableを継承しているに過ぎない。

但しこれらのComponentを実際にCatalyst経由で使わず、切り離した独立したモジュールとして動作させたい場合は、COMPONENT経由で呼ぶにせよ、直接new叩いて呼び出すにせよ、$cを渡している部分を無視してoptionalのパラメーターはhashrefで渡せばOKだと思われる。


但しNEXTによるCOMPONENTメソッドのchainを考えると、COMPONENTメソッド経由でインスタンス化するのが良いかと思われる。

まとめ

  • ComponentはMyApp::(M(odel)?|V(iew)?|C(ontroller)?)以外にもsearch_extraで読み込める
  • Controller以外のComponentは極力context依存なコードを書かない*2
  • ComponentをCatalyst以外から使いたいときはCOMPONENTメソッドで初期化する。

でこれってどんな事が言えるかって事なんですけど、

  • 上手くsearch_extraを使えば異なるCatalystプロジェクトで作成したComponentが再利用できる
  • Context依存コードを極力少なくすると単体のモジュールとして使えるヨ

って事になる訳ですな。

Modelに関しては当然そうあるべきだと思うし、Viewでも例えばCatalyst::View::Jempleteとか、
staticなファイル作るときにこのViewクラスが使えたら〜とかとか。
応用できるんじゃないかなーと思うわけです。

あるいは、そもそも別のCPANモジュールとして作ったComponentを後付的に呼び出す事も可能な訳です。

*1:もしどうしてもCatalyst::Componentを継承したくない特別な理由があれば、Module::Pluggableの設定をゴニョゴニョすれば出来るとは思います…意味無いと思うけど。

*2:最もviewも物凄い依存してるケースがほとんどだし、そうせざるを得ない気がするけど