読者です 読者をやめる 読者になる 読者になる

Perl日記

PerlとかRubyとかPHPとかPythonとか

MVCその5

続き。
WEB+DB PRESS Vol.44

フレームワーク化する

 仕上げにControllerをより汎用的なものに昇華させます。(略)新しい機能が必要になったときに「プログラム本体に手をつけずにプラグイン的に処理を追加できる」ということです。
WEB+DB PRESS Vol.44 P172

え、いきなりそんなとこまでいくんですか。
なんて思ってしまったけれど、そうかこれがフレームワークか。

ライブラリパスに置いただけ(プログラム本体側には手をつけない)で/helloにアクセスすると「Hello,World!」が表示される、というControllerに仕上げるのです。
WEB+DB PRESS Vol.44 P172

うーん、URLでディスパッチさせるのかー。
確かに最近「/」が多いURLをよく見た気がするけど、そういうとこって全部URLで振り分けしているんだろうか。
普通にディレクトリ掘ってるのかと思ってたけど…。


じゃあコントローラを分けてみる。
とはいえ、ほとんど、本に載ってたサンプル。

ディスパッチャ

フレームワーク本体。

lib/MyHatena.pm
package MyHatena;
use strict;
use warnings;
use base qw/Class::Accessor::Lvalue::Fast/;
use MyHatena::View;
use MyHatena::Response;
use UNIVERSAL::require;

__PACKAGE__->mk_accessors(qw/request path/);

sub dispatch {
  my $self = shift;

  my $con = $self->controller;

  $con->req = $self->request;
  $con->res = MyHatena::Response->new;
  $con->view = MyHatena::View->new({
    path_segments => [$self->path_segments]
  });

  $con->handle_request;

  return $con->res;
}

sub controller {
  my $self = shift;
  my $handler = join('::', ('MyHatena', 'Controller',
                map { ucfirst } $self->path_segments));
  $handler->require or die $@;
  return $handler->new;
}

sub path_segments {
  my $self = shift;
  $self->path ||= '/';

  my @path_segments = split '/', $self->path;
  shift @path_segments;
  push @path_segments, 'index' unless @path_segments;

  return @path_segments;
}

1;
__END__

$self->path のところには最初にCGI.pmの$q->path_infoを入れておく。
つまりは、$ENV{PATH_INFO}だ。
おお、ここで前に勉強したことが役に立つとは。

コントローラ

lib/MyHatena/Controller.pm

Contlloerの親。

package MyHatena::Controller;
use strict;
use warnings;
use base qw/Class::Accessor::Lvalue::Fast/;

__PACKAGE__->mk_accessors(qw/req res view/);

sub handle_request {
  my $self = shift;

  # Call Controller inherited
  $self->do_task;

  # responseがなければViewがRender
  if (! defined $self->res->content) {
    $self->res->content = $self->view->render;
  }

  $self->res->content_type ||= 'text/html';

  return $self->res;
}

1;
__END__

do_task() は、このController.pmを継承しているクラスが定義する。
このControllerだけではなにもできない。

lib/MyHatena/Response.pm

リクエストの受け取りと、レスポンスの返却用の入れ物。

package MyHatena::Response;
use strict;
use warnings;
use base qw/Class::Accessor::Lvalue::Fast/;

__PACKAGE__->mk_accessors(qw/content_type content/);

1;
__END__

入れ物だけ。

lib/MyHatena/Contoller/Search.pm

アプリケーション側のコントローラ。
アプリケーション側のコントローラをどんどん増やしていくことによって、アプリケーションを充実させていくことができる。

package MyHatena::Controller::Search;
use strict;
use warnings;
use base qw/MyHatena::Controller/;
use MyHatena::Tag;

sub do_task {
  my $self = shift;

  my $query = $self->req->param('search_tag');

  my $entries;

  if (defined $query) {
    my $result = MyHatena::Tag->search($query)
      or die "Can't search by [$query].";
    $entries = [$result->entries];
  }
  else {
    $entries = [];
    $query = q{};
  }

  $self->view->vars(
    result => $entries,
    search_tag => $query,
  );
}

1;
__END__

Modelである検索処理を呼び出すコントローラ。
結果をview用のオブジェクトに入れておく。
というわけで、ViewはViewで改めて分離する。

lib/MyHatena/View.pm
package MyHatena::View;
use strict;
use warnings;
use base qw/Class::Accessor::Lvalue::Fast/;
use Template;

__PACKAGE__->mk_accessors(qw/path_segments vars/);

sub render {
  my $self = shift;
  my $template = Template->new(ABSOLUTE => 1);
  $template->process($self->template_file, $self->vars, \my $out)
    or die $template->error;
  return $out;
}

sub template_file {
  my $self = shift;
  my $file = '/Users/rightgo09/Script/Perl/template/'
           . join('/', @{$self->path_segments});

  $file .= '.html';

  return $file;
}

sub vars {
  my $self = shift;
  $self->{vars} ||= {};

  if (@_) {
    my %args = @_;
    while (my ($key, $value) = each %args) {
      $self->{vars}->{$key} = $value;
    }
  }
  return $self->{vars};
}

1;
__END__

Controllerで何も表示するもの(content)がなければ、URLに沿ったテンプレートが選ばれて、そのテンプレートを表示する。
varsの中身を置き換えながら。

CGI

tag_search5.cgi

フレームワークの起動とディスパッチ。

#!/use/local/bin/perl
use strict;
use warnings;
use CGI;
use FindBin::libs;
use MyHatena;

my $q = CGI->new;

my $app = MyHatena->new;
$app->request = $q;
$app->path    = $q->path_info;

my $res = $app->dispatch;

print $q->header( -type => $res->content_type, -charset => 'utf-8' );
print $res->content;

__END__

ディスパッチして返ってきたレスポンスオブジェクトを表示するだけ。
すごいなー。
他の処理は全部モジュールに任せてしまっている。
一枚岩のCGIばっかりやってきたからこのシンプルさはすごく新鮮だ。

ここまでまとめ

  • Modelはアプリケーションの処理の中心。ドメインロジックを実装するところ
  • Controllerは仲介者
  • Viewは見た目

WEB+DB PRESS Vol.44 P174

Fat Model, Skinny Controllerは聞いたことあるけど、こういうことなのかな。
メインはあくまでメインでまとめる。
見た目はあくまで表示くらいしかしない。
仲介はあくまでそれらを仲介するだけ。


ということで初めてMVCに触れてみたけれど、アプリ側のControllerがちょっと重たい印象。
いやまあ仲介するのはそれだけ大変!、みたいなことなんだろうけど。


あとClass::Accessor::Lvalue::Fastは地味に便利だった。


さて、じゃあCatalystでやり直そう。