使用 Agavi 和 Doctrine 添加表单和数据库支持

红薯 发布于 2009/09/08 21:44
阅读 1K+
收藏 1

在本系列的第 1 部分中,我介绍了 Agavi 并解释了一些特性,这些特性让 Agavi 适合用于构建可扩展、遵从标准的 Web 应用程序。通过使用样例应用程序 Web Automobiles Sales Platform (WASP),我逐步向您展示了创建新的 Agavi 项目的基础知识,帮助您理解 Agavi 推荐的文件系统布局,并熟悉 Agavi 的命令行构建脚本。我还介绍了所有 Agavi 应用程序的基础组件 —— 操作(Actions)、视图(Views)和路由(Routes)—— 并展示了一些内置的 Agavi 输入验证器。

尽 管 Agavi 过去常常用于提供静态内容,但是它也能很好地处理更复杂的情况。在第 2 部分中,您将检验 Agavi 处理复杂内容的能力 —— 在接下来几个小节中,您将学习如何接收、验证和处理来自 Web 表单的输入,并将 Agavi 应用程序连接到 MySQL 数据库。

使用 Agavi 创建表单

首先,这里快速浏览了 WASP 应用程序的索引页面(图 1):


图 1. WASP 应用程序的索引页面
WASP 应用程序的索引页面屏幕截图

您在上一篇文章中已经准备好代码,以处理连接到静态内容的两个链接。我们继续前进,开始处理 “联系我们” 链接。顾名思义,这个链接指向一个联系表单,通过该表单可以联系到汽车交易商。实现这个功能的总体过程与在前一篇文章中构建 StaticContentAction 的过程相似。

首先启动 Agavi 构建脚本并输入下列值:

shell> agavi action-wizard
...
Module name: Default
Action name: Contact
Space-separated list of views to create for Contact [Success]: Input Error Success

 

这 将创建一个新的 ContactAction 和其他 3 个视图。您已经熟悉 ContactSuccessView 和 ContactErrorView 这两个标准的视图,它们根据操作的成功或失败而显示。第三个视图 ContactInputView 是全新的;它是用户看到的第一个视图,并且显示将接受用户输入的 Web 表单。

为 $WASP_ROOT/app/routing.xml 文件中的 ContactAction 添加一个新的路由,如 清单 1 所示:


清单 1. Default/ContactAction 路由定义

				        
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
<ae:configuration>
<routes>
...
<!-- action for contact form "/contact" -->
<route name="contact" pattern="^/contact$" module="Default"
action="Contact" />

</routes>
</ae:configuration>
</ae:configurations>

 

完成之后,更新 $WASP_ROOT/app/templates/Master.php 上的主模板,并将主菜单中的 “联系我们” 链接超链接到上面的路由(清单 2):


清单 2. 主模板

				        
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
...
<div id="menu">
<ul>
<li><a href="<?php echo $ro->gen('index'); ?>">
Home</a></li>
<li><a href="#">For Sale</a></li>
<li><a href="<?php echo $ro->gen('content',
array('page' => 'other-services')); ?>">Other
Services</a></li>
<li><a href="<?php echo $ro->gen('content',
array('page' => 'about-us')); ?>">About Us</a></li>
<li><a href="<?php echo $ro->gen('contact'); ?>">
Contact Us</a></li>

</ul>
</div>
...

 

接下来,在 $WASP_ROOT/app/modules/Default/actions/ContactAction.class.php 中的 ContactAction 类文件内部,通过定义 getDefaultViewName()executeRead() 方法指定 ContactInputView 在默认情况下对所有 GET 请求显示(清单 3):


清单 3. Default/ContactAction 定义

				        
<?php
class Default_ContactAction extends WASPDefaultBaseAction
{
public function getDefaultViewName()
{
return 'Input';
}

public function executeRead(AgaviRequestDataHolder $rd)
{
return 'Input';
}
}
?>

 

在 $WASP_ROOT/app/modules/Default/templates/ContactInput.php 上使用一个简单的 Web 表单更新相应的模板文件,如 清单 4 所示。注意,您可以从本文附带的代码归档中找到该表单的 CSS 规则(见 下载)。


清单 4. Default/ContactInput 模板

				        
<h3>Contact Us</h3>
<form action="<?php echo $ro->gen(null); ?>" method="post">
<label for="name" class="required">Name:</label>
<input id="name" type="text" name="name" />
<p/>
<label for="email" class="required">Email address:</label>
<input id="email" type="text" name="email" />
<p/>
<label for="message" class="required">Message body:</label>
<textarea id="message" name="message" style="width:300px; height:200px">
</textarea>
<p/>
<input type="submit" name="submit" class="submit" value="Send Message" />
</form>

 

现在,当您在浏览器中重新打开 WASP 索引页面并单击 “联系我们” 时,您将看到如 图 2 所示的 Web 表单:


图 2. WASP 联系表单
WASP 联系表单的屏幕截图

现在才走完一半路程。您还需要定义当用户填写表单字段并提交数据时将发生什么。

验证表单输入

当用 户提交表单时,客户端浏览器通过 POST 事务向服务器发送输入数据。回想前一篇文章,您应该记得 Agavi 使用一个极其严格的输入过滤器:将自动拒绝没有在验证规则中显式指定的任何 GET 和 POST 变量。因此,当您定义如何处理表单输入时,第一步通常是为表单输入字段定义验证器。

因为 ContactInput 表单包含 3 个字段,所以必须为每个字段定义验证器。清单 5 给出了 $WASP_ROOT/app/modules/Default/validate/Contact.xml 中的验证规则:


清单 5. Default/ContactAction 验证器

				        
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
parent="%core.module_dir%/Default/config/validators.xml"
>
<ae:configuration>

<validators method="write">
<validator class="string">
<arguments>
<argument>name</argument>
</arguments>
<errors>
<error for="required">ERROR: Name is missing</error>
<error>ERROR: Name is invalid</error>
</errors>
<ae:parameters>
<ae:parameter name="required">true</ae:parameter>
</ae:parameters>
</validator>

<validator class="email">
<arguments>
<argument>email</argument>
</arguments>
<errors>
<error for="required">ERROR: Email address is missing</error>
<error>ERROR: Email address is invalid</error>
</errors>
<ae:parameters>
<ae:parameter name="required">true</ae:parameter>
</ae:parameters>
</validator>

<validator class="string">
<arguments>
<argument>message</argument>
</arguments>
<errors>
<error for="required">ERROR: Message body is missing</error>
<error>ERROR: Message body is invalid</error>
</errors>
<ae:parameters>
<ae:parameter name="required">true</ae:parameter>
</ae:parameters>
</validator>
</validators>

</ae:configuration>
</ae:configurations>

 

清单 5 中的配置为这 3 个表单字段设置了 2 个验证器:AgaviStringValidators 是 namemessage 字段的验证器,而 AgaviEmailValidator 是 email 字段的验证器。尤其要注意每个验证器附带的 <error> 代码块;它们定义不同失败场景时的错误消息,并且是关键的组件(随后您将看到)。

假设输入数据通过了验证,Agavi 将在定义如何处理 POST 输入的 ContactAction 中查找 executeWrite() 方法。在这个例子中,仅需将输入格式化为一条电子邮件消息,并发送到指定的电子邮件地址。因此,第二步是向完成该任务的 ContactAction 添加一个 executeWrite(),如 清单 6 所示:


清单 6. 包含新的 executeWrite() 方法的 Default/ContactAction 定义

				        
<?php
class Default_ContactAction extends WASPDefaultBaseAction
{
...

public function executeWrite(AgaviRequestDataHolder $rd)
{
$name = $rd->getParameter('name');
$email = $rd->getParameter('email');
$message = $rd->getParameter('message');
$subject = 'Contact form submission';
$to = 'webmaster@wasp.example';
if (@mail($to, $subject, $message, "From: $name <$email>\r\n")) {
return 'Success';
} else {
return 'Error';
}
}
}
?>

 

Agavi 将根据电子邮件消息是否被发送显示 ContactSuccessView 或 ContactErrorView。在这些视图模板中,不需要非常完美的消息:仅需使用一个简单的消息表明表单提交结果。下面分别显示了 $WASP_ROOT/app/modules/Default/templates/ContactSuccess.php 和 $WASP_ROOT/app/modules/Default/templates/ContactError.php:

Your message was successfully sent!

 

There was an error. Your message could not be sent. Please try again later.

 

现在先告一段落!试用它,看看还有什么想法。

使用表单填充过滤器

如 果您的洞察力比较好,就会发现一些东西:根据当前的设置,当发送电子邮件消息出现错误或一个或多个表单字段验证失败时,应用程序都显示相同的行为(显示 ContactErrorView)。在现实中,您很可能希望应用程序对这两种情况作出不同的响应。具体而言,如果用户输入验证失败,通常希望应用程序重 新显示相同的表单,并突出显示无效的字段,让用户能够纠正错误并重新提交表单。

Agavi 自带的一个工具非常神奇,让您能够轻松实现以上功能:AgaviFormPopulationFilter。要查看它的实际运行效果,请回到 ContactAction(清单 3)并向其添加 清单 7 中的代码:


清单 7. 激活了 FPF 的 Default/ContactAction 定义

				        
<?php
class Default_ContactAction extends WASPDefaultBaseAction
{
...

public function handleError(AgaviRequestDataHolder $rd)
{
return 'Input';
}
}
?>

 

当表单验证失败时,Agavi 的默认行为是显示相应的错误视图。向使用另一个视图的名称的操作添加 handleError() 方法,以覆盖该行为。以上代码告诉 Agavi 在表单字段验证失败时重新显示 ContactInputView。

现在,在 Web 浏览器中返回到 “联系我们” 表单,并尝试向其填充无效数据或留空,然后提交。Agavi 应该显示 ContactInputView(而不是 ContactErrorView),并在无效字段下面显示错误消息,如 图 3 所示:


图 3. WASP 联系表单,突出显示输入错误
WASP 联系表单,突出显示输入错误

这就是 AgaviFormPopulationFilter 的特长。当一个或多个表单输入字段验证失败时,这个工具将自动为无效字段显示错误消息。此外,它还将有效的输入值放回到表单中,因此用户不需重新输入。错误消息是从验证器获取的(还记得我在前面小节中提到的 <error> 代码块吗?)。

在 Agavi 应用程序中,AgaviFormPopulationFilter 是默认启用的。您可以调整 $WASP_ROOT/app/config/global_filters.xml 中的参数关闭它或改变它的行为。现在给出了 AgaviFormPopulationFilter 的默认配置:

<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
xmlns="http://agavi.org/agavi/config/parts/filters/1.0">

<ae:configuration context="web">
<filters>
<filter name="FormPopulationFilter" class="AgaviFormPopulationFilter">

<!-- only run for request method "write" (=POST on web)
by default (can be changed at runtime, of course) -->
<!-- if you omit this, it will never run -->
<ae:parameter name="methods">
<ae:parameter>write</ae:parameter>
</ae:parameter>

<!-- only run for output type "html" (so it doesn't break on,
say, JSON data) -->
<!-- if you omit this, it will run for all output types -->
<ae:parameter name="output_types">
<ae:parameter>html</ae:parameter>
</ae:parameter>

<!-- error message insertion rules -->
<!-- they are run in sequence; once the first one matched,
execution stops -->
<!--
errors that belong to more than one field (e.g. date validator)
can be handled using "multi_field_error_messages"
"normal" errors are handled through "field_error_messages"
errors that yield no match and those that have no corresponding
field are inserted using rules defined in "error_messages".
-->

<!-- for all field error messages. -->
<ae:parameter name="field_error_messages">
<!-- ${htmlnsPrefix} is either empty (for HTML) or something like
"html:" for XHTML documents with xmlns="..." notation. Always use this,
makes your code more bullet proof. XPath needs the namespaces when the document
is namespaced -->

<!-- all input fields that are not checkboxes or radios, and
all textareas -->
<ae:parameter name="self::${htmlnsPrefix}input[not(@type='checkbox'
or @type='radio')] | self::${htmlnsPrefix}textarea">
<!-- if this rule matched, then the node found by the rule is our
starting point for inserting the error message(s). -->

<!-- can be any of "before", "after" or "child" (to insert as prev,
next sibling or last child) -->
<ae:parameter name="location">after</ae:parameter>
<!-- a container groups all errors for one element. ${errorMessages}
is a string containing all errors (see below) -->
<ae:parameter name="container">
<![CDATA[<div class="errors">${errorMessages}</div>]]>
</ae:parameter>
<!-- this defines the HTML for each individual error message;
those are then put into the container. ${errorMessage} is the
error message string -->
<ae:parameter name="markup">
<![CDATA[<p class="error">${errorMessage}</p>]]>
</ae:parameter>
</ae:parameter>

<!-- all other inputs - note how we select the parent element and
insert ourselves as last child of it -->
<ae:parameter name="parent::*">
<ae:parameter name="location">child</ae:parameter>
<ae:parameter name="container">
<![CDATA[<div class="errors">${errorMessages}</div>]]>
</ae:parameter>
<ae:parameter name="markup">
<![CDATA[<p class="error">${errorMessage}</p>]]>
</ae:parameter>
</ae:parameter>
</ae:parameter>

<!--
<ae:parameter name="multi_field_error_messages">
</ae:parameter>
-->

<!-- everything that did not match any of the rules above,
or errors that do not belong to a field -->
<ae:parameter name="error_messages">
<!-- insert before the element -->
<!-- that can be an input, or a form, if the error does not belong
to a field or didn't match anywhere else -->
<ae:parameter name="self::*">
<ae:parameter name="location">before</ae:parameter>
<!-- no container here! we just insert paragraph elements -->
<ae:parameter name="markup">
<![CDATA[<p class="error">${errorMessage}</p>]]>
</ae:parameter>
</ae:parameter>
</ae:parameter>
</filter>

...
</filters>
</ae:configuration>
</ae:configurations>

 

因为 AgaviFormPopulationFilter 负责处理输入验证错误,所以您还可以将 $WASP_ROOT/app/modules/Default/templates/ContactError.php 更新为:

There was an error sending your message. Please try again later.

集成 Agavi 和 Doctrine

现在您已经知道 Agavi 中的表单是如何工作的,接下来我们学习更复杂的东西。在前一个例子中,操作仅将用户输入转换成电子邮件消息,并使用 PHP 的 mail() 函数发送它。然而,另一个常见的需求是将用户输入读或写到数据存储中(文件或数据库表)。因为 Agavi 包含针对大部分常见数据库系统的连接器,所以这并不难实现。

拥 有数据库之后,您还需要使用模型与之交互。模型可以对数据进行管理、操作和计算。尽管您可以编写自己的模型,但是更简便的方法是使用 Doctrine 或 Propel 等对象关系映射器自动生成它们。在这个系列中,您将使用 Doctrine,它因其灵活性和易于使用而非常流行。

为了进行展示,我们将创建一些新的操作,允许卖方在 WASP 应用程序中列出待售的二手车,同时允许买方查看二手车列表并向卖方发送购买请求。提交的列表将存储在一个 MySQL 数据库中,并且在经过审核之后可以在 WASP 站点上查看和搜索它们。

在编写执行以上任务的操作代码之前,您需要让 Doctrine 能够与 Agavi 交互。以下步骤显示了这个过程:

步骤 1:创建应用程序数据库

首先,进入 MySQL 命令提示符,然后为应用程序创建一个新的空数据库:

shell>mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 59
Server version: 5.1.28-rc-community MySQL Community Server (GPL)
Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> CREATE DATABASE wasp;
Query OK, 1 row affected (0.00 sec)

 

完成之后,定义一个仅能访问该数据库的用户帐号。这是良好的安全实践,因为这样能确保即使这个用户帐户受到威胁,其他数据库也是安全的。

mysql> GRANT ALL ON wasp.* TO wasp@localhost IDENTIFIED BY 'wasp';
Query OK, 1 row affected (0.00 sec)

 

现在,创建一些表来存储汽车列表,如下所示:

mysql> USE wasp;
Database changed

mysql> CREATE TABLE IF NOT EXISTS `listing` (
-> `RecordID` int(10) unsigned NOT NULL AUTO_INCREMENT,
-> `RecordDate` date NOT NULL,
-> `OwnerName` varchar(255) NOT NULL,
-> `OwnerTel` varchar(25) DEFAULT NULL,
-> `OwnerEmail` text NOT NULL,
-> `VehicleManufacturerID` int(11) NOT NULL,
-> `VehicleModel` varchar(255) NOT NULL,
-> `VehicleYear` year(4) NOT NULL,
-> `VehicleColor` varchar(30) NOT NULL,
-> `VehicleMileage` int(11) NOT NULL,
-> `VehicleIsFirstOwned` tinyint(1) NOT NULL,
-> `VehicleAccessoryBit` int(11) NOT NULL,
-> `VehicleIsCertified` tinyint(1) NOT NULL,
-> `VehicleCertificationDate` date DEFAULT NULL,
-> `VehicleSalePriceMin` int(11) NOT NULL,
-> `VehicleSalePriceMax` int(11) NOT NULL,
-> `VehicleSalePriceIsNegotiable` tinyint(1) NOT NULL DEFAULT '0',
-> `Note` text,
-> `OwnerCity` varchar(255) NOT NULL,
-> `OwnerCountryID` int(11) NOT NULL,
-> `DisplayStatus` tinyint(1) NOT NULL,
-> `DisplayUntilDate` date DEFAULT NULL,
-> PRIMARY KEY (`RecordID`)
-> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.13 sec)

 

这是应用程序的主数据存储。它包含待售的汽车的详细信息,包括汽车的品牌、制造年份、颜色、已行驶公里数、位置和销售价格。每个汽车记录都包含两个管理字段 DisplayStatusDisplayUntilDate,它们控制是否在 WASP 站点上显示汽车列表,以及显示多长时间。

注意,这个表在 VehicleManufacturerIDOwnerCountryID 字段中使用了外键引用,这两个字段分别指定汽车的制造商和车主当前的国籍。下面继续为这些键引用创建源表:

mysql> CREATE TABLE IF NOT EXISTS `manufacturer` (
-> `ManufacturerID` int(11) NOT NULL AUTO_INCREMENT,
-> `ManufacturerName` varchar(255) NOT NULL,
-> PRIMARY KEY (`ManufacturerID`)
-> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.05 sec)

mysql> CREATE TABLE IF NOT EXISTS `country` (
-> `CountryID` int(11) NOT NULL AUTO_INCREMENT,
-> `CountryName` varchar(255) NOT NULL,
-> PRIMARY KEY (`CountryID`)
-> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.05 sec)

 

完成之后,使用示例记录填充这些表:

mysql> INSERT INTO `manufacturer` (`ManufacturerID`, `ManufacturerName`) 
VALUES(1, 'Ferrari');

Query OK, 1 row affected (0.06 sec)

mysql> INSERT INTO `manufacturer` (`ManufacturerID`, `ManufacturerName`)
VALUES(2, 'Porsche');

Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `manufacturer` (`ManufacturerID`, `ManufacturerName`)
VALUES(3, 'BMW');

Query OK, 1 row affected (0.00 sec)


mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
VALUES(1, 'United States');

Query OK, 1 row affected (0.05 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
VALUES(2, 'United Kingdom');

Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
VALUES(3, 'India');

Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
VALUES(4, 'Singapore');

Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
VALUES(5, 'Germany');

Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
VALUES(6, 'France');

Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
VALUES(7, 'Italy');

Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
VALUES(8, 'Spain');

Query OK, 1 row affected (0.02 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`)
VALUES(9, 'Hungary');

Query OK, 1 row affected (0.00 sec)

 

步骤 2:下载 Doctrine 库并添加到应用程序中

准备好数据库之后,下一步就是下载 Doctrine 库并将其添加到应用程序中。要安装 Doctrine,请访问它的主页,下载并解压缩源代码压缩文件,然后将压缩文件的 lib/ 目录中的内容复制到 $WASP_ROOT/libs/doctrine/。在 参考资料 小节可以找到 Doctrine Web 站点的链接。本文使用 Doctrine V 1.1。

shell> cd /usr/local/apache/htdocs/wasp/libs
shell> mkdir doctrine
shell> cp -R /tmp/Doctrine-1.1.1/lib/* doctrine/

 

步骤 3:创建 Doctrine 模型并添加到应用程序中

下一步是为应用程序生成 Doctrine 模型。Doctrine 可以通过它的 generateModelsFromDb() 方法自动为您完成该任务。首先,为输出模型创建一个临时目录:

shell> cd /tmp
shell> mkdir models

 

然后,创建一个简单的 PHP 脚本,它将使用 Doctrine 为先前创建的数据库对象自动生成模型。清单 8 是一个示例 PHP 脚本 /tmp/doctrine-gen.php(要了解详细信息,请查看 Doctrine 手册,参考资料 提供了相关链接):


清单 8. 生成 Doctrine 模型的 PHP 脚本

				        
<?php
// include main Doctrine class file
// change this per your system
include '/usr/local/apache/htdocs/wasp/libs/doctrine/Doctrine.php';
spl_autoload_register(array('Doctrine', 'autoload'));

// create Doctrine manager
$manager = Doctrine_Manager::getInstance();

// create database connection
$conn = Doctrine_Manager::connection('mysql://wasp:wasp@localhost/wasp', 'doctrine');

// auto-generate models
Doctrine::generateModelsFromDb('models', array('doctrine'),
array('generateTableClasses' => true));
?>

 

使用 PHP 解释器从命令行执行这个脚本:

shell> php doctrine-gen.php

 

Doctrine 开始为数据库表生成模型。在脚本执行完成之后,查看 /tmp/models/ 和 /tmp/models/generated/,您应该看到如 图 4 所示的内容:


图 4. 自动生成的模型
自动生成的 Doctrine 模型的屏幕截图

图 4 中显示的文件是表示数据库对象的类。/tmp/models/generated/ 目录下的类是由 Doctrine 生成的基类,而在 /tmp/models 目录下的类是子类,您可以使用它们给基类添加更多功能。查看 /tmp/models/generated/BaseListing.php,您将看到一个 Doctrine 对象,它的属性与 MySQL 数据库表中的字段对应:

<?php

/**
* BaseListing
*
* This class has been auto-generated by the Doctrine ORM Framework
*
* @property integer $RecordID
* @property date $RecordDate
* @property string $OwnerName
* @property string $OwnerTel
* @property string $OwnerEmail
* @property integer $VehicleManufacturerID
* @property string $VehicleModel
* @property integer $VehicleYear
* @property string $VehicleColor
* @property integer $VehicleMileage
* @property integer $VehicleIsFirstOwned
* @property integer $VehicleAccessoryBit
* @property integer $VehicleIsCertified
* @property date $VehicleCertificationDate
* @property integer $VehicleSalePriceMin
* @property integer $VehicleSalePriceMax
* @property integer $VehicleSalePriceIsNegotiable
* @property string $Note
* @property string $OwnerCity
* @property integer $OwnerCountryID
* @property integer $DisplayStatus
* @property date $DisplayUntilDate
*
* @package ##PACKAGE##
* @subpackage ##SUBPACKAGE##
* @author ##NAME## <##EMAIL##>
* @version SVN: $Id: Builder.php 5441 2009-01-30 22:58:43Z jwage $
*/
abstract class BaseListing extends Doctrine_Record
{
public function setTableDefinition()
{
$this->setTableName('listing');
$this->hasColumn('RecordID', 'integer', 4,
array('type' => 'integer', 'length' => 4, 'unsigned' => 1,
'primary' => true, 'autoincrement' => true));
$this->hasColumn('RecordDate', 'date', null,
array('type' => 'date', 'notnull' => true));
$this->hasColumn('OwnerName', 'string', 255,
array('type' => 'string', 'length' => 255, 'notnull' => true));
$this->hasColumn('OwnerTel', 'string', 25,
array('type' => 'string', 'length' => 25));
$this->hasColumn('OwnerEmail', 'string', null,
array('type' => 'string', 'notnull' => true));
$this->hasColumn('VehicleManufacturerID', 'integer', 4,
array('type' => 'integer', 'length' => 4, 'notnull' => true));
$this->hasColumn('VehicleModel', 'string', 255,
array('type' => 'string', 'length' => 255, 'notnull' => true));
$this->hasColumn('VehicleYear', 'integer', null,
array('type' => 'integer', 'notnull' => true));
$this->hasColumn('VehicleColor', 'string', 30,
array('type' => 'string', 'length' => 30, 'notnull' => true));
$this->hasColumn('VehicleMileage', 'integer', 4,
array('type' => 'integer', 'length' => 4, 'notnull' => true));
$this->hasColumn('VehicleIsFirstOwned', 'integer', 1,
array('type' => 'integer', 'length' => 1, 'notnull' => true));
$this->hasColumn('VehicleAccessoryBit', 'integer', 4,
array('type' => 'integer', 'length' => 4, 'notnull' => true));
$this->hasColumn('VehicleIsCertified', 'integer', 1,
array('type' => 'integer', 'length' => 1, 'notnull' => true));
$this->hasColumn('VehicleCertificationDate', 'date', null,
array('type' => 'date'));
$this->hasColumn('VehicleSalePriceMin', 'integer', 4,
array('type' => 'integer', 'length' => 4, 'notnull' => true));
$this->hasColumn('VehicleSalePriceMax', 'integer', 4,
array('type' => 'integer', 'length' => 4, 'notnull' => true));
$this->hasColumn('VehicleSalePriceIsNegotiable', 'integer', 1,
array('type' => 'integer', 'length' => 1, 'default' => '0', 'notnull' => true));
$this->hasColumn('Note', 'string', null, array('type' => 'string'));
$this->hasColumn('OwnerCity', 'string', 255,
array('type' => 'string', 'length' => 255, 'notnull' => true));
$this->hasColumn('OwnerCountryID', 'integer', 4,
array('type' => 'integer', 'length' => 4, 'notnull' => true));
$this->hasColumn('DisplayStatus', 'integer', 1,
array('type' => 'integer', 'length' => 1, 'notnull' => true));
$this->hasColumn('DisplayUntilDate', 'date', null,
array('type' => 'date'));
}

}
?>

 

这个 BaseListing 类被 /tmp/models/Listing.php 中的 Listing 类扩展,扩展后的类为:

<?php

/**
* Listing
*
* This class has been auto-generated by the Doctrine ORM Framework
*
* @package ##PACKAGE##
* @subpackage ##SUBPACKAGE##
* @author ##NAME## <##EMAIL##>
* @version SVN: $Id: Builder.php 5441 2009-01-30 22:58:43Z jwage $
*/
class Listing extends BaseListing
{

}
?>

 

目前这个子类是空的,它是添加定制属性和方法的适当位置。子类还是定义模型之间的关系的地方 —— 这个任务必须手动实现。为了进行演示,使用以下代码更新空的 Listing 类(清单 9):


清单 9. 扩展后的 Listing 模型

				        
<?php
class Listing extends BaseListing
{
public function setUp()
{
$this->hasOne('Manufacturer', array(
'local' => 'VehicleManufacturerID',
'foreign' => 'ManufacturerID'
)
);
$this->hasOne('Country', array(
'local' => 'OwnerCountryID',
'foreign' => 'CountryID'
)
);
}
}
?>

 

清单 9 中的代码指定每个 Listing 有一个 Manufacturer 和一个 Country。

您还应该在 Manufacturer 和 Country 模型中指定反向关系,如下所示(清单 1011):


清单 10. 扩展后的 Country 模型

				        
<?php
class Country extends BaseCountry
{
public function setUp()
{
$this->hasMany('Listing', array(
'local' => 'CountryID',
'foreign' => 'OwnerCountryID'
)
);
}
}
?>



清单 11. 扩展后的 Manufacturer 模型

				        
<?php
class Manufacturer extends BaseManufacturer
{
public function setUp()
{
$this->hasMany('Listing', array(
'local' => 'ManufacturerID',
'foreign' => 'VehicleManufacturerID'
)
);
}
}
?>

 

参考资料 小节提供一个 Doctrine 手册链接,这个手册非常详尽地解释了 Doctrine 模型以及模型之间的关系。

为了将这些生成的模型添加到 Agavi 应用程序,需要将它们复制到应用程序根目录下的 $WASP_ROOT/app/lib/doctrine 目录中:

shell> cd /usr/local/apache/htdocs/wasp/app/lib
shell> mkdir doctrine
shell> cp /tmp/models/* doctrine/
shell> cp /tmp/models/generated/* doctrine/

 

这个步骤结束之后,Doctrine 库就被安装到 $WASP_ROOT/app/libs/doctrine,而模型被安装到 $WASP_ROOT/app/lib/doctrine。

步骤 4:配置 Agavi 以使用 Doctrine

最后的步骤是让 Agavi 知道 Doctrine 模型的存在,以及配置应用程序以对数据库查询使用 Agavi 的 Doctrine 适配器。这涉及到许多小步骤:

为了自动加载主 Doctrine 类,需要编辑 $WASP_ROOT/app/config/autoload.xml 并为其添加条目,如 清单 12 所示:


清单 12. 配置 Agavi 以自动加载 Doctrine

				        
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns="http://agavi.org/agavi/config/parts/autoload/1.0"
xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
parent="%core.system_config_dir%/autoload.xml">
<ae:configuration>

...
<autoload name="Doctrine">
%core.app_dir%/../libs/doctrine/Doctrine.php
</autoload>

</ae:configuration>
</ae:configurations>

 

在 $WASP_ROOT/app/config/settings.xml 上编辑主应用程序配置文件,并在 Agavi 应用程序中启用数据库支持,如 清单 13 所示:


清单 13. 配置 Agavi 启用数据库支持

				        
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
xmlns="http://agavi.org/agavi/config/parts/settings/1.0">
<ae:configuration>
...
<settings>
<setting name="app_name">WASP</setting>

<setting name="available">true</setting>
<setting name="debug">false</setting>

<setting name="use_database">true</setting>
<setting name="use_logging">false</setting>
<setting name="use_security">true</setting>
<setting name="use_translation">false</setting>
</settings>

</ae:configuration>

...
</ae:configurations>

 

在 $WASP_ROOT/app/config/databases.xml 上编辑应用程序的配置文件,并将 Doctrine 配置为默认的数据库适配器。为 MySQL 数据库设置 DSN,并且将路径配置为在步骤 3 中安装 Doctrine 模型的位置($WASP_ROOT/app/lib/doctrine)。这确保 Agavi 将在必要时自动加载模型。


清单 14. 根据 Doctrine DSN 配置 Agavi

				        
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
xmlns="http://agavi.org/agavi/config/parts/databases/1.0">

<ae:configuration>
<databases default="doctrine">

<database name="doctrine" class="AgaviDoctrineDatabase">
<ae:parameter name="dsn">mysql://wasp:wasp@localhost/wasp</ae:parameter>
<ae:parameter name="load_models">%core.lib_dir%/doctrine</ae:parameter>
</database>

</databases>
</ae:configuration>

</ae:configurations>

获取数据库记录

现在,Agavi、Doctrine 和 MySQL 之间的通信已经畅通,接下来需要编写一个 ViewAction,以从 MySQL 数据库获取并显示汽车列表。首先,使用一些示例记录填充 listing 表;这方便您在操作的初始开发阶段对其进行测试:

mysql> INSERT INTO listing (RecordID, RecordDate, OwnerName, OwnerTel, 
OwnerEmail, VehicleManufacturerID, VehicleModel, VehicleYear, VehicleColor,
VehicleMileage, VehicleIsFirstOwned, VehicleAccessoryBit, VehicleIsCertified,
VehicleCertificationDate, VehicleSalePriceMin, VehicleSalePriceMax,
VehicleSalePriceIsNegotiable, Note, OwnerCity, OwnerCountryID, DisplayStatus,
DisplayUntilDate) VALUES (1, '2009-06-08', 'John Doe', '00123456789876',
'john@wasp.example.com', 2, 'Boxster', 2005, 'Yellow', 15457, 1, 23, 1,
'2008-01-01', 35000, 40000, 1, 'Well cared for. In good shape, no scratches
or bumps. Has prepaid annual service contract till 2009.', 'London', 2,
1, '2009-10-15');

Query OK, 1 row affected (0.05 sec)

mysql> INSERT INTO listing (RecordID, RecordDate, OwnerName, OwnerTel,
OwnerEmail, VehicleManufacturerID, VehicleModel, VehicleYear, VehicleColor,
VehicleMileage, VehicleIsFirstOwned, VehicleAccessoryBit, VehicleIsCertified,
VehicleCertificationDate, VehicleSalePriceMin, VehicleSalePriceMax,
VehicleSalePriceIsNegotiable, Note, OwnerCity, OwnerCountryID, DisplayStatus,
DisplayUntilDate) VALUES (2, '2009-06-08', 'Jane Doe', '00987654321236',
'jane@wasp.example.com', 2, '911 Turbo', 2003, 'Black', 17890, 1, 23, 1,
'2008-06-19', 17000, 25000, 1, '', 'Cambridge', 2, 1, '2009-10-15');

Query OK, 1 row affected (0.00 sec)

 

现在,通过以下步骤给 WASP 应用程序添加必要的功能:

步骤 1:创建占位符类

汽车列表可以看作是 WASP 应用程序的一个功能独立的组件,因此与这个组件相关的操作和视图应该放在另一个独立的模块中。启动 Agavi 构建脚本并创建一个新的模型,如下所示:

shell> agavi module-create
...
Module name: Listing

 

完成之后,添加一个新的 DisplayAction 来处理列表的显示。为了将操作与 DisplayErrorView 和 DisplaySuccessView 视图连接起来,那么需要在得到提示时提供下列值:

shell> agavi action-wizard
...
Module name: Listing
Action name: Display
Space-separated list of views to create for Display [Success]: Error Success

 

现在,Agavi 将生成必要的类文件并将它们放到正确的位置中。

步骤 2:定义路由

添加一个引用最新创建的操作的路由,如 清单 15 所示:


清单 15. Listing/DisplayAction 路由定义

				        
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
<ae:configuration>
<routes>
...
<!-- action for listing pages "/listing" -->
<route name="listing" pattern="^/listing" module="Listing">
<route name=".display" pattern="^/display/(id:\d+)$" action="Display" />
</route>

</routes>
</ae:configuration>
</ae:configurations>

 

这个路由定义希望 id 变量被包含为 URL GET 请求的一部分,并通过在路由定义中使用捕捉组来表明。这个变量表示汽车列表的唯一标识符,与 MySQL 数据库中的 listing.RecordID 主键字段对应。记住,您必须将这个变量添加到 DisplayAction 的验证器,这样它才能通过 Agavi 的输入验证过滤器。

这也是您见到的第一个嵌套路由例子。在嵌套路由定义中,内部路由继承与外部路由匹配的模式,然后可以进一步修改该模式。在实现 CRUD 功能时,这个特性提供了极大的便利,其中 URL 具有相同的基础部分和不同的后缀,如下所示:

/object/display/23
/object/add
/object/edit/23
/object/delete/23

 

使用以上的路由定义时,包含 /listing 模式的 URL 首先与外部路由进行匹配。然后,Agavi 检查剩余的模式,并根据模式包含的内容决定哪个子路由最匹配,并将请求指向该路由的操作。当然,以上的定义仅包含一个子路由,但不要着急,随后将添加更多 的子路由。

步骤 3:定义验证规则

因为仅有一个输入变量被传递到 DisplayAction,所有验证非常简单 —— 只需使用一个 AgaviNumberValidator(清单 16):


清单 16. Listing/DisplayAction 验证器

				        
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
parent="%core.module_dir%/Listing/config/validators.xml"
>
<ae:configuration>

<validators method="read">
<validator class="number">
<arguments>
<argument>id</argument>
</arguments>
<ae:parameters>
<ae:parameter name="type">int</ae:parameter>
<ae:parameter name="required">true</ae:parameter>
<ae:parameter name="min">1</ae:parameter>
</ae:parameters>
</validator>
</validators>

</ae:configuration>
</ae:configurations>

 

步骤 4:编写操作代码

处理了路由和验证之后,下一个步骤是为 DisplayAction 指定视图。因为 DisplayAction 将只处理 GET 请求,所以您必须指定一个与生成的视图同名的 executeRead() 方法。清单 17 显示了编写出的操作:


清单 17. Listing/DisplayAction 定义

				        
<?php
class Listing_DisplayAction extends WASPListingBaseAction
{

public function getDefaultViewName()
{
return 'Success';
}

public function executeRead(AgaviRequestDataHolder $rd)
{
return 'Success';
}
}
?>

 

步骤 5:编写视图代码

现在,您将进入这个小节的核心部分:设置 DisplaySuccessView 以显示汽车列表。清单 18 显示了编写出的视图:


清单 18. Listing/DisplaySuccessView 定义

				        
<?php
class Listing_DisplaySuccessView extends WASPListingBaseView
{
public function executeHtml(AgaviRequestDataHolder $rd)
{
$this->setupHtml($rd);
$this->setAttribute('_title', 'View Listing');
$id = $rd->getParameter('id');
$q = Doctrine_Query::create()
->from('Listing l')
->leftJoin('l.Manufacturer m')
->leftJoin('l.Country c')
->where('l.RecordID = ?', $id);
$result = $q->fetchArray();
if (count($result) == 1) {
$this->setAttribute('listing', $result[0]);
return 'Success';
} else {
return $this->createForwardContainer(
AgaviConfig::get('actions.error404_module'),
AgaviConfig::get('actions.error404_action'));
}
}
}
?>

 

executeHtml() 方法的前面几行设置视图模板,并获取输入变量 $_GET['id'] 的值。然后这个值被插入到一个 Doctrine 查询中,该查询试图在数据库中查找匹配列表(要详细了解 Doctrine 查询的细节,请查看 参考资料 小节提供的对应手册页链接)。如果查询仅返回一条记录,该结果将作为联合数组分配到模板变量 $t['listing']。如果没有匹配项,或找到多个匹配项,视图将自动跳转到应用程序的 Error404 操作。

这次,通过设置 DisplayErrorView(清单 19)指定输入验证失败时发生的行为不失为一个好主意。


清单 19. Listing/DisplayErrorView 定义

				        
<?php

class Listing_DisplayErrorView extends WASPListingBaseView
{
public function executeHtml(AgaviRequestDataHolder $rd)
{
$this->setupHtml($rd);
return $this->createForwardContainer(
AgaviConfig::get('actions.error404_module'),
AgaviConfig::get('actions.error404_action'));
}
}

?>

 

非常简单 —— 将再次跳转到默认的 Error404 操作。

最后,设置 DisplaySuccess 模板实际显示从数据库获得的信息(清单 20):


清单 20. Listing/DisplaySuccess 模板

				        
<h3>FOR SALE: <?php printf('%d %s %s (%s)',
$t['listing']['VehicleYear'], $t['listing']['Manufacturer']['ManufacturerName'],
ucwords(strtolower($t['listing']['VehicleModel'])),
ucwords(strtolower($t['listing']['VehicleColor']))); ?></h3>

<div id="container">
<div id="specs">
<table cellspacing="5">
<tr>
<td class="key">Listing ID: </td>
<td class="value"><?php echo $t['listing']['RecordID']; ?></td>
</tr>
<tr>
<td class="key">Year of manufacture: </td>
<td class="value"><?php echo
$t['listing']['VehicleYear']; ?></td>
</tr>
<tr>
<td class="key">Color: </td>
<td class="value"><?php echo
$t['listing']['VehicleColor']; ?></td>
</tr>
<tr>
<td class="key">Mileage: </td>
<td class="value"><?php echo
$t['listing']['VehicleMileage']; ?></td>
</tr>
<tr>
<td class="key">Ownership: </td>
<td class="value"><?php echo
($t['listing']['VehicleIsFirstOwned'] == 1) ? 'First owner' :
'Multiple owners'; ?></td>
</tr>
<tr>
<td class="key">Certification: </td>
<td class="value"><?php echo
($t['listing']['VehicleIsCertified'] == 1) ? 'Certified, as of '
. date('d M Y', strtotime($t['listing']['VehicleCertificationDate']))
: 'Not certified'; ?></td>
</tr>
<tr>
<td class="key">Accessories: </td>
<td class="value">
<?php echo ($t['listing']['VehicleAccessoryBit'] == 0) ?
'None <br/>' : null; ?>
<?php echo ($t['listing']['VehicleAccessoryBit'] & 1) ?
'Power steering <br/>' : null; ?>
<?php echo ($t['listing']['VehicleAccessoryBit'] & 2) ?
'Power windows <br/>' : null; ?>
<?php echo ($t['listing']['VehicleAccessoryBit'] & 4) ?
'Audio system <br/>' : null; ?>
<?php echo ($t['listing']['VehicleAccessoryBit'] & 8) ?
'Video system <br/>' : null; ?>
<?php echo ($t['listing']['VehicleAccessoryBit'] & 16) ?
'Keyless entry system <br/>' : null; ?>
<?php echo ($t['listing']['VehicleAccessoryBit'] & 32) ?
'GPS <br/>' : null; ?>
<?php echo ($t['listing']['VehicleAccessoryBit'] & 64) ?
'Alloy wheels <br/>' : null; ?>
</td>
</tr>
<tr>
<td class="key">Location: </td>
<td class="value"><?php echo
$t['listing']['OwnerCity']; ?>,
<?php echo $t['listing']['Country']['CountryName']; ?></td>
</tr>
<tr>
<td class="key">Sale price: </td>
<td class="value"> $<?php echo
$t['listing']['VehicleSalePriceMin']; ?> - $<?php echo
$t['listing']['VehicleSalePriceMax']; ?> <?php echo
($t['listing']['VehicleSalePriceIsNegotiable'] == 1) ? '(negotiable)'
: null; ?></td>
</tr>
<tr>
<td class="key">Description: </td>
<td class="value"><?php echo
$t['listing']['Note']; ?></td>
</tr>
</table>
</div>
</div>

 

这个模板将数据库记录的各种元素作为 $t['listing'] 联合数组的键读取(还记得在 DisplaySuccessView 中对 setAttribute() 的调用吗?),并将信息显示为整齐的 HTML 表。要查看实际效果,请打开浏览器并尝试访问先前添加到 MySQL 数据库的两个示例记录:http://wasp.localhost/listing/display/1 或 http://wasp.localhost/listing/display/2。您应该看到如 图 5 所示的内容。


图 5. 汽车列表
样例汽车列表屏幕截图

注意,如果您尝试给 URL 传递无效或缺失的 ID,Agavi 将跳转到默认的 “Page not found” 错误页面,如 图 6 所示。这就是 DisplayErrorView 的真实效果。


图 6. 无效列表 ID 引起的错误页面
无效列表 ID 引起的错误页面

本系列第 2 部分到此结束。在本文中,我们进一步探索了 Agavi 世界,解释了如何接受和验证通过 Web 表单提交的用户输入,并了解了 Agavi 的表单填充过滤器。此外,我还展示了如何在 Agavi 应用程序中访问数据库,即创建 MySQL 数据库,使用 Doctrine ORM 生成模型,以及使用这些模型连接到数据库并执行查询。

现在,样例应用程序比以前智能了一些:它拥有一个联系表单、知道如何发送电子邮件,并且能够从 MySQL 数据库获取汽车列表。不过,还没有一个让用户直接向数据库添加列表的界面。这个功能和其他一些功能将在本系列的第 3 部分中讨论。

下载 小节下载本文实现的所有代码。我建议您下载并开始试用它,尝试向它添加新东西。我敢保证您能从中获得更多的知识。祝您实验愉快,下次见!

加载中
返回顶部
顶部