IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Linux | Web development  >

使用 Maypole 构建 Web 应用程序

用于快速简便地构建有数据库支持的应用程序的 Perl 框架

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

Simon Cozens (simon@simon-cozens.org), 自由作家和程序员

2004 年 6 月 25 日

Simon Cozens 将他对啤酒的喜爱转化为一个 Perl 应用程序服务器——从一个简单的前端到数据库服务器,再发展为一个社区网络 Web 应用程序。无论如何,他从啤酒开始讲起。

我最近一个项目——Maypole——的开发始于 4000 多年前,故事开始时,Ninkasi 女神向闪族(Sumerian)人 传授了一种神秘饮料的配方。她的配方调合了蛇麻草、大麦、酵母和水(以及一些其他的特殊成分),立即轰动 一时,而且,从那时起就成为了大学教育的一个基石。

当我还是一个学生时,我将所有的啤酒分为几类:Guinness、fizzy lager 和 proper English bitter。在 2000 年的开放源代码大会(Open Source Conference 2000)上,Adam Turoff 送给我一瓶美味的 Chimay Blue,使我茅塞顿开,我开始进入 到一个各种各样味道和细微口味的啤酒世界。

Ninkasi 的配方的最大问题是,最终得到的啤酒味道越好,您喝的就越多,而且,出于一些原因, 您早晨起床时就会越记不清它有多好,所以,您永远不知道您是不是想再次去购买那种啤酒。 所以您无论如何还要去买来尝一尝是不是您所喜爱的。这是享受,但不经济。我发现我自己需要 某种数据库来对我的口味进行追踪。

本文不会半途而废(或者应该是 284 毫升吧?),它以一个简单的数据库开始,到一个完整的用 Perl 实现的基于 Web 的、有数据 库支持的应用程序而结束。

构建数据库

第一步简单。我创建了一个将啤酒与它们的酿酒厂和酒吧关联起来的 SQL schema:


清单 1. 啤酒数据库 schema
        create table brewery (
            id int not null auto_increment primary key,
            name varchar(30),
            url varchar(50),
            notes text,
        );
        create table beer (
            id int not null auto_increment primary key,
            brewery integer,
            style integer,
            name varchar(30),
            ...
        );
        create table handpump (
            id int not null auto_increment primary key,
            beer integer, pub integer
        );
        create table pub (
            id int not null auto_increment primary key,
            name varchar(60),
            url varchar(120),
            notes text
        );
        create table style (
            id int not null auto_increment primary key,
            name varchar(60),
            notes text
        );

然后呢?我并不希望在 mysql shell 中输入无法确定的 INSERT INTO 声明语句,尤其是在喝掉了四品脱 Hook Norton Generation 之后。 当然,我可以编写一些命令行 Perl 脚本来读取小的文本文件,并将它们输入到数据库,或者类似的方法,但 是,保持数据库及时更新、可视、易编辑的理想方法是让它拥有一个 Web 界面。

此时,我意识到这个项目突然变得更为通用,而不仅是用作追踪我喜爱的饮料的方法,我可以创建一个实用的框架, 为所有数据库提供一个 Web 前端。这开始时看起来好像工作量很大,所以我好几年都将这个想法放在一边。

后来,一些思想的聚合在 Perl 社区中带来了很大的影响:Template Toolkit、 Class::DBI 对象数据库映射层,还有用于 Web 应用程序的 MVC(Model-View-Controller)-风格的思想。 Template Toolkit 的创建者 Andy wardley 解释了其含义:

MVC-for-the-Web 实际上试图完成的是关注点的明确分离。将您的数据库代码放在一个地方,将应用程序代码放在 另一个地方,将描述代码放在第三个地方。那样,您可以随意去除或修改不同的元素,而有希望并不去影响其他部分(当然,这要取决 于您的关注点划分的合理程度)。这是通用的思路,也是良好的习惯。MVC 完成了关注点的分离,作为对输入(控制,controls)和输入(视图,views)进行明确划分的副产品。

不过,虽然有那么多的人在讨论 MVC Web 应用程序,我发现有些奇怪的是没有任何人真正地将其整理并发布为一个在 Perl 中切实可用 的通用框架。Java™ 程序员拥有 Struts,它使部署此类应用程序变得轻松而有趣,而且在当前非常流行,但是 Perl 程序员 却被迫自己写自己的——一遍又一遍。我认为每个大的 Perl 公司都有其自己的大型应用程序框架。让我们来看看它们的大致上是怎样 应用的。





回页首


剧中人物表

Kake Pugh 的关于如何不用编写大量代码就可以编写 Web 应用程序的文章(在 Resources 中查找链接) 介绍了用于开发 MVC 框架的一组通用组件,在 Maypole 中我借鉴了很多她的思想,包括相同的组件。

我们将从模型的目标开始——这是一组定义了应用程序的行为的类:当有人点击 search 时, 就会转到一些执行检索的代码,并返回结果让视图来处理。

由于大部分应用程序使用关系型数据库作为它们的数据存储,也由于对象是用于描述的简便方法,是的,是对象, 模型类通常基于一个对象数据库映射系统。在众多可以完成此功能的 Perl 库中,我最喜欢的是 Class::DBI ,因为它极其简明,而且还有一组扩展类使得它更为简单。例如,如果我这样开始:


清单 2a. 使用 CDBI::Loader 创建类
        use Class::DBI::Loader;
        Class::DBI::Loader->new( namespace => "BeerDB", dsn => "dbi:mysql:beerdb" );

然后,不可思议地, BeerDB::Beer , BeerDB::BreweryBeerDB::Pub 、 和 BeerDB::Handpump 这几个类就出现了,然后我可以指明:


清单 2b. CDBI::Loader 创建类
        my $hooky      = BeerDB::Beer->retrieve(12); # Retrieve by ID
        my $generation = BeerDB::Beer->search( name => "Generation" );
        $generation->score(9); # Good beer!

如果接下来我要指定表之间的关系,我可以像这样指明:


清单 2c. 用 CDBI::Loader 创建类
        BeerDB::Beer->has_a(brewery => "BeerDB::Brewery");
        my $rch        = BeerDB::Brewery->find_or_create( name => "RCH" );
        my $pitchfork  = BeerDB::Beer->create({
            name    => "Pitchfork",
            brewery => $rch
        });
        print $pitchfork->brewery->name;

现在,只需要两行代码就可以让模型类合理完成我期望它完成的事情。我喜欢不用很多代码就能实现的东西, 因此 Mayple 当前使用了一个基于 Class::DBI::Loader 的模型类系统。 没有理由让一个人不去发展和编写他自己的 Maypole::Model::Tangram , 或者去使用 Perl 中很多其他数据库-对象映射库的某个包装器,由于 Mayple 正是设计让您来做那些,而我们 不得不从某处开始讲起,所以我自 Maypole::Model::CDBI 开始。

Template Toolkit 可以很好地从一个应用程序中将描述逻辑抽象出来,因此 Maypole::View::TT 是它的一个 瘦包装器(thin wrapper),用模型类返回的对象来对其进行描述。类似的,有些人可能想编写 Maypole::View::HTMLTemplate ,不过,Template Toolkit 完成了我需要做的事情。

在控制端,Apache 的 mod_perl 接口是与基于 Perl 的 Web 应用程序进行交互的好途径。 我们可以使用 CGI,而且我也想在近期编写一个 CGI::Maypole ,不过,Maypole 现在有最好的前端: Apache::MVC 。(实际上,Maypole 是 CDBI 之前的 Apache::MVC ,Apache 组件完全不考虑在内。)





回页首


全部放在一起

现在我们已经有了组件。我们怎样将它们放在一起以构成我们的 Web 应用程序呢?要使用 Apache::MVC ,我们需要编写一个可以与 Apache 交互的驱动程序包。 由于应用程序只是需要进行检索、编辑、添加、删除以及查看数据,实际上程序非常短——Maypole 的 最终目的就是让您不必为所有通用操作编写任何代码。

所以,第一步,我们先继承 Apache::MVC ,然后告诉它我们计划使用哪个数据库:


清单 3. 最简单的 Maypole 应用程序
        package BeerDB;
        use base 'Apache::MVC';
        BeerDB->setup("dbi:mysql:beerdb");

setup 的专门调用将连接到数据库,为每个表设置一个类,然后让那些类去继承 Maypole::Model::CDBI

接下来我们为应用程序定义一些配置参数:站点的基 URL,这样我们可以构造到其他命令的链接; 在我们得到分页的输出之前,在一个列表中定义每页行数的最大值;以及我们想要显示的表——在我们的例子中,我们需要 将 handpump 表排除在外,因为它只是用来连接 pubs 表和 beers 表的一个链接表。如果我们要显示数据库中所有的表,我们就不需要最后一行。

BeerDB->config->{uri_base} = "http://neo.trinity-house.org.uk/beerdb/";
BeerDB->config->{rows_per_page} = 10;
BeerDB->config->{display_tables} = [qw[beer brewery pub style]];

我们需要告诉各个包它们期望的是哪种类型的数据,以使得 Class::DBI::FromCGI 可以通过 CGI 值自动地更新数据库:


清单 4. 描述可接受的数据
        BeerDB::Brewery->untaint_columns( printable => [qw/name notes url/] );
        BeerDB::Style->untaint_columns( printable => [qw/name notes/] );
        BeerDB::Beer->untaint_columns(
            printable => [qw/abv name price notes/],
            integer => [qw/style brewery score/],
            date =>[ qw/date/],
        );

最后,我们需要告诉各个类彼此如何联系。例如,当我们浏览一种啤酒并查看它的酿酒厂时,我们 不希望只是得到一个它在 brewery 表中所在行的数字 ID; 我们想要得到的是酿酒厂的名字和到显示那个酿酒厂的 页面的链接。类似的,我们需要告诉 BeerDB::Brewery 类有很多 啤酒关联到它,这样它将显示所有啤酒的列表。

我们可以使用 Class::DBI 中的 has_ahas_many 方法来做这些事情,不过我总忘记它们如何使用,所以我编写了一个小模块来 自己完成:


清单 5. Class::DBI::Loader::Relationship
        use Class::DBI::Loader::Relationship;
        BeerDB->config->{loader}->relationship($_) for (
            "a brewery produces beers",
            "a style defines beers",
            "a pub has beers on handpumps");
        1;

那就是我们需要为我们的啤酒数据库编写的所有程序——大约二十行 Perl 代码。

当然,需要在非常多的模板来为用户描述这些大量的逻辑,不过,幸运的是, Maypole 工厂默认模板提供了几乎所有我们需要的。所以,让我们安装这些应用程序并让其运行, 看看它是什么样子的。





回页首


插上电源并打开

我们已经写完了驱动程序类。现在我们需要告诉 Apache 关于它的情况,并将模板放置到位。首先, 我们将这些加入到 Apache 的配置中:


清单 6. 为 BeerDB 所做的 Apache 配置
        <Location /beerdb>
            SetHandler perl-script
            PerlHandler BeerDB
        </Location>

然后我们在 Web root 下的 beerdb 子目录下创建两个目录:custom 和 factory。 factory 将放置 Maypole 附带的模板,这些我们不需要修改。我们可以覆盖那些 模块,来为我们的应用程序进行特别的定制(不会有那么多)。例如,我们的 custom/header 将 会被每一个主页最先调用:


清单 7. custom header
        <HTML>
            <HEAD>
                <TITLE> Beer Database </TITLE>
        <META http-equiv=Content-Type content="text/html; charset=utf-8">
        <LINK title=myStyle href="/beerdb.css" type=text/css rel=stylesheet>
        </HEAD>
        <BODY>
        <DIV class="content">

我们还提供了一个叫做 frontpage 的定制页作为应用程序的一个入口点。马上,我们将看到它如何应用。


清单 8. front page 模板
        [% INCLUDE header %]
        <h2> The beer database </h2>
        <TABLE BORDER="0" ALIGN="center" WIDTH="70%">
        [% FOR table = config.display_tables %]
        <TR>
        <TD>
        <A HREF="[%table%]/list">List by [% table %]</A>
        </TD>
        </TR>
        [% END %]
        </TABLE>
        <BR>

这只是为我们前面配置的可显示表的列表中的每个表提供了到 *class*/link 页的链接。

现在我们已经都设置好了,可以将浏览器定位到 http://localhost/beerdb/beer/list,然后我们可以看到类似如下内容:


图 1. 所有啤酒的列表
图 1. 所有啤酒的列表

我们的默认模板提供了一个列表页,有各行的分页视图;查看单独条目的链接; 可以查看相关数据,比如到风格和酿酒厂页的链接;一个检索区域;和一个添加 新条目的区域。如果我们选择一个酿酒厂,我们会得到那个酿酒厂所生产的啤酒 列表:


图 2. Hook Norton
图 2. Hook Norton

我们还可以编辑记录、删除记录,等等。我们只用了二十行代码就拥有了一个 到我们数据库的功能完全的界面。





回页首


给面包涂上黄油

当然,它这样短只是因为它是一个相当小而且简单的应用程序——我们只是为数据库加了一个前端(有时也被 称为 CRUD 接口)来创建、读取、更新和删除数据库记录。

实际生活中的应用程序所要求的不仅仅是浏览数据库。作为一个简单的实际应用程序,我开发了一个类似于 Friendster 或 Orkut 的基于 Maypole 的社区网络工具。它叫做 Flox,一方面因为它将很多人集中到一起,另一方面因为它只使用于我的家乡 Oxford 和它的大学。

首先,我们来看 Maypole 如何发挥它的本领,然后我们将研究为了让 Flox 可以使用我们需要做哪些事情, 最后是实现的细节。

Maypole 基于请求对象的思想。这有一些类似于 Apache 请求对象,不过层次更高——它可以识别 表、类、模板和方法。那么,举例来说,请求过程开始时要处理 URL。这是 Maypole 本身所 毫不知情的;获得 URL 是 Maypole 如何运行的一个功能,所以抽象的 parse_location 方法实际上在 Apache::MVC 中如下实现:


清单 9. URL 如何被解析
        sub parse_location {
            my $self = shift;
            $self->{path} = $self->{ar}->uri;
            my $loc = $self->{ar}->location;
            $self->{path} ||= "frontpage";
            my @pi = split /\//, $self->{path};
            shift @pi while @pi and !$pi[0];
            $self->{table} = shift @pi;
            $self->{action} = shift @pi;
            $self->{args} = \@pi;
            $self->{params} = { $self->{ar}->content };
            $self->{query}  = { $self->{ar}->args };
        }

这样从隐藏在 Apache 中的 $self->{ar} 对 象中得到了路径,然后尝试去填充 tableactionargs 的内容。( @pi 表示 path info。)

这样,URL /brewery/view/1 将表示为对控制 brewery 表的类中的 view 方法( action )的调用,即 Maypole 所知道 的 BeerDB::Brewery ,并将参数 1 传递给 args 数组。 另外,URL 中的所有查询参数都保存到 $r->{query} 中,post 来的数据存 入 $r->{params} 中。

既然我们已经初步了解了请求要做什么事情,我们需要确保允许它可以这样做。首先,我们检查请求是“可用的(applicable)”—— 也就是说,我们允许通过 Web 调用那个方法。由于 view 方法定义时使用了 Perl 属 性 :Exported ,这就表示我们可以通过 Maypole URL 来调用它。 BeerDB::Brewery::view 继承自 Maypole::Model::Base::View , 它的定义如下:

sub view :Exported {}

没错,它没有任何代码。稍后我们将回到这一话题。

所以,对于任何一个我们想要对数据库执行的动作,我们可以简单地创建一个方法,赋与它 :Exported 属性, 然后它就可以通过 Maypole 进行访问。


清单 10. 创建一个新的动作
        package BeerDB::Beer;
        sub drink :Exported {
            my ($self, $r) = @_;
            # Implementation is left as an exercise
            # for the interested reader.
        }

这个完成以后,我们可以访问 /beer/drink/1,Maypole 将把 drink 作为动作, 然后 drink 方法就会被调用。当然,那得是在我们认证可以做这件事的前提下。

请求从您的浏览器出发再返回到您的浏览器的旅程的下一个阶段包括检查特定请求是否允许访问——您可能不希望每个人都 能饮用您的啤酒。Maypole 中的 authenticate 方法默认是许可的,不过可以在您自己的类中 被覆盖。当我们开发完整的应用程序时将看到如何来做到这一点。

既然请求已经通过了所有检查和障碍,它就可以交给模型类来处理了。默认的 process 动作作用于模型类,在调用这个动作之前要去查看 args 中的 第一个参数。使用 /beer/view/1,我们会有一个有点类似下面内容的请求对象:


清单 11. 填充来来自于 URL 的请求对象
        $r = {
            path => "/beer/view/1",
            table => "beer",
            model_class => "BeerDB::Beer",
            action => "view",
            args => [ 1 ],
            ...
        }

第一个参数用于指定要查看的表的行。Maypole 将在 beer 表中查找 ID 为 1 的行,并创建一个 BeerDB::Beer 对象来描述这一行。这将存放在 objects 位置。 process 还会设置 template 位置为动作的名称,于是我们的对象现在看起来像是这样:


清单 12. 将数据添加到请求
        $r = {
            path => "/beer/view/1",
            table => "beer",
            model_class => "BeerDB::Beer",
            action => "view",
            args => [ ],
            objects => [ bless { id => 1, name => ... }, "BeerDB::Beer" ],
            template => "view",
            ...
        }

如果只是要查看一个对象,我们不需要做任何更多的工作,只需要让对象可访问;实际的显示将由模板来处理。这就是为什么我们的 view 方法不需要有任何代码的原因,虽然它需要存在以使得它可以被设置为 :Exported

那么,模板如何起作用呢?一旦请求被 process ed 且动作方法 view 被调用后,它就会被提交到模板类;默认情况下,这个模板类是 Maypole::View::TT ,它使用了 Template Toolkit。它首先找到模板: 它首先到特定于这个类的目录中查找。如果有一个名为 /beer/view 的文件,首先就会被使用;然后是一个对通用于 所有类的定制模板;最后是工厂附带的 do-the-right-thing 模板:factory/view。

对象被传递到模板中,同时还有请求以及一个关于类的元数据的加载,这样您就可以创建一个完全通用的模板。不过, 对象被传递时也会使用一个别名:如果是 beer/view 的情况,对象也可以作为 beer 使用,所以您可以指明:


清单 13. 通过名字使用一个对象
        <h2> [% beer.name %] </h2>
        Style: [% beer.style %]

但是酿酒厂会被作为 brewery 传递进来:

        <h2> [% brewery.name %] </h2> 
        
        
Address: [% brewery.address %]

如果您要访问记录有所有酿酒厂的 brewery/list,这些都是可用的,当然是作为 breweries


清单 14. 模板中的多重对象
        <TABLE>
        [% FOR brewery = breweries %]
        <TR>
        <TD>[% brewery.name %] </TD>
        <TD>[% brewery.address %] </TD>
        <TD>[% brewery.url %] </TD>
        <TD>[% brewery.notes %] </TD>
        </TR>
        [% END %]
        </TABLE>

一旦请求通过了模板系统,输出就会返回到浏览器。这里是对这个过程的总结:


图 3. 流程图:用户请求
图 3. 流程图




回页首


Flox 基本原理

回到 Flox。对任何社区网络站点的核心来说,有三个概念:用户,用户之间的“友谊(friendship)”联系,以及现有用户对 没有在这个系统中的人的邀请。当然,我们可以添加社区、消息和很多其他内容,不过,我们将从其核心内容开始, 而且我们将使用数据库表对这些概念进行建模。

用户将由电子邮件地址来标识,有名字、档案、密码和状态。我们将使用电子邮件地址作为登录名。 为了让邀请过程更为简单,我们将有两组用户:那些状态为 real 的用户是已经接受了系统邀请 的真实用户,那些状态为 invited 的用户还没有接受邀请。


清单 15. Flox 的 schema
        CREATE TABLE user (
            id int not null auto_increment primary key,
            email varchar(255),
            first_name varchar(50),
            last_name varchar(50),
            profile text,
            password varchar(255),
            status ENUM("real", "invitee")
        );

现在,可以从用户表中的一位用户向另一位用户发出邀请,另外的一个列用于记录什么时候可以认为 另一方对此邀请不感兴趣而拒绝:


清单 16. 邀请表
        CREATE TABLE invitation (
            id char(32) not null primary key,
            issuer int,
            recipient int,
            expires date
        );

当一个邀请被接受后,接受者的状态就会改变为 real,然后这个邀请就会从表中删除。

对于友谊链接,我们将以实际上有损却比较简单的方式建模。链接发生于用户表中的一个条目与另一个条目之间, 和前面一样,它或者是“offered”,或者是“confirmed”。如果被接受,就会创建一个相应的“confirmed”链接。 和邀请相同,如果被拒绝,它只是从表中删除。

认证

我们将要面对的第一点就是认证和当前用户的概念。Maypole 中的认证有一个问题,默认情况下 是允许每个人访问所有内容的;我们可以通过在子类中覆盖 authenticate 方法并检查请求以确定是否应该让其通过来解决这一问题。

例如,我们将会需要让人们在没有登录的情况下访问,以接受或拒绝邀请,因为显然他们会由于还没有帐号而正要接受 一个邀请。所以,我们可以像这样开始我们的 authenticate


清单 17. 一个基本的认证方法
        sub authenticate {
            my ($self, $r) = @_;
            return OK if $r->table eq "invitation" and
                ($r->action eq "accept" or $r->action eq "reject");
            return;
        }

然后,我们希望找到一个 cookie 或一些认证标记来标识一位用户。让我们假定我们有一个 get_user 方法可以得到当前用户(如果有的话),并将其存入 $r->{user} 。如果有了它,我们将可以指明:


清单 18. 对用户的认证
        sub authenticate {
            my ($self, $r) = @_;
            return OK if $r->table eq "invitation" and
                ($r->action eq "accept" or $r->action eq "reject");
            $r->get_user;
            return OK if $r->{user};
            return;
        }

这几乎是我们想要的了,除了一点,它将会没理由地将用户定向到一个标准的 Apache 403 页面, 而不是给出登录的选项。我们希望让请求实际上继续处理,但是是将用户重定向到一个登录表单。我们 的做法是像这样:


清单 19. 重定向到一个登录表单
        sub authenticate {
            my ($self, $r) = @_;
            return OK if $r->table eq "invitation" and
                ($r->action eq "accept" or $r->action eq "reject");
            $r->get_user;
            return OK if $r->{user};
            $r->template("login");
            return OK;
        }

值得欣慰的是,如果其他什么内容设置了请求 /user/delete/2 的模板,Maypole 足够智能而不会进行操作,不会去首先删除用户 2,而是稍后弹出一个登录对话框以询问您是否被允许这样做。登录模板本身看起来像是这样:


清单 20. 登录表单模板
        [% INCLUDE header %]
        <H2> You need to log in </H2>
        <DIV class="login">
            [% IF login_error %]
               <FONT COLOR="#FF0000"> [% login_error %] </FONT>
            [% END %]
           <FORM ACTION="/[% request.path %]" METHOD="post">
                Email address: <INPUT TYPE="text" name="email"> <BR>
                Password: <INPUT TYPE="password" NAME="password"> <BR>
                <INPUT TYPE="submit">
           </FORM>
        </DIV>

注意我们如何使用 request.path 来将用户重定向回到他们先前 访问的页面;如果登录成功,则他们第二次发出这个请求时,认证方法将会成功并会顺利地继续 传递请求。

最后一点迷惑之处是 get_user 方法,它由 Maypole 插件模块 Maypole::Authentication::UserSessionCookie 提供;它会检查 会话 cookie 并使用 Apache::Session 来将一个会话 cookie 关联到 一个登录进来的用户名。它还负责将 cookie 分发给还没有 cookie 的客户机,前提是它们 也提供了正确的用户名和密码。

邀请

现在对模板来说用户的大部分浏览和编辑是相对微不足道的;困难的工作是设置邀请和朋友关联。我们 已经进行了很长时间,所以我们在这里只去看一个邀请;关联或多或少算是相同行为的一个子集。

当一个已经登录的用户点击 Invite a friend 按钮后,他们会被引导到 /invitation/create; 我们希望这是一个表单,让他们可以输入他们想要邀请的朋友的名字和电子邮件地址。这不需要任何另外的数据, 所以我们使用另一个 Maypole 技巧:如果我们请求一个类似于 create 的方法, 而它不是对 Flox::Invitation 类开放的,它不是马上放弃。实际上,它去检查是否有 有效的 create 模板,如果有,就将它现在所有的内容传递给那个模板。以上的流程图非常 简单,应该是类似于这样:


图 4. 流程图:不通过动作而调用模板
图 4. 流程图:不通过动作而调用模板

如果我们需要在请求表单上显示一些内容以指明当前用户,我们可以利用认证过程为我们 定义的 $r->{user} ,可以在模板中通过变量 [% request.user %] 访问它。

“invite user”表单提交到 /invitation/do_edit/——这是应用改变并将用户返回到适当页的标准“back-end” 记录创建和编辑方法。

现在我们需要发出邀请。表单给我们传递来 forenamesurnameemail 参数,于是我们使用奇妙的 CGI::Untaint 模块来确保他们是我们所期望的: forenamesurname 应该是无格式的字符串,而 email 应该是有效的电子邮件地址:


清单 21. “漂白(Untainting)”数据
        my $h = CGI::Untaint->new(%{$r->{params}});
        my (%errors, %ex);
        my %map = (email => "email",
                   forename => "printable",
                   surname => "printable");
        for (keys %map) {
            $ex{$_} = $h->extract("-as_$map{$_}" => $_)
                or $errors{$_} = $h->error;
        }

如果有任何错误,我们都会将表单返回给用户,同时还有用来突出问题的模板变量中的错误:


清单 22. 将错误返回给用户
        if (keys %errors) {
            $r->{template_args}{message} = "There was something wrong with that...";
            $r->{template_args}{errors} = \%errors;
            $r->{template} = "issue";
            return;
        }

我们需要确定被邀请的用户还不在系统中。如果他已经在,那么有两种可能:第一,他可能是一位“real”用户,在 这种情况下我们重定向浏览器以查看他的档案:


清单 23. 当一位被邀请的用户已经接受时
        my ($user) = Flox::User->search({ email => $ex{email} });
        if ($user) {
            if ($user->status eq "real") {
                $r->objects([ $user ]);
                $r->template("view");
                $r->model_class("Flox::User");
                $r->{template_args}{message} =
                    "That user already seems to exist on Flox.
                     Is this the one you meant?";

注意,我们正在对请求种类进行本质上的修改。我们的做法是将浏览器重定向到 /user/view/ 以及我们 找到的用户的 ID,不过这样做让我们可以向模板添加另外的消息。

另一种可能是,如果他们已经是一位被邀请的用户,但是还没有接受其他人的邀请。


清单 24. 当一位被邀请的用户还没有接受时
            } else {
                # Put it back to the form
                $r->{template_args}{message} = "That user has already been invited;
                                                please wait for them to accept";
                $r->{template} = "issue";
            }
            return;
        }

现在我们知道我们已经拥有了一位新用户,于是我们可以在用户表中创建一条新记录,状态为 invitee


清单 25. 创建一个新用户
        my $new_user = Flox::User->create({
            email => $ex{email},
            first_name => $ex{forename},
            last_name  => $ex{surname},
            status => "invitee"
        });

然后我们发送一封邮件,请他们来访问 /invitation/accept/$ID,在这里 ID 是邀请的 ID 的一个 相对安全的 32 位 cookie;我们还在邀请表中创建了行:


清单 26. 添加新邀请对象
        Flox::Invitation->create({
            id => $cookie
            issuer => $r->{user},
            recipient => $new_user,
            expires => Time::Piece->new(time + LIFETIME)->datetime
        });

最后,我们让模板返回到 view,让类返回到 Flox::User , 以使得我们可以将用户重定向去再次浏览他自己的档案:


清单 27. 让用户返回到他的档案
        $r->objects([ $r->{user} ]);
        $r->template("view");
        $r->model_class("Flox::User");
        $r->{template_args}{message} = "Invitation sent to $ex{email}.";

当用户访问 URL 来接受邀请时是最后一个有趣之处。在为确保邀请有效而进行一些基本的检查之后,然后我们指明:


清单 28. 删除邀请并重定向用户
            my $recipient = $invite->recipient;
            my $issuer = $invite->issuer;
            $invite->delete;
            $recipient->status("real");
            $r->{user} = $recipient;
            $r->model_class("Flox::User");
            $r->objects([ $recipient ]);
            $r->template("firsttime_edit");

这样会删除邀请,因为我们不再需要它了。现在我们将接受者的状态设置为 real,并 设置好内容以使我们可以为新用户显示 firsttime_edit 模板。这会允许他定制他的档案和密码,然后 开始构造联系!





回页首


结束语

本文略述了使用 Maypole 进行 MVC 开发的一些原理,并具体地展示了 Maypole 如何帮助您使用 MVC 模型 在 Perl 中快速开发 CRUD 应用程序以及更复杂的 Web 应用程序。Maypole 是设计用来让您可以使用最少量的 代码实现复杂的企业数据库 Web 应用程序——它的目标是为您完成所有结构上的工作,并允许您编写需要的代码和模板 来封装您希望要做的事情。我相信,Web 编程应该是那样的。



参考资料



关于作者

Simon Cozens 是一位 Perl 程序员,也是一名作家。他发布了 90 多个 Perl 模块。 他的书有 Beginning Perl 和即将出版的 Advanced Perl Programming 第二版。 他还为 O'Reilly 和 Associates 维护着 Perl.com 站点。 当不在计算机面前时,他喜欢创造音乐,玩日本游戏 Go,或者在当地的教堂中传教。可以通过 simon@simon-cozens.org 与 Simon 联系。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款