Hatena::ブログ(Diary)

Yet Another Hackadelic

2006-11-05 I hope to be real hacker!

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を後付的に呼び出す事も可能な訳です。

Catalyst::Log::Log4perlの出力先を任意のファイルにする。

始めに

Catalyst::Logを敢えてLog::Log4perlに変えるケースって良くあるとは思うんですが、デフォルトだとstderrへの書き込みのようなので、起動のさせ方に依っては嬉しくない記載になる可能性が高いです。*3

と言う訳できちんと外部のファイルに明示的に出力する方法です。

追記

path_toがsetup_home実行前だと使えないとかって記述が不適切*4だったので、修正しました。

Catalyst::Log::Log4perlでのLog4perlの呼び出しを確認

ドキュメントにも書いてありますが、

new($config, [%options])

This builds a new Catalyst::Log::Log4perl object. If you provide an argument to new(), it will be passed directly to Log::Log4perl::init.

snip ...

Without any arguments, new() will initialize a root logger with a single appender, Log::Log4perl::Appender::Screen, configured to have an identical layout to the default Catalyst::Log object.

Catalyst::Log::Log4perl->new
基本的にはargumentsを指定するとLog::Log4perl->initに直接それを代入します。 またargumentsを指定しない場合は、基本的にはひとつのappenderとなり、AppenderオブジェクトはLog::Log4perl::Appender::Screenになります。 そしてその出力LayoutはCatalyst::Logと同等になるとあります。 newメソッドのargumentsを指定しない場合の処理は下記になります。
my $log      = Log::Log4perl->get_logger("");
my $layout   = Log::Log4perl::Layout::PatternLayout->new("[%d] [catalyst] [%p] %m%n");
my $appender = Log::Log4perl::Appender->new(
    "Log::Log4perl::Appender::Screen",
    'name'   => 'screenlog',
    'stderr' => 1,
);
$appender->layout($layout);
$log->add_appender($appender);
$log->level($DEBUG);
これを外部出力としたい場合は前述の通りargumentsを指定して、Log::Log4perlインスタンスを生成する事になります。

CatalystのLogオブジェクトとCatalyst::Log::Log4perl

基本的にはCatalyst::Log::Log4perlの使い方としては、
package MyApp;

use strict;
use warnings;

use Catalyst::Runtime '5.70';

use Catalyst qw/-Debug ConfigLoader Static::Simple/;
use Catalyst::Log::Log4perl;

our $VERSION = '0.01';

__PACKAGE__->config(
    name => 'MyApp',
);

__PACKAGE__->log(Catalyst::Log::Log4perl->new("$FindBin::Bin/../conf/log4perl.conf"));
__PACKAGE__->setup;
のようになります。 ところでCatalystの場合はsetupメソッド中のsetup_logメソッドによりlogインスタンスが初期化されます。 setupメソッドの当該処理の前後を見てみましょう。
# Process options
my $flags = {};

foreach (@arguments) {
    if (/^-Debug$/) {
        $flags->{log} =
          ( $flags->{log} ) ? 'debug,' . $flags->{log} : 'debug';
    }
    elsif (/^-(\w+)=?(.*)$/) {
        $flags->{ lc $1 } = $2;
    }
    else {
        push @{ $flags->{plugins} }, $_;
    }
}

$class->setup_home( delete $flags->{home} );
$class->setup_log( delete $flags->{log} );
  • Debugが指定された時の$flags->{log}への設定なんですが、
事前に$flags->{log}が設定されている場合はその値をカンマ区切りで追記する形になっています。 この予め指定されてる状況は-log=warnみたいな感じで@argumentsに入れた場合または、-Debugが複数指定された場合で、
use Catalyst qw/-Debug -log=warn,info/;
のように指定出来る模様。但しこれを指定した所で、デフォルトでの挙動は全然変わりません。*5 ここの$flags->{log}に関しては実際のsetup_logメソッドを見てみます。
sub setup_log {
    my ( $class, $debug ) = @_;

    unless ( $class->log ) {
        $class->log( Catalyst::Log->new );
    }

    my $app_flag = Catalyst::Utils::class2env($class) . '_DEBUG';

    if (
          ( defined( $ENV{CATALYST_DEBUG} ) || defined( $ENV{$app_flag} ) )
        ? ( $ENV{CATALYST_DEBUG} || $ENV{$app_flag} )
        : $debug
      )
    {
        no strict 'refs';
        *{"$class\::debug"} = sub { 1 };
        $class->log->debug('Debug messages enabled');
    }
}
渡した$flags->{log}はsetup_logメソッド内では$debug変数に格納されます。
まず$class->logが指定されていない場合は、Catalyst::Logが使用されます。 複数ある環境変数がtrueと解釈されるか、それらが指定されていないならば、 $debugがtrue扱いになっていれば、デバッグ扱いとなり、loggingが開始されると言う寸法です。
注意しなければならないのはsetup_logの直前にてsetup_homeが呼ばれていると言う事です。 即ちsetupが終了していない場合は、configで事前にhomeパラメーターを指定しない限りは、path_toメソッドが使えないって事です。但し予めhomeパラメータを何らかの方法で指定してあれば当然使えます。 良く考えたらCatalyst->importでsetup_home呼んでるから不要でした。 普通にpath_to使えます。

Catalyst::Log::Log4perlと同等のLog4perlの設定ファイル

大体下記のようになります。
log4perl.logger = DEBUG, A1
log4perl.appender.A1 = Log::Log4perl::Appender::File
log4perl.appender.A1.layout= Log::Log4perl::Layout::PatternLayout
log4perl.appender.A1.layout.ConversionPattern = [%d] [catalyst] [%p] %m%n
log4perl.appender.A1.filename = ../logs/myapp.log
ファイル名の部分は適当にアレンジして下さい。

MyAppにCatalyst::Log::Log4perlを設定する

path_toを使わないならば、こんな記述になるでしょう。
use FindBin;
use Catalyst::Log::Log4perl;

__PACKAGE__->log(Catalyst::Log::Log4perl->new("$FindBin::Bin/../conf/log4perl.conf"));
これでも十分動作します。 あるいはpath_toを使う場合は、
__PACKAGE__->log(Catalyst::Log::Log4perl->new(__PACKAGE__->path_to(qw/conf log4perl.conf/)->stringify));
stringifyメソッドを明示的に叩かないとLog4perl内で怒られます。w
多分、path_toを使った方がCatalyst風なんでこちらをお勧めしておきます。

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

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

*3:FastCGI by Apacheなど

*4:Catalyst->importで実行済みだから気にしなくて良い

*5:もし意味を持たせたいならば、setup_logメソッドを拡張する必要があります。