Dec 14, 2008

Sledgeのソースを追った記録。

社内の共通言語がPerlなところに最近入社したわけですが、殆どのサービスがSledgeで作られているので、理解する必要がありありなんですけど、Perlが危ういレベルなので解説記事を見てもちゃんと理解できませんでした。で、最近「続・初めてのPerl」を読み出してようやくOOPerlをやる上で必要な情報に触れ出したので、ちゃんとソースコードを追いかけてみました。頭に残るようソースを追った記録を残しておきたいと思います。

あ、自分のPerl歴とかを書いておくと、

  • 約1年前くらいに、man perlから情報を辿りながらちょっとしたテキストツールは作ったことがある。(dbのselectした結果をファイルに吐いて、正規表現で良い具合に整形して、何かとデータを付き合わせるみたいの)
  • ちゃんとした勉強を開始したのは12/1から。
  • 最初に初めてのPerlを読んだ。ただし、ファイル操作とかプロセス管理は斜め読み。
  • 今は続・初めてのPerl 改訂版を読んでる。リファレンスが出てきたところ。
  • オブジェクト指向はそこそこ理解しているつもり。

ぐらいです。なので、見る人もそういう覚悟でお願いします。題材ですが「Sledgeのインストールと設定方法」で追ってみます。

まず、スタートはhtdocs/index.cgiへのリクエストから。以下のスクリプトが実行されます。

#!/usr/local/bin/perl

use strict;
use Hello::Pages::Root;
Hello::Pages::Root->new->dispatch('index');

Hello::Pages::Root->new->dispatch('index'); ここが肝で、意味はHello::Pages::Rootってモジュールのnewメソッドを呼び出してインスタンス生成して、dispatchメソッドを呼び出す。 OOらしいコードです。そういうわけで次はHello::Pages::Rootモジュールを見ます。ファイルは、lib/Pages/Root.pm。

package Hello::Pages::Root;
 
use strict;
use base qw(Hello::Pages);
 
__PACKAGE__->tmpl_dirname('.');
 
sub dispatch_index {
    my $self = shift;
    $self->tmpl->param(string => 'Hello World!');
}
 
1;
で、ここにはnewメソッドはないわけだけど、「use base モジュール名」な構文でクラスの継承が可能。なので、次はHello::Pages(lib/Hello/Pages.pm)を見る。
package Hello::Pages;
use strict;
use Sledge::Pages::Compat;

use Sledge::Authorizer::Null;
use Sledge::Charset::Default;
use Sledge::SessionManager::Cookie;
use Sledge::Session::MySQL;
use Sledge::Template::TT;

use Hello::Config;

# メソッドの定義が以下に続く

ここにもnewメソッドは定義されてません。ソレハオカシイダロってことでちょっと調べてみたら、実はuse base以外にも継承方法があって、それぞれのクラスが持つISAって名前の配列に親クラス名をぶっこむ方法。で、Perlでは「use ModuleName」をしたときに、ロードされた側のモジュールにimportってメソッドがあった場合にuseのタイミングで実行されます。なので、useしているモジュールのどこかにimportメソッドがあって、さらにISAに親クラスを入れるコードがあろうものなら、継承になるようです。。。

まあ、名前的にSledge::Pages::Compatが一番怪しいので中を見たところ、
# perldoc -l Sledge
# で、Sledge.pmの場所がわかるので、その辺を探す。

package Sledge::Pages::Compat;
# $Id: Compat.pm,v 1.1.1.1 2003/02/13 06:59:36 miyagawa Exp $
#
# Tatsuhiko Miyagawa 
# Livin' On The EDGE, Co., Ltd..
#

use strict;
use constant MOD_PERL => defined $ENV{MOD_PERL};

sub import {
    my $base = MOD_PERL ? 'Sledge::Pages::Apache' : 'Sledge::Pages::CGI';
    eval qq{require $base};
    {
		my $pkg = caller;
		no strict 'refs';
		unshift @{"$pkg\::ISA"}, $base;
    }
}

1;

importメソッドの中にある「unshift @{"$pkg\::ISA"}, $base;」ってのがそれ。
# この継承方法嫌だなー
やってることはコードのとおり、実行環境がmod_perlかcgiかによって継承元を振り分け。callerは、このモジュールをuseしたモジュール名を返してくれるようです。僕はmod_perlで動かしていたので、Sledge::Pages::Apacheから継承。

package Sledge::Pages::Apache;

use strict;
use base qw(Sledge::Pages::Base);

use Apache;
use Apache::Request;

sub create_request {
    my($self, $r) = @_;
    my $req = Apache::Request->new($r || Apache->request);
    $req->param;		# do parse here
    return $req;
}
ここにもnewはないけど、Sledge::Pages::Baseを継承している。で、それを見ると・・・
sub new {
    my $class = shift;
    my $self = bless {}, $class;
    $self->init(@_);
    $self->invoke_hook('AFTER_INIT');
    return $self;
}

あった。これがHello::Pages::Root->newで使われているメソッド。ひとまず主要な流れがわかれば満足なので、invoke_hookはここでは無視。自分的なメインラインはinit。initを見る。newの真下にあります。

sub init {
    my($self, $r) = @_;
    $self->r($self->create_request($r));
    $self->authorizer($self->create_authorizer);
    $self->manager($self->create_manager);
    $self->charset($self->create_charset);
}

rメソッドです。rはなんだ?と調べてみると、Sledge::Pages::Baseの頭付近にこんなコードが。

__PACKAGE__->mk_accessors(
    'r',           # Apache::Request or Sledge::Request::CGI
    'session',     # Sledge::Session
    'manager',     # Sledge::SessionManager
    'authorizer',  # Sledge::Authorizer
    'charset',     # Sledge::Charset
    'tmpl',        # Sledge::Template
    'fillin_form', # Sledge::FillInForm
    'finished',    # flag whether request is finished
    'page',        # page name (arg to dispatch())
    'filters',     # filter subs
);

__PACKAGE__は自分自身。つまりSledge::Pages::Baseを意味し、mk_accessorsは親クラスにあたるClass::Accessorのメソッド。このメソッドを使うと。引数でわたした名前のアクセサメソッドをつくってくれるようです。例えばrの例だと、

r(値);
で、値をrに設定。
r();
で読み出し。

initメソッドに戻って、rの部分を見ると「$self->r($self->create_request($r))」。 create_requestはさっき見たSledge::Pages::Apacheの中で定義してありましたねー。でまあ、やっていることはリクエストを抽象化したオブジェクトのセットです。$r経由でリクエストパラメータにアクセスとかできる感じですね。以降のコードも同様に、認証(authorizer)、セッション管理(manager)、文字コード(charset)の抽象オブジェクトをセット。最後にこのインスタンスを返しているので、「Hello::Pages::Root->new」で返ってくるインスタンスで色んな値を参照できるし、色んなメソッドが実行できるよ!ってところですね。

newの次はdispatch。これもSledge::Pages::Baseに定義されていました。

sub dispatch {
    my($self, $page) = @_;
    return if $self->finished; # already redirected?

    local *Sledge::Registrar::context = sub { $self };
    Sledge::Exception->do_trace(1) if $self->debug_level;
    eval {
        $self->init_dispatch($page);
        $self->invoke_hook('BEFORE_DISPATCH') unless $self->finished;
        if ($self->is_post_request && ! $self->finished) {
            my $postmeth = 'post_dispatch_' . $page;
            $self->$postmeth() if $self->can($postmeth);
        }
        unless ($self->finished) {
            my $method = 'dispatch_' . $page;
            $self->$method();
            $self->invoke_hook('AFTER_DISPATCH');
        }
        $self->output_content unless $self->finished;
    };
    $self->handle_exception($@) if $@;
    $self->_destroy_me;
}

postとgetでルートを分けているみたいですね。今回通るルートだと、init_dispatchを実行した後dispatch_indexを実行なので、init_dispatchから確認したいと思います。

sub init_dispatch {
    my($self, $page) = @_;
    $self->page($page);
    $self->construct_session unless defined $self->session;
    $self->authorizer->authorize($self);
    $self->charset->convert_param($self);
    $self->load_template($page);
    $self->load_fillin_form if $self->is_post_request;
}

と言っても名前の示すとおりです。$self->page($page)は、「Hello::Pages::Root->new->dispatch('index');」で渡した"index"という名前を保存。construct_sessionはセッション情報が無い場合にセッションIDとかを生成、authorizerは認証機構に使うクラスのようです。charsetはリクエストパラメータの文字コード変換。どの文字コードを使用するかは、Hello::Pages(lib/Hello/Pages.pm)に定義されている以下のメソッドで決まります。

sub create_charset {
    my $self = shift;
    return Sledge::Charset::Default->new($self);
}

Sledge::Charset::Defaultはeuc-jpですが、Sledgeのインストールしてあるディレクトリ配下のCharsetという名前のディレクトリ以下を見ると、Shift_JIS.pm、UTF8.pmとかも用意してあるので、このメソッドの最後の行を、return Sledge::Charset::UTF8->new($self);としてあげればUTF-8に変換してくれるようです。

init_dispatchに戻って続きを見ると、load_template($page)です。ここでview/index.htmlをテンプレートとして使うために$selfに読み込ませてます。最後の$self->load_fillin_formですが、postの場合なので流します。

次がいよいよ最後のdispatch_indexです。lib/Hello/Pages/Root.pm内にあります。今回の例ではこんなのでした。

sub dispatch_index {
    my $self = shift;
    $self->tmpl->param(string => 'Hello World!');
}

view/index.htmlのテンプレートを埋めるだけですね。ここで生成されたHTMLをdispatchメソッドの最後の$self->output_contentによってクライアントに返して終了となります。


と、レスポンスまでのコードを追ってみましたとさ。

Posted at 08:20 in | WriteBacks (1) | Edit
Edit this entry...

wikieditish message: Ready to edit this entry.
















A quick preview will be rendered here when you click "Preview" button.