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::SchemaCatalyst::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::SchemaCatalyst::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 类似于 Moosearound, 最常见的 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::RoleApplicatorCatalyst::TraitFor::Request::ProxyBase, Catalyst::TraitFor::Request::BrowserDetect (Catalyst::Plugin::Browser)

基于 CatalystX::Component::TraitsCatalyst::Model::DBIC::Schema

Session 和 Authentication

Catalyst 的高度可扩展名声很大程度上来自 Session, Authentication 和 View

Session

我们所说的 Session 指的是 Catalyst::Plugin::Session

Session 分为两个部分

永久登录可以参阅 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 数据的一个当前 signature

    get_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 例子

资源

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.