 | 级别: 中级 Vikram Vaswani, 创始人, Melonfire
2009 年 9 月 21 日 在第 4 部分中为您的 Agavi 示例程序实现一个简单的搜索引擎并添加对多种输出类型(XML、RSS 或 SOAP)的支持。这个 5 部分系列是为对开源、灵活并且可伸缩的 Agavi 框架感兴趣的 PHP 开发人员编写的。
简介
在本系列的第 3 部分中,您经历了构建基于 Web 的应用程序时经常遇到的一个任务:实现一个允许管理员通过 Web 界面执行 CRUD 操作的管理模块。您还探索了 Agavi 的安全模型,构建了用于验证用户的登录系统,以保护对应用程序资源的访问。
现在继续 Agavi 学习,为这个 WASP(Web 汽车销售平台)示例应用程序添加更多功能。您将实现一个搜索引擎,允许用户直接搜索数据库,获取匹配特定条件的清单。而且,Agavi 为开发人员提供了一个复杂的框架,允许他们为应用程序轻松添加对多种输出类型(XML、RSS 或 SOAP)的支持。本文将学习如何通过最少的编程支持从搜索引擎返回 XML 编码的结果。
处理搜索标准
 |
常用缩略词
- API:应用程序编程接口
- CRUD:创建、阅读、更新和删除
- CVS:并发版本系统
- DOM:文档对象模型
- HTML:超文本标记语言
- HTTP:超文本传输协议
- MVC:模型-视图-控制器
- PDF:可移植文档格式
- RSS:真正简易的聚合
- URL:统一资源定位器
- XML:可扩展标记语言
|
|
到目前为止,这个 WASP 应用程序可以接受经销商提交的车辆清单并将其存储在数据库中以便批准。本系列第 3 部分开发的管理模块允许管理员审查并批准这些提交的清单,以便在 Web 站点上显示它们。管理员还可以定义每个清单在网站上显示的时间长度。
要使潜在买家更容易地找到满足他们需要的车辆,现在最好向应用程序添加一个搜索功能。这个搜索界面将接收来自买家的特定条件,搜索批准的清单以寻找满足条件的汽车,最后显示结果以进一步检查。
首先,使用 Agavi 构建脚本向 Listing 模块添加一个新的 SearchAction:
shell> agavi action-wizard
Module name: Listing
Action name: Search
Space-separated list of views to create for Search [Success]: Error Success
|
并且更新应用程序的路由表,为这个 Action 添加一个新的路由,如 清单 1 所示:
清单 1. Listing/SearchAction 路由定义
<?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=".create" pattern="^/create$" action="Create" />
<route name=".display" pattern="^/display/(id:\d+)$" action="Display" />
<route name=".search" pattern="^/search$" action="Search" />
</route>
...
</routes>
</ae:configuration>
</ae:configurations>
|
SearchAction 的默认行为是显示一个搜索表单,以便用户输入各种搜索条件。完成这个任务的典型方法是对表单使用一个 SearchInputView,对结果使用一个 SearchSuccessView。但是您已经了解了这种技术……并且我讨厌乏味的重复!因此,我将进行一点小小的变化,将表单和它的结果合并到一个 SearchSuccessView 中,这也将是 SearchAction 的默认视图(见 清单 2)。
清单 2. Listing/SearchAction 定义
<?php
class Listing_SearchAction extends WASPListingBaseAction
{
public function getDefaultViewName()
{
return 'Success';
}
}
?>
|
现在看看搜索表单本身。清单 3 是 SearchSuccessView 的代码。注意,这个视图中使用了 AgaviFormPopulationFilter,以确保搜索表单使用用户输入的条件自动重新填充。
清单 3. Listing/SearchSuccessView 定义
<?php
class Listing_SearchSuccessView extends WASPListingBaseView
{
public function executeHtml(AgaviRequestDataHolder $rd)
{
$this->setupHtml($rd);
$this->getContext()->getRequest()->setAttribute('populate',
array('fsearch' => true), 'org.agavi.filter.FormPopulationFilter');
}
}
?>
|
清单 4 显示了对应的 SearchSuccess 模板:
清单 4. Listing/SearchSuccess 模板
<h3>Search Listings</h3>
<form id="fsearch" action="<?php echo $ro->gen('listing.search'); ?>"
method="get">
<fieldset>
<legend>Criteria</legend>
Color:
<input id="color" type="text" name="color" style="width:120px" />
Year:
<input id="year" type="text" name="year" size="4" style="width:100px" />
Price:
<input id="price" type="text" name="price" style="width:140px" />
<button id="search" type="submit"/>
</fieldset>
</form>
<h3>Search Results</h3>
<?php if (count($t['records']) == 0): ?>
No records available
<?php else: ?>
<?php $x = 1; ?>
<?php foreach ($t['records'] as $record): ?>
<div>
<strong><?php echo $x; ?>.
<a href="<?php echo $ro->gen('listing.display',
array('id' => $record['RecordID'])); ?>"><?php printf('%d %s %s (%s)',
$record['VehicleYear'], $record['Manufacturer']['ManufacturerName'],
ucwords(strtolower($record['VehicleModel'])),
ucwords(strtolower($record['VehicleColor']))); ?></a>
</strong>
<br/>
Mileage: <?php echo $record['VehicleMileage']; ?>
<br/>
Sale price: $<?php echo $record['VehicleSalePriceMin']; ?> -
$<?php echo $record['VehicleSalePriceMax']; ?>
<?php echo ($record['VehicleSalePriceIsNegotiable'] == 1) ?
'(negotiable)' : null; ?>
<br/>
Location: <?php echo $record['OwnerCity']; ?>,
<?php echo $record['Country']['CountryName']; ?>
<br/>
Submitted: <?php echo date('d M Y', strtotime($record['RecordDate'])); ?>
<p/>
</div>
<?php $x++; ?>
<?php endforeach; ?>
<?php endif; ?>
<p/>
<strong>
<a href="<?php echo $ro->gen('listing.create'); ?>">
Add a new listing</a>
</strong>
|
在 清单 4 中,您看到了模板的两个部分。第一个部分包含一个搜索表单,用户可以用该表单输入他们的选购条件。这些值被提交和处理之后,第二个部分将显示在数据库中发现的匹配记录。这里还有一个添加新清单的链接。
为简便起见,我将表单限制为 3 个条件:颜色、价格和制造年份。毫无疑问,这些输入值在进入 SearchAction 之前必须接受验证。清单 5 是必要的验证规则:
清单 5. Listing/SearchAction 验证器
<?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>
<validator class="string">
<arguments>
<argument>color</argument>
</arguments>
<errors>
<error>ERROR: Vehicle color is invalid</error>
</errors>
<ae:parameters>
<ae:parameter name="required">false</ae:parameter>
</ae:parameters>
</validator>
<validator class="number">
<arguments>
<argument>year</argument>
</arguments>
<errors>
<error for="type">ERROR: Vehicle year of manufacture is invalid
</error>
<error for="min">ERROR: Vehicle year of manufacture is before 1900
</error>
<error for="max">ERROR: Vehicle year of manufacture is after 2020
</error>
</errors>
<ae:parameters>
<ae:parameter name="type">int</ae:parameter>
<ae:parameter name="min">1901</ae:parameter>
<ae:parameter name="max">2020</ae:parameter>
<ae:parameter name="required">false</ae:parameter>
</ae:parameters>
</validator>
<validator class="number">
<arguments>
<argument>price</argument>
</arguments>
<errors>
<error>ERROR: Vehicle price is invalid</error>
</errors>
<ae:parameters>
<ae:parameter name="type">int</ae:parameter>
<ae:parameter name="min">0</ae:parameter>
<ae:parameter name="required">false</ae:parameter>
</ae:parameters>
</validator>
</validators>
</ae:configuration>
</ae:configurations>
|
您应该已经从 清单 4 注意到一点:与以前不同的是,这个搜索表单使用 GET 方法而不是 POST 方法。通常,搜索表单应该使用 GET 作为请求方法,因为它们并不更改服务器上的任何数据。相比之下,第 第 1 部分、第 2 部分 和 第 3 部分 部分中的表单修改或添加了服务器上的数据,因此它们使用 POST 方法更合适。使用 GET 方法有可能获取总是指向最近搜索结果的 URL — 很快我将利用这一点!
处理搜索结果
由于这个搜索表单使用 GET 方法提交输入,您需要使用一个额外的 executeRead() 方法来更新 清单 2 中的 SearchAction,executeRead() 方法读取这个输入并将它包含进一个搜索查询中。根据用户提交的搜索参数,使用迭代方式构建查询,并将查询限制到那些可以在当前日期内有效显示的已批准清单。清单 6 是必要的代码。
清单 6. 修改后的 Listing/SearchAction 定义
<?php
class Listing_SearchAction extends WASPListingBaseAction
{
public function getDefaultViewName()
{
return 'Success';
}
public function executeRead(AgaviRequestDataHolder $rd)
{
try {
// create base query
$q = Doctrine_Query::create()
->from('Listing l')
->leftJoin('l.Manufacturer m')
->leftJoin('l.Country c')
->addWhere('l.DisplayStatus = 1')
->addWhere('l.DisplayUntilDate >= CURDATE()');
// add criteria
if ($rd->getParameter('color')) {
$q->addWhere('l.VehicleColor LIKE ?', sprintf('%%%s%%',
$rd->getParameter('color')));
}
if ($rd->getParameter('year')) {
$q->addWhere('l.VehicleYear = ?', $rd->getParameter('year'));
}
if ($rd->getParameter('price')) {
$q->addWhere('? BETWEEN l.VehicleSalePriceMin AND l.VehicleSalePriceMax',
$rd->getParameter('price'));
}
$q->orderBy('l.RecordDate DESC');
// execute query and assign results to template variable
$results = $q->fetchArray();
$this->setAttribute('records', $results);
return 'Success';
} catch (Exception $e) {
$this->setAttribute('error', $e->getMessage());
return 'Error';
}
}
}
?>
|
重要的是要知道,如果没有使用任何标准调用,清单 6 中的查询将返回所有已批准和有效的清单,并按日期排序。
错误都通过 SearchErrorView 处理,无论是关于验证的还是关于查询执行的。错误处理使用 第 3 部分:使用 Agavi 添加验证和管理功能 介绍的技术 — 由 SearchErrorView 确定是显示 SearchSuccess 模板还是显示 SearchError 模板。清单 7 是 SearchErrorView 的代码,清单 8 是对应的 SearchError 模板的代码。
清单 7. Listing/SearchErrorView 定义
<?php
class Listing_SearchErrorView extends WASPListingBaseView
{
public function executeHtml(AgaviRequestDataHolder $rd)
{
$this->setupHtml($rd);
// if validation errors, render input template
if ($this->container->getValidationManager()->getReport()->count()) {
$this->getLayer('content')->setTemplate('SearchSuccess');
$this->getContext()->getRequest()->setAttribute('populate',
array('fsearch' => true), 'org.agavi.filter.FormPopulationFilter');
}
}
}
?>
|
清单 8. Listing/SearchError 模板
<h3>Search Listings</h3>
There was an error processing your submission. Please try again later.
<br/><br/>
<code style="color: red">
<?php echo $t['error']; ?>
</code>
|
从 清单 7 可以清楚地看出,AgaviFormPopulationFilter 被显式附加到搜索表单上,方法是在对 $this->getContext()->getRequest()->setAttribute 的调用中命名表单的 id 属性('fsearch')。这确保验证器生成的错误消息正确地显示在表单中。由于您以前没有这样做过,您可能想知道为何需要这样做。根据 Agavi 开发团队的解释,这个步骤在这个特别的示例中之所以必要的原因是,AgaviFormPopulationFilter 通常使用表单的操作 URL 匹配当前 URL,从而确定需要运行的表单。但是,由于这个示例中的搜索表单使用 GET 方法提交数据,当前 URL 通常会包含一列其他搜索参数。这样,AgaviFormPopulationFilter 就不能够匹配表单的操作 URL。要在调用 AgaviFormPopulationFilter 时解决这个问题,应明确指定表单的 id 属性。
要查看搜索功能的实际操作,浏览到 http://wasp.localhost/listings/search 并在搜索表单中输入您的搜索条件。提交表单后,页面刷新以显示匹配您的条件的结果清单。图 1 是您看到的画面的一个示例。
图 1. 带有搜索结果的 WASP 搜索表单
提交无效输入后,AgaviFormPopulationFilter 将返回一条错误消息,如 图 2 所示。
图 2. 带有无效输入的 WASP 搜索表单
至此,这个 WASP 应用程序拥有了一个有效的搜索引擎。现在只剩下一件小事:在应用程序的主菜单中设置一个链接以访问最新的车辆清单。如前所述(见 清单 6),如果您调用 SearchAction 而没有输入任何标准,就会生成一个包含所有已批准和有效的清单(按日期排序)的结果集。因此,您只需编辑主模板文件 $WASP_ROOT/app/templates/Master.php 并在主菜单中的 “For Sale” 链接上使用 Agavi 的路由生成器,如 清单 9 所示:
清单 9. 修改后的主模板
<!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">
...
<!-- begin header -->
<div id="header">
<div id="logo">
<img src="/images/logo.jpg" />
</div>
<div id="menu">
<ul>
<li><a href="<?php echo $ro->gen('index'); ?>">
Home</a></li>
<li><a href="<?php echo $ro->gen('listing.search'); ?>">
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>
</div>
<!-- end header -->
...
</html>
|
理解输出类型
现代应用程序往往需要以几种不同的格式显示相同的数据。例如,相同的结果集可能显示为 HTML 表、CSV 文件、XML 文档或 RSS 提要。Agavi 可以轻松实现这一功能,因为它允许每个视图支持多种输出类型。这样,您可以以不同的方式显示同一个 Action 的结果而不用复制任何 Action 代码。
每个 Agavi 应用程序都默认预配置为使用 HTML 输出类型。检查这个 WASP 应用程序的输出类型配置文件 $WASP_ROOT/app/config/output_types.xml,您将看到这个配置(见 清单 10)。
清单 10. 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/output_types/1.0">
<ae:configuration>
<output_types default="html">
<output_type name="html">
<renderers default="php">
<renderer name="php" class="AgaviPhpRenderer">
<ae:parameter name="assigns">
<ae:parameter name="routing">ro</ae:parameter>
<ae:parameter name="request">rq</ae:parameter>
<ae:parameter name="controller">ct</ae:parameter>
<ae:parameter name="user">us</ae:parameter>
<ae:parameter name="translation_manager">tm</ae:parameter>
<ae:parameter name="request_data">rd</ae:parameter>
</ae:parameter>
<ae:parameter name="default_extension">.php</ae:parameter>
<!-- this changes the name of the variable with all template attributes
from the default $template to $t -->
<ae:parameter name="var_name">t</ae:parameter>
</renderer>
</renderers>
<layouts default="standard">
<!-- standard layout with a content and a decorator layer -->
<layout name="standard">
<!-- content layer without further params. this means the standard
template is used, i.e. the one with the same name as the current view -->
<layer name="content" />
<!-- decorator layer with the HTML skeleton, navigation etc; set to
a specific template here -->
<layer name="decorator">
<ae:parameter name="directory">%core.template_dir%
</ae:parameter>
<ae:parameter name="template">Master</ae:parameter>
</layer>
</layout>
<layout name="admin">
<layer name="content" />
<layer name="decorator">
<ae:parameter name="directory">%core.template_dir%
</ae:parameter>
<ae:parameter name="template">AdminMaster</ae:parameter>
</layer>
</layout>
<!-- another example layout that has an intermediate wrapper layer in
between content and decorator -->
<!-- it also shows how to use slots etc -->
<layout name="wrapped">
<!-- content layer without further params. this means the standard
template is used, i.e. the one with the same name as the current view -->
<layer name="content" />
<layer name="wrapper">
<!-- use CurrentView.wrapper.php instead of CurrentView.php as
the template for this one -->
<ae:parameter name="extension">.wrapper.php</ae:parameter>
</layer>
<!-- decorator layer with the HTML skeleton, navigation etc; set to
a specific template here -->
<layer name="decorator">
<ae:parameter name="directory">%core.template_dir%
</ae:parameter>
<ae:parameter name="template">Master</ae:parameter>
<!-- an example for a slot -->
<slot name="nav" module="Default" action="Widgets.Navigation" />
</layer>
</layout>
<!-- special layout for slots that only has a content layer to prevent
the obvious infinite loop that would otherwise occur if the decorator layer
has slots assigned in the layout; this is loaded automatically by
ProjectBaseView::setupHtml() in case the current container is run as a slot -->
<layout name="simple">
<layer name="content" />
</layout>
</layouts>
<ae:parameter name="http_headers">
<ae:parameter name="Content-Type">text/html; charset=UTF-8
</ae:parameter>
</ae:parameter>
</output_type>
</output_types>
</ae:configuration>
<ae:configuration environment="production.*">
<output_types default="html">
<!-- use a different exception template in production envs -->
<!-- others are defined in settings.xml -->
<output_type name="html"
exception_template="%core.template_dir%/exceptions/web-html.php" />
</output_types>
</ae:configuration>
</ae:configurations>
|
按照以下三个步骤在您的 Agavi 应用程序中引入一种新的输出类型:
步骤 1:定义输出类型以及任何标头、渲染器和布局
和路由一样,输出类型配置也用 XML 表示。每个输出类型定义都有一个惟一名称。一个输出类型也可以有选择地包含多个 HTTP 标头(以便该输出类型在请求时被发送到请求客户端)、一个渲染器(除基本的 PHP 外,Agavi 还包括 Smarty、eZ 和 TAL 渲染器),以及一个或多个布局。
步骤 2:使用新的输出类型链接路由
您可以使用一个 output_type 属性标记每个路由定义,该属性指定某个特定的路由将使用的输出类型。下面是一个示例:
<route name="show" pattern="^/show$" module="Default" action="Show" output_type="xml" />
|
或者,您也可以建立一个 “全部捕获(catch-all)” 路由,使其根据请求 URL 自动设置输出类型。这意味着以 .rss 结尾的 URL 请求被自动设置为使用 RSS 输出类型,以 .pdf 结尾的 URL 请求被自动设置为使用 PDF 输出类型,以此类推。清单 12 展示了这种方法的一个示例。
步骤 3:在视图中添加新的输出类型支持
为满足对一个 View 的请求,Agavi 执行这个 View 的 executeXXX() 方法,其中 XXX 引用请求的输出类型。因此,Agavi 将运行 executeHtml() 以满足对 HTML 资源的请求,运行 executeRss() 以满足对 RSS 资源的请求,运行 executeXml() 以满足对 XML 资源的请求,以此类推。这也是目前为止您创建的每个 View 类都有一个 executeHtml() 方法的原因。
以 XML 格式显示搜索结果
为了更好地理解在 Agavi 中如何处理输出类型,向 WASP 搜索引擎添加 XML 支持。首先,更新输出类型配置文件 $WASP_ROOT/app/config/output_types.xml 并定义一个新的 XML 输出类型(见 清单 11)。
清单 11. XML 输出类型定义
<?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/output_types/1.0">
<ae:configuration>
<output_types default="html">
<output_type name="html">
...
</output_type>
<output_type name="xml">
<ae:parameter name="http_headers">
<ae:parameter name="Content-Type">text/xml; charset=UTF-8
</ae:parameter>
</ae:parameter>
</output_type>
</output_types>
</ae:configuration>
</ae:configurations>
|
然后,更新这个应用程序的路由表并为新的 XML 输出类型建立一个 “全部捕获” 路由(见 清单 12)。
清单 12. XML 输出类型的路由定义
<?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>
<!-- handler for .xml requests -->
<route name="xml" pattern=".xml$" cut="true" stop="false" output_type="xml" />
...
</routes>
</ae:configuration>
</ae:configurations>
|
这个路由放置在路由表的顶端,匹配所有以 .xml 后缀结尾的请求,并设置这些请求以使用 XML 输出类型。但这个路由还有以下功能:
cut 属性表明是否在继续之前从请求 URL 删除匹配的子字符串片段。在本例中,该属性设置 true,以便在一个匹配发生后删除请求 URL 中的 .xml 后缀。
- 路由定义中的
stop 属性表明在第一个匹配之后是否继续路由处理。在本例中,该属性设置为 false,以确保该请求继续检查列表,直到请求 URL 的剩余部分被匹配并调用适当的 Action。
这个配置的最终效果是:如果 Agavi 收到一个请求,比如 http://wasp.localhost/listings/display/1.xml,它将检查路由表并立即发现一个匹配 —— 一个顶级 “全部捕获” 路由。Agavi 从请求 URL 删除 .xml 后缀,然后将请求的输出类型设置为 XML。随后针对列出的路由继续检查请求的剩余部分 http://wasp.localhost/listings/display/1,直到发现一个与 listing.display 匹配的路由,并调用 Listing/DisplayAction。DisplayAction 完成后,Agavi 将在选中的 DisplayView 中查找 executeXml() 方法,执行该方法,并将结果返回客户端。
下一步是更新 SearchSuccessView 并添加一个 executeXml() 方法,该方法读取 SearchAction 生成的结果集并将其转换为格式良好的 XML。清单 13 是相关代码:
清单 13. Listing/SearchSuccessView 定义
<?php
class Listing_SearchSuccessView extends WASPListingBaseView
{
public function executeHtml(AgaviRequestDataHolder $rd)
{
$this->setupHtml($rd);
$this->getContext()->getRequest()->setAttribute('populate',
array('fsearch' => true), 'org.agavi.filter.FormPopulationFilter');
}
public function executeXml(AgaviRequestDataHolder $rd)
{
// get records
$records = $this->getAttribute('records');
// create document
$dom = new DOMDocument('1.0', 'utf-8');
// create root element
$root = $dom->createElementNS('http://www.melonfire.com/agavi-wasp',
'wasp:results');
$dom->appendChild($root);
// import to SimpleXML for easier manipulation
$xml = simplexml_import_dom($dom);
// add result count
$xml->addChild('count', count($records));
// add results
foreach ($records as $record) {
$listing = $xml->addChild('result');
$listing->addChild('id', $record['RecordID']);
$listing->addChild('submissionDate', $record['RecordDate']);
$listing->addChild('manufacturer',
$record['Manufacturer']['ManufacturerName']);
$listing->addChild('model', ucwords(strtolower($record['VehicleModel'])));
$listing->addChild('year', $record['VehicleYear']);
$listing->addChild('color', $record['VehicleColor']);
$listing->addChild('year', $record['VehicleYear']);
$listing->addChild('mileage', $record['VehicleMileage']);
$price = $listing->addChild('price');
$price->addChild('min', $record['VehicleSalePriceMin']);
$price->addChild('max', $record['VehicleSalePriceMax']);
$location = $listing->addChild('location');
$location->addChild('city', $record['OwnerCity']);
$location->addChild('country', $record['Country']['CountryName']);
}
// return output
return $xml->asXML();
}
}
?>
|
清单 13 中的 executeXml() 方法使用 PHP 中的 DOM 扩展来生成一个 XML 文档的主体结构。然后该方法将这个主体结构转换为一个 SimpleXML 对象并填充该主体结构,填充方法是迭代 SearchAction 返回的结果集并将每条记录表示为一个 XML 节点集合。最终的 XML 文档通过调用 SimpleXML 中的 asXML() 方法返回客户端。
清单 14 是 清单 13 生成的输出的 XML 示例:
清单 14. 一个 XML 编码的结果集
<?xml version="1.0" encoding="utf-8"?>
<wasp:results xmlns:wasp="http://www.melonfire.com/agavi-wasp">
<wasp:count>2</wasp:count>
<wasp:result>
<wasp:id>1</wasp:id>
<wasp:submissionDate>2009-07-03</wasp:submissionDate>
<wasp:manufacturer>Porsche</wasp:manufacturer>
<wasp:model>Boxster</wasp:model>
<wasp:year>2005</wasp:year>
<wasp:color>Yellow</wasp:color>
<wasp:year>2005</wasp:year>
<wasp:mileage>15457</wasp:mileage>
<wasp:price>
<wasp:min>35000</wasp:min>
<wasp:max>40000</wasp:max>
</wasp:price>
<wasp:location>
<wasp:city>London</wasp:city>
<wasp:country>United Kingdom</wasp:country>
</wasp:location>
</wasp:result>
<wasp:result>
<wasp:id>7</wasp:id>
<wasp:submissionDate>2009-06-07</wasp:submissionDate>
<wasp:manufacturer>Ferrari</wasp:manufacturer>
<wasp:model>612 Scaglietti</wasp:model>
<wasp:year>2003</wasp:year>
<wasp:color>Yellow</wasp:color>
<wasp:year>2003</wasp:year>
<wasp:mileage>10974</wasp:mileage>
<wasp:price>
<wasp:min>125000</wasp:min>
<wasp:max>200000</wasp:max>
</wasp:price>
<wasp:location>
<wasp:city>London</wasp:city>
<wasp:country>United Kingdom</wasp:country>
</wasp:location>
</wasp:result>
</wasp:results>
|
以类似的方式更新 SearchErrorView,以便在输入验证或查询执行中出现错误时返回一个 XML 编码的错误消息。由于生成一个 XML 编码的错误消息是您将多次执行的常用任务,最好将这个代码添加到 WASPBaseView 类中(如 清单 15 所示),以便在所有扩展这个 View 的子类中使用。
清单 15. Listing/WASPListingBaseView 定义
<?php
/**
* The base view from which all Listing module views inherit.
*/
class WASPListingBaseView extends WASPBaseView
{
// set values from selection lists
function setInputViewAttributes() {
$q = Doctrine_Query::create()
->from('Country c');
$this->setAttribute('countries', $q->fetchArray());
$q = Doctrine_Query::create()
->from('Manufacturer m');
$this->setAttribute('manufacturers', $q->fetchArray());
}
// generate XML-encoded error message
function getErrorXml($message) {
$dom = new DOMDocument('1.0');
$root = $dom->createElementNS('http://www.melonfire.com/agavi-wasp',
'wasp:error');
$dom->appendChild($root);
$xml = simplexml_import_dom($dom);
$xml->addChild('message', $message);
return $xml->asXML();
}
}
?>
|
通过神奇的继承关系,现在您可以在 SearchErrorView(参见 清单 16)中调用这个方法来处理针对 XML 输出类型的验证和应用程序错误。
清单 16. Listing/SearchErrorView 定义
<?php
class Listing_SearchErrorView extends WASPListingBaseView
{
public function executeHtml(AgaviRequestDataHolder $rd)
{
$this->setupHtml($rd);
// if validation errors, render input template
if ($this->container->getValidationManager()->getReport()->count()) {
$this->getLayer('content')->setTemplate('SearchSuccess');
$this->getContext()->getRequest()->setAttribute('populate',
array('fsearch' => true), 'org.agavi.filter.FormPopulationFilter');
}
}
public function executeXml(AgaviRequestDataHolder $rd)
{
if ($this->container->getValidationManager()->getReport()->count()) {
return $this->getErrorXml('Validation error');
} else {
return $this->getErrorXml('Application error');
}
}
}
?>
|
要查看其实际操作情况,浏览到 http://wasp.localhost/listing/search.xml,您将看到类似于 图 3 所示的内容:
图 3. XML 格式的搜索结果
您可以将搜索标准附加到请求 URL,您的 XML 输出将被适当过滤,就像 HTML 版本一样。例如,浏览到 http://wasp.localhost/listing/search.xml?color=yellow&year=2005,您将看到通过那些条件过滤的结果集,如 图 4 所示:
图 4. XML 格式的搜索结果
用 XML 格式显示单独的记录
您可以同样轻松地显示单独的车辆清单(而不是搜索结果)的 XML 视图。您只需使用返回 XML 输出的 executeXml() 方法更新各种 DisplayViews。
首先,清单 17 将显示 DisplayAction 的外观(注意,本系列的 第 2 部分:使用 Agavi 和 Doctrine 添加表单和数据库支持 已将它更新为只显示已批准的和有效的清单)。
清单 17. Listing/DisplayAction 定义
<?php
class Listing_DisplayAction extends WASPListingBaseAction
{
public function getDefaultViewName()
{
return 'Success';
}
public function executeRead(AgaviRequestDataHolder $rd)
{
try {
$id = $rd->getParameter('id');
$q = Doctrine_Query::create()
->from('Listing l')
->leftJoin('l.Manufacturer m')
->leftJoin('l.Country c')
->where('l.RecordID = ?', $id)
->addWhere('l.DisplayStatus = 1')
->addWhere('l.DisplayUntilDate >= CURDATE()')
->orderBy('l.RecordDate DESC');
$result = $q->fetchArray();
if (count($result) == 1) {
$this->setAttribute('listing', $result[0]);
return 'Success';
} else {
return 'Redirect404';
}
} catch (Exception $e) {
$this->setAttribute('error', $e->getMessage());
return 'Error';
}
}
}
?>
|
清单 18 更新 DisplaySuccessView 并添加一个 executeXml() 方法,该方法动态构造一个表示车辆清单的 XML 文档。
清单 18. Listing/DisplaySuccessView 定义
<?php
class Listing_DisplaySuccessView extends WASPListingBaseView
{
public function executeHtml(AgaviRequestDataHolder $rd)
{
$this->setupHtml($rd);
}
public function executeXml(AgaviRequestDataHolder $rd)
{
// get record
$record = $this->getAttribute('listing');
// create document
$dom = new DOMDocument('1.0', 'utf-8');
// create root element
$root = $dom->createElementNS('http://www.melonfire.com/agavi-wasp',
'wasp:result');
$dom->appendChild($root);
// import to SimpleXML for easier manipulation
$xml = simplexml_import_dom($dom);
// add nodes to XML output
$xml->addChild('id', $record['RecordID']);
$xml->addChild('submissionDate', $record['RecordDate']);
$xml->addChild('manufacturer', $record['Manufacturer']['ManufacturerName']);
$xml->addChild('model', ucwords(strtolower($record['VehicleModel'])));
$xml->addChild('year', $record['VehicleYear']);
$xml->addChild('color', strtolower($record['VehicleColor']));
$xml->addChild('mileage', $record['VehicleMileage']);
$xml->addChild('singleOwner', $record['VehicleIsFirstOwned']);
$xml->addChild('certified', $record['VehicleIsCertified']);
$xml->addChild('certifiedDate', $record['VehicleCertificationDate']);
$xml->addChild('note', $record['Note']);
$accessoryArr = array(
'1' => 'Power steering',
'2' => 'Power windows',
'4' => 'Audio system',
'8' => 'Video system',
'16' => 'Keyless entry system',
'32' => 'GPS',
'64' => 'Alloy wheels'
);
$accessories = $xml->addChild('accessories');
foreach ($accessoryArr as $k => $v) {
if ($record['VehicleAccessoryBit'] & $k) {
$accessories->addChild('item', $v);
}
}
$price = $xml->addChild('price');
$price->addChild('min', $record['VehicleSalePriceMin']);
$price->addChild('max', $record['VehicleSalePriceMax']);
$price->addChild('negotiable', $record['VehicleSalePriceIsNegotiable']);
$location = $xml->addChild('location');
$location->addChild('city', $record['OwnerCity']);
$location->addChild('country', $record['Country']['CountryName']);
// return output
return $xml->asXML();
}
}
?>
|
清单 19 展示了一个生成的结果示例:
清单 19. 一条 XML 编码的记录
<?xml version="1.0" encoding="utf-8"?>
<wasp:result xmlns:wasp="http://www.melonfire.com/agavi-wasp">
<wasp:id>1</wasp:id>
<wasp:submissionDate>2009-07-03</wasp:submissionDate>
<wasp:manufacturer>Porsche</wasp:manufacturer>
<wasp:model>Boxster</wasp:model>
<wasp:year>2005</wasp:year>
<wasp:color>yellow</wasp:color>
<wasp:mileage>15457</wasp:mileage>
<wasp:singleOwner>1</wasp:singleOwner>
<wasp:certified>1</wasp:certified>
<wasp:certifiedDate>2008-01-01</wasp:certifiedDate>
<wasp:note></wasp:note>
<wasp:accessories>
<wasp:item>Power steering</wasp:item>
<wasp:item>Power windows</wasp:item>
<wasp:item>Audio system</wasp:item>
<wasp:item>Keyless entry system</wasp:item>
</wasp:accessories>
<wasp:price>
<wasp:min>35000</wasp:min>
<wasp:max>40000</wasp:max>
<wasp:negotiable>1</wasp:negotiable>
</wasp:price>
<wasp:location>
<wasp:city>London</wasp:city>
<wasp:country>United Kingdom</wasp:country>
</wasp:location>
</wasp:result>
|
清单 20 是修改后的 DisplayErrorView:
清单 20. Listing/DisplayErrorView 定义
<?php
class Listing_DisplayErrorView extends WASPListingBaseView
{
public function executeHtml(AgaviRequestDataHolder $rd)
{
$this->setupHtml($rd);
}
public function executeXml(AgaviRequestDataHolder $rd)
{
if ($this->container->getValidationManager()->getReport()->count()) {
return $this->getErrorXml('Validation error');
} else {
return $this->getErrorXml('Application error');
}
}
}
?>
|
要查看其实际操作,浏览到 http://wasp.localhost/listing/display/1.xml,您将看到如 图 5 所示的内容。
图 5. XML 格式的一个单独的清单
注意,在最后两个示例中,Action 中的代码保持不变。通过允许开发人员决定如何在 View 中(而不是在 Action)中处理各种输出类型,Agavi 最小化了代码复制,同时坚持了 MVC 和 DRY(不要重复自己)原则。
结束语
至此,这个 WASP 示例应用程序的功能已经完备,其所有菜单链接都激活了。卖家可以上传车辆细节,买家可以搜索符合他们的要求的车辆。通过一个有用的管理模块,管理员可以编辑、修改和审批清单,开发人员可以使用一个 XML 接口来构造自己的应用程序。剩下的事情是进一步完善这个应用程序,我将在本系列的下一篇(也是最后一篇)文章中介绍这方面的内容。
从 下载 小节下载本文实现的所有代码。我建议您下载并开始试用它,尝试向它添加新东西。我敢保证您能从中获得更多的知识。祝您实验愉快,下次见!
下载 | 描述 | 名字 | 大小 | 下载方法 |
|---|
| 包含最新功能的 WASP 应用程序压缩文档 | wasp-04.zip | 3,852KB | HTTP |
|---|
参考资料 学习
获得产品和技术
讨论
关于作者  | 
|  | Vikram Vaswani 是 Melonfire 的创始人和 CEO,该公司是一家专门研究开源工具和技术的咨询服务公司。他还著有 PHP Programming Solutions 和 How to do Everything with PHP and MySQL 等著作。 |
对本文的评价
|  |