NAME
Book::Chinese::MasterPerlToday::Catalyst - Catalyst Framework
DESCRIPTION
本章主要介绍如何使用 Catalyst 来构建一个程序。
cpan> install Catalyst::Runtime
cpan> install Catalyst::Devel
cpan> install Catalyst::Manual
详尽的文档请参阅 Catalyst::Manual
前言
所有模块或工具的用意都在于简化您任务所需的代码。
Catalyst 采用当前最流行的 MVC 结构。
V(View)
MVC 中的 V 是比较清晰的,输出可以是直接的文本($c->res->body),可以是 HTML(如 Catalyst::View::TT), 可以是 RSS, 或者是 JSON 或其他的(如 Wx 等 GUI)。所有的输出都是经过 ->res->body 或 ->res->write, 最终由 Catalyst::Engine::* 来渲染。
M(Model)
Catalyst 中的 M 是瘦 Model. 常见的通过 Catalyst::Model::DBIC::Schema 和 Catalyst::Model::Adaptor 来加载外部 Model. 使用这种方法的好处是:真正的 DBIx::Class 结构 ORM 或其他模块可以作为 Catalyst Model,也可以被 cron 等外部 pl 脚本使用。
C(Controller)
Controller 与 Model 是最容易混淆的结构。有些代码你不知道放 Model 里还是放 Controller 里好。个人浅见,以该代码是否可以被外部 pl 程序调用来决定放哪里。
Controller 最大的用处是定义一系列的 URI 和通过 params 或 Model 的返回值来确定使用采用什么 action。简而言之,连接 Model 和 View 的一个桥梁。
扩展 Catalyst
参阅 Catalyst::Manual::ExtendingCatalyst, Catalyst::Manual::Internals, Catalyst::Manual::Plugins
Plugin
Plugin 在 5.80 以前(乃至现在)是使用最广的一种扩展方式,虽然有些 plugin 其实没必要成为一个 plugin。
Plugin 通过更新 Catalyst 流程中的某些子程序,或者给 $c 添加一些函数。
通常更改 Catalyst 流程的 plugin 是值得的。但是如果仅仅是方便调用,给 $c 添加某个函数,是非常没有必要成为一个 plugin 的。
Catalyst 的整个流程包括两个部分。
setup
一个是 setup 部分,在 Catalyst 启动的时候调用一次。
setup setup_home setup_log setup_plugins setup_dispatcher setup_engine setup_stats setup_components setup_actions setup_finalize
上述 setup_* 函数我们一般不进行改动,如果自定义的 Plugin 需要在启动的时候检查 config 或做一些初始化的话,一般直接改 sub setup
use MRO::Compat; sub setup { my $c = shift; $c->maybe::next::method(@_); $c->setup_my_plugin_stuff; }
上述 setup plugin 例子有 Catalyst::Plugin::ConfigLoader, Catalyst::Plugin::I18N 等
Catalyst::Plugin::Server 使用了 setup_dispatcher
request
另一个是 request 部分,每个 url 的 request 都调用一次。
handle_request prepare prepare_request prepare_connection prepare_query_parameters prepare_headers prepare_cookies prepare_path prepare_body (unless parse_on_demand) prepare_body_parameters prepare_parameters prepare_uploads prepare_action dispatch finalize finalize_uploads finalize_error (if one happened) finalize_headers finalize_cookies finalize_body
这种 plugin 很多。
Catalyst::Plugin::PageCache 使用 dispatch+finalize
Catalyst::Plugin::Unicode 使用 prepare_parameters+finalize
Controller/View/Model
这种类型的扩展一般用于被 use base 或 use parent
View
View 是最常见的一种扩展,最常用的如 Catalyst::View::TT, Catalyst::View::JSON, TT::Alloy, PHP, Mason, Template::Declare, GraphViz, GD 等。所有的 View 扩展都基于 Catalyst::View, 它们都覆盖 sub process, 并且在函数最后用 $c->res->body 输出内容。
Model
Model 的扩展不多,最常用的当然是 Catalyst::Model::DBIC::Schema 和 Catalyst::Model::Adaptor. 大部分的 Model 都跟 storage 有关。
所有的 Model 扩展语法跟我们平常写的并无任何区别,所不同的仅仅在于公用性。
写 Model 扩展的时候,有时候我们会用到 Catalyst::Component::InstancePerContext, 该模块是对 ACCEPT_CONTEXT 的一个技巧应用。当你的模块需要在每个 request 里都应用一些代码时非常有用。
Controller
Controller 的模块并不多,毕竟共享的 Controller 内容很窄。更多的我们将在下文中讲到。
最有名气的当属 Catalyst::Controller::WrapCGI 该模块能让 cgi 脚本运行在 Catalyst 里,这将有助于你的计划安排,你可以在以后恰当的时间将该 cgi 改为 Catalyst
其他的有 Catalyst::Controller::REST, Catalyst::Controller::reCAPTCHA
ActionClass
Action 类似于 Moose 的 around
, 最常见的 ActionClass 是 Catalyst::Action::RenderView
所有的 Action 都基于 Catalyst::Action 并且需要 sub execute, 原始的 sub 调用通过 MRO::Compat, 大致类似
sub execute {
my $self = shift;
my ($controller, $c ) = @_;
$self->next::method( @_ );
$self->next::method( @_ ); 可以放到 sub execute 的任何地方。这意味你可以在原始 sub 之前写代码也可以在其之后。
Controller 属性
如果你阅读过 Catalyst::Controller 的源码的话,你会发现一些 _parse_*_attr 的 sub 类如 _parse_Global_attr, _parse_Path_attr, _parse_Regex_attr, _parse_Chained_attr 等
我们可以通过自定义属性和增加该属性对应的 _parse_*_attr 来扩展 Catalyst Controller.
这种类型的扩展有 Catalyst::Controller::ActionRole, Catalyst::Controller::SOAP
这种类型的扩展一般直接返回一个 ActionClass, 或配合 create_action
来进行操作。
其它 (TraitFor)
因为基于强大的 Moose 系统,所以我们也可以用一些 Moose 的方法来扩展 Catalyst.
比如基于 CatalystX::RoleApplicator 的 Catalyst::TraitFor::Request::ProxyBase, Catalyst::TraitFor::Request::BrowserDetect (Catalyst::Plugin::Browser)
基于 CatalystX::Component::Traits 的 Catalyst::Model::DBIC::Schema
Session 和 Authentication
Catalyst 的高度可扩展名声很大程度上来自 Session, Authentication 和 View
Session
我们所说的 Session 指的是 Catalyst::Plugin::Session
Session 分为两个部分
State
State 一般推荐只有 Catalyst::Plugin::Session::State::Cookie, URI State 不太安全
Store
Store 有很多种,你可以存到 DBI (DBIC) 里也可以存到 Memcached 或 FastMmap
永久登录可以参阅 Catalyst::Plugin::Session::DynamicExpiry, 该模块通过每次都更改 cookie 的 Expire 日期来获得长时间的储存。
坦白说,该 Session 模块并不是很好。因为统一的 API 接口缺陷,你不能通过一个 user_id 来删除该用户的 session 数据。Session 模块的过期数据清理也需要自己做。
但是整体来说,还是做到了应该需要做到的。
最后,我们以 Catalyst::Plugin::Session::State::Cookie + Catalyst::Plugin::Session::Store::DBIC 为例,讲述一点内部知识。
sessionid
第一步,我们将在用户调用 $c->session 或 $c->sessionid 或其他的时候,通过
get_session_id
来获得 sessionid在 State::Cookie 里,get_session_id 仅仅是查询 $c->request->cookies->{$cookie_name};
session
在得到 sessionid 之后,我们将在用户调用 $c->session 或 ->flash 的时候,通过
get_session_data
来获得 session 的数据。同时,我们通过 Object::Signature 来得到 session 数据的一个当前 signatureget_session_data 函数在 Catalyst::Plugin::Session::Store::Delegate, 它将通过
session_store_delegate_key_to_accessor
来调用得到 session("session:$sid"), expires("expires:$sid")request
上述流程当在调用 $c->session 时发生,如果不调用就不会通过 Store 得到 session 数据。
request 里你可以只读的调用 $c->session->{key} 或者可写 delete $c->session->{far} 或 $c->session->{bar} = 1;
finalize_headers
finalize_headers 主要用于更改 cookie 的 Expire 时间。
个人认为这不是很好,因为每次 request 都需要更改 cookie 的 Expire 时间,这很浪费。一般来说,要么 cookie 为浏览器进程 cookie, 要么就保存的久一点,不用每次 request 都更新 cookie 的 Expire
finalize_session
该过程在 finalize_body 之前调用。
首先我们将使用 Object::Signature 来得到当前 session 数据 ($c->_session) 的 signature 值。然后跟上面 get_session_data 之后的值比较。
如果在 request 中只是只读的调用 $c->session 的话,那这两个值应当是一样的。
如果进行了新增更改或删除的话,那这两个值就不一样了。这时候我们就会调用 Store 里的
store_session_data
来更新数据。这里还有个更新 session 的 expires 时间。这个跟上面的更新 cookie Expire 时间一样,每次 request 都会调用。有点浪费。
Authentication
Authentication 指的是 Catalyst::Plugin::Authentication
Authentication 分为两个部分
Credential
Credential 是指通过什么方式来验证这个 user 提供的登录信息是否正确。最常用的是 Catalyst::Authentication::Credential::Password, 通过 password 来验证。
其他的还有 OpenID, OAuth, Authen::Simple, HTTP, 甚至 Flickr, FBConnect等
所有的 Credential 模块都拥有 sub authenticate
Store
Store 是指去哪里找到这个 user, 最常用的是 Catalyst::Authentication::Store::DBIx::Class
其他的还有 LDAP, RDBO, KiokuDB, Jifty::DBI, Htpasswd 等
Store 一般分为 2 个模块,一个是主模块用于如何查找 user (通过 sub find_user),另一个是 User 模块,该模块定义了 from_session, for_session, get_object, AUTOLOAD 等。
我们将以 Catalyst::Authentication::Credential::Password + Catalyst::Authentication::Store::DBIx::Class 为例,讲述一点内部知识。
流程
在 $c->authenticate 验证通过后,将通过 for_session 来 insert 些数据到 $c->session 里。
在以后的 request 里,通过 from_session 可以得到 $c->user
$c->user
用 Catalyst::Authentication::Store::DBIx::Class 的话,$c->user 就是 Catalyst::Authentication::Store::DBIx::Class::User
用 Catalyst::Authentication::Store::Jifty::DBI 的话,$c->user 就是 Catalyst::Authentication::Store::Jifty::DBI::User
所有的这些 Catalyst::Authentication::Store::*::User 都定义了 get_object, $c->user->get_object 才返回真正的 user 数据。
而当你调用 $c->user->xxxx 的时候,一般是调用了 Catalyst::Authentication::Store::*::User 的
AUTOLOAD
FAQ
auto 中怎么用 $c->detach 不好使?
if ( $bla ) { $c->forward('/where/some'); return 0; } return 1;
在 sub auto 中,return 值是非常重要的。return 0 就直接去 sub end 了。
param 和 params
use Data::Dumper; sub test_params :Path('test_params') { my ($self, $c) = @_; my $test = { a => $c->req->param('a'), b => $c->req->param('b'), c => $c->req->param('c'), }; $c->res->body(Dumper(\$test)); }
当 DBIx::Class ->create 的时候,上文这种格式还是比较常见的。但是这种格式返回的结果可能会让你大吃一惊。
E:\Fayland\chinese-perl-book\eg\Catalyst\TestApp>perl script/testapp_test.pl /test_params?b=1 # 省略输出 $VAR1 = \{ '1' => 'c', 'a' => 'b' };
快速的修正可以使用 params
sub test_params2 :Path('test_params2') { my ($self, $c) = @_; my $test = { a => $c->req->params->{'a'}, b => $c->req->params->{'b'}, c => $c->req->params->{'c'}, }; $c->res->body(Dumper(\$test)); } E:\Fayland\chinese-perl-book\eg\Catalyst\TestApp>perl script/testapp_test.pl /test_params2?b=1 # 省略输出 $VAR1 = \{ 'c' => undef, 'a' => undef, 'b' => '1' };
Catalyst 例子
-
Catalyst & DBIx::Class 所基于的 Wiki
-
基于文件的博客
-
A base class for Catalyst MVC components
-
论坛
资源
一年一度的 Advent: http://dev.catalystframework.org/wiki/adventcalendararticles
IRC: irc.perl.org#catalyst
AUTHOR
Fayland Lam, <fayland at gmail.com>
COPYRIGHT & LICENSE
Copyright (c) 2009 Fayland Lam
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.