MVCその5
フレームワーク化する
仕上げに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でやり直そう。