 | 级别: 中级 Vikram Vaswani, 创始人, Melonfire
2009 年 11 月 04 日 本文是为 PHP 开发人员介绍开源、灵活和可伸缩的 Agavi 框架的五部分系列文章的最后一部分。在本文中,您将学习为 Agavi 应用程序上传文件、在会话中储存用户数据、集成第三方库和创建定制输入验证器。
简介
在本系列的第 4 部分结束之后,您已经具备一个功能齐全的 Web 应用程序,该应用程序包含管理模块、搜索引擎和 XML 输出功能。现在,您可能对本文讨论的主题摸不着头脑,因为 Web Automobiles Sales Platform (WASP) 应用程序的基本需求已经得到满足。
在最后一篇文章中,我将讨论一些您在构建 Web 应用程序时必须使用的额外技术和概念。这些技术覆盖较大的范围,从简单的分页和数据库记录排序,再到复杂的通过 Web 表单支持文件上传和编写定制输入验证器。对于所有这些情况,Agavi 框架都提供一些内置的工具,帮助您更轻松、更快捷、更安全地完成工作。让我们现在开始行动!
数据库记录排序
首先介绍结果集的分页和排序。图 1演示了如何在管理模块的摘要页面 http://wasp.localhost/admin/listing/index 显示结果集。
图 1. WASP 清单摘要页面
现在,我们添加一些功能,让用户能够对这些对象进行排序,以根据不同的条件显示结果。首先,编辑 AdminIndexSuccess 模板,并向每个表的标题添加排序链接,如 清单 1所示。
清单 1. Listing/AdminIndexSuccess 模板
<h3>View All Listings</h3>
<?php if (count($t['records']) == 0): ?>
No records available
<?php else: ?>
<div id="list">
<form action="<?php echo $ro->gen('admin.listing.delete'); ?>" method="post" >
<table cellspacing="5">
<tr>
<td class="key"></td>
<td class="key"></td>
<td class="key">Submission Date
<a href="<?php echo $ro->gen('admin.listing.index',
array('s' => 'RecordDate', 'd' => 'asc')); ?>">⇑</a>
<a href="<?php echo $ro->gen('admin.listing.index',
array('s' => 'RecordDate', 'd' => 'desc')); ?>">⇓</a>
</td>
<td class="key">Manufacturer
<a href="<?php echo $ro->gen('admin.listing.index',
array('s' => 'VehicleManufacturerID', 'd' => 'asc'));
?>">⇑</a>
<a href="<?php echo $ro->gen('admin.listing.index',
array('s' => 'VehicleManufacturerID', 'd' => 'desc'));
?>">⇓</a>
</td>
<td class="key">Model
<a href="<?php echo $ro->gen('admin.listing.index',
array('s' => 'VehicleModel', 'd' => 'asc'));
?>">⇑</a>
<a href="<?php echo $ro->gen('admin.listing.index',
array('s' => 'VehicleModel', 'd' => 'desc'));
?>">⇓</a>
</td>
<td class="key">Year
<a href="<?php echo $ro->gen('admin.listing.index',
array('s' => 'VehicleYear', 'd' => 'asc')); ?>">⇑</a>
<a href="<?php echo $ro->gen('admin.listing.index',
array('s' => 'VehicleYear', 'd' => 'desc'));
?>">⇓</a>
</td>
<td class="key">Mileage
<a href="<?php echo $ro->gen('admin.listing.index',
array('s' => 'VehicleMileage', 'd' => 'asc'));
?>">⇑</a>
<a href="<?php echo $ro->gen('admin.listing.index',
array('s' => 'VehicleMileage', 'd' => 'desc'));
?>">⇓</a>
</td>
<td class="key">Color
<a href="<?php echo $ro->gen('admin.listing.index',
array('s' => 'VehicleColor', 'd' => 'asc'));
?>">⇑</a>
<a href="<?php echo $ro->gen('admin.listing.index',
array('s' => 'VehicleColor', 'd' => 'desc'));
?>">⇓</a>
</td>
<td class="key"></td>
</tr>
<?php foreach ($t['records'] as $record): ?>
<tr>
<td><input type="checkbox" name="id[]"
value="<?php echo $record['RecordID']; ?>" style="width:2px" />
</td>
<td><?php echo $record['RecordID']; ?></td>
<td><?php echo date('d M Y', strtotime($record['RecordDate'])); ?>
</td>
<td><?php echo $record['Manufacturer']['ManufacturerName']; ?>
</td>
<td><?php echo $record['VehicleModel']; ?></td>
<td><?php echo $record['VehicleYear']; ?></td>
<td><?php echo $record['VehicleMileage']; ?></td>
<td><?php echo $record['VehicleColor']; ?></td>
<td><a href="<?php echo $ro->gen('admin.listing.edit',
array('id' => $record['RecordID'])); ?>">Edit</a></td>
</tr>
<?php endforeach; ?>
<tr>
<td colspan="9"><input type="submit" name="submit"
style="width:150px" value="Delete Selected" /></td>
</tr>
</table>
</form>
</div>
<?php endif; ?>
|
 |
常用缩略词
- API:应用程序编程接口(Application program interface)
- CSS:层叠样式表(Cascading stylesheet)
- HTML:超文本标记语言(Hypertext Markup Language)
- MVC:模型 - 视图 - 控制器(Model-View-Controller)
- OOP:面向对象编程(Object-oriented programming)
- ORM:对象关系映射(Object-Relational Mapping)
- SQL:结构化查询语言(Structured Query Language)
- URL:统一资源定位器(Uniform Resource Locator)
- XML:可扩展标记语言(Extensible Markup Language)
|
|
如 清单 1所示,每个链接有两个参数:
s表示排序所依据的字段
d表示排序的方向(升序或降序)
其次,编辑 AdminIndexAction 验证器以支持这两个参数(清单 2)。注意,清单 2使用 AgaviInArrayValidator 限制了每个参数的值列表。
清单 2. Listing/AdminIndexAction 验证器
<?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="inarray">
<arguments>
<argument>s</argument>
</arguments>
<errors>
<error>ERROR: Invalid sort field</error>
</errors>
<ae:parameters>
<ae:parameter name="required">false</ae:parameter>
<ae:parameter name="values">RecordID,RecordDate,
VehicleManufacturerID,VehicleModel,
VehicleColor,VehicleYear,VehicleMileage</ae:parameter>
<ae:parameter name="sep">,</ae:parameter>
</ae:parameters>
</validator>
<validator class="inarray">
<arguments>
<argument>d</argument>
</arguments>
<errors>
<error>ERROR: Invalid sort direction</error>
</errors>
<ae:parameters>
<ae:parameter name="required">false</ae:parameter>
<ae:parameter name="values">asc,desc</ae:parameter>
<ae:parameter name="sep">,</ae:parameter>
</ae:parameters>
</validator>
</validators>
</ae:configuration>
</ae:configurations>
|
然后,编辑 AdminIndexAction 的 executeRead()方法以读取这些参数,并使用另一个 ORDER BY 子句修改 Doctrine 查询,如 清单 3所示。
清单 3. Listing/AdminIndexAction 定义
<?php
class Listing_AdminIndexAction extends WASPListingBaseAction
{
public function getDefaultViewName()
{
return 'Success';
}
public function executeRead(AgaviRequestDataHolder $rd)
{
try {
// get input variables
$id = $rd->getParameter('id');
$sort = $rd->isParameterValueEmpty('s') ?
'RecordID' : $rd->getParameter('s');
$dir = $rd->isParameterValueEmpty('d') ?
'asc' : $rd->getParameter('d');
// create query
$q = Doctrine_Query::create()
->from('Listing l')
->leftJoin('l.Manufacturer m')
->leftJoin('l.Country c')
->orderBy(sprintf('%s %s', $sort, $dir));
$result = $q->fetchArray();
// set view variables
$this->setAttribute('records', $result);
return 'Success';
} catch (Exception $e) {
$this->setAttribute('error', $e->getMessage());
return 'Error';
}
}
final public function isSecure()
{
return true;
}
}
?>
|
现在,当重新通过 http://wasp.localhost/admin/listing/index 访问摘要页面时,您将看到排序链接显示在每个表标题的旁边。选择这些链接之一,将根据指定的字段和方向对结果集进行重新排序。图 2显示了一个例子。
图 2. 添加了排序功能的 WASP 清单摘要页面
数据库记录分页
处理大型数据集的另一个常见需求是以页的形式显示数据,这样不仅减少数据库服务器的负载(生成更小的结果集),而且便于用户更高效地管理数据(在更小的块中查看信息)。与其他一些框架不同,Agavi 没有附带内置的分页对象。不过,您可以通过 Doctrine 轻松实现该功能。
Doctrine 带有一个 Doctrine_Pager 对象,它充当任何涉及数据库记录分页的操作的命令枢纽。它支持两种最常见的数据库结果集分页类型(渐进式(sliding)和 跳跃式(jumping)),并且允许对格式进行大量定制和显示页码和链接。本文无法详细讨论这些主题;参考资料部分提供相关 Doctrine 手册页的链接。
第一步是更新 AdminIndexAction 验证器以接受一个额外的页参数,我将其称为 p。清单 4提供了验证器的代码:
清单 4. Listing/AdminIndexAction 验证器
<?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>p</argument>
</arguments>
<errors>
<error>ERROR: Invalid page number</error>
</errors>
<ae:parameters>
<ae:parameter name="type">int</ae:parameter>
<ae:parameter name="required">false</ae:parameter>
<ae:parameter name="min">1</ae:parameter>
</ae:parameters>
</validator>
</validators>
</ae:configuration>
</ae:configurations>
|
您还需要更新 AdminIndexAction 以注意到这个额外参数。清单 5对清单 4 的代码进行了修改。
清单 5. Listing/AdminIndexAction 定义
<?php
class Listing_AdminIndexAction extends WASPListingBaseAction
{
public function getDefaultViewName()
{
return 'Success';
}
public function executeRead(AgaviRequestDataHolder $rd)
{
try {
// get input variables
$id = $rd->getParameter('id');
$sort = $rd->isParameterValueEmpty('s')
? 'RecordID' : $rd->getParameter('s');
$dir = $rd->isParameterValueEmpty('d')
? 'asc' : $rd->getParameter('d');
$page = $rd->getParameter('p');
// create query
$q = Doctrine_Query::create()
->from('Listing l')
->leftJoin('l.Manufacturer m')
->leftJoin('l.Country c')
->orderBy(sprintf('%s %s', $sort, $dir));
// set pager parameters
$perPage = 5;
$numPageLinks = 5;
// initialize pager
$pager = new Doctrine_Pager($q, $page, $perPage);
// execute paged query
$result = $pager->execute(array(), Doctrine::HYDRATE_ARRAY);
// initialize pager layout
$pagerRange = new Doctrine_Pager_Range_Sliding(
array('chunk' => $numPageLinks), $pager
);
$pagerUrlBase = $this->getContext()->getRouting()->gen(
'admin.listing.index', array('s' => $sort, 'd' => $dir)
);
$pagerLayout = new Doctrine_Pager_Layout($pager, $pagerRange, $pagerUrlBase);
// set page link display template
$pagerLayout->setTemplate(
'<a href="{%url}&p={%page}">{%page}</a>'
);
$pagerLayout->setSelectedTemplate('{%page}');
$pagerLayout->setSeparatorTemplate(' ');
// set view variables
$this->setAttribute('records', $result);
$this->setAttribute('pages', $pagerLayout->display(null, true));
return 'Success';
} catch (Exception $e) {
$this->setAttribute('error', $e->getMessage());
return 'Error';
}
}
final public function isSecure()
{
return true;
}
}
?>
|
清单 5包含许多新的元素,并提供解释。清单 5初始化一个 Doctrine_Pager 对象并向对象构造器传递 3 个关键参数:需要执行的 SQL 查询、当前页码(从输入变量 p获取)和每页显示的结果数(假定为 5)。然后,Doctrine_Pager 对象执行包含适当 LIMIT 子句的查询,仅获取所需的记录。
这还没有完成!您还将显示一个页码列表,并允许用户在结果集中前进或后退。这通过 Doctrine_Pager_Layout 对象来实现,您可以使用该对象定义分页模式、每个页链接的基 URL 和显示的页链接的数量。此外,还可以使用 Doctrine_Pager_Layout 对象的 setTemplate()方法精确控制页码的格式,让页码彼此分离并显示在最终的布局中。当所有配置完成之后,调用 Doctrine_Pager_Layout 对象的 display()方法将为必要的页链接生成 HTML 代码,然后将其分配给模板变量 $t['pages']。
现在惟一需要做的就是更新 AdminIndexSuccess 模板,并将 HTML 代码并入到其中(清单 6)。
清单 6. Listing/AdminIndexSuccess 模板
<h3>View All Listings</h3>
<?php if (count($t['records']) == 0): ?>
No records available
<?php else: ?>
<div class="pager">
Pages: <?php echo $t['pages']; ?>
</div>
<div id="list">
...
</div>
<?php endif; ?>
<div class="pager">
Pages: <?php echo $t['pages']; ?>
</div>
|
图 3显示了结果的例子。
图 3. WASP 清单汇总,分为多页
在会话中储存数据
对管理模块的另一个改进(小改进)是仅在用户登录时有选择地显示特定链接(比如 “注销” 链接)。这相当简单;您仅需为 AgaviUser::isAuthenticated()方法编写一个条件测试。清单 7显示了需要对 AdminMaster 模块进行的修改。
清单 7. AdminMaster 模块
<!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">
<head>
...
</head>
<body>
<!-- begin header -->
<div id="header">
<div id="logo">
<img src="/images/logo-admin.jpg" />
</div>
<div id="menu">
<?php if($this->getContext()->getUser()->isAuthenticated()): ?>
<ul>
<li><a href="<?php echo $ro->gen('admin.listing.index'); ?>"
>Listings</a></li>
<li><a href="<?php echo $ro->gen('admin.logout'); ?>"
>Log Out</a></li>
</ul>
<?php endif; ?>
</div>
</div>
<!-- end header -->
<!-- begin body -->
...
<!-- end body -->
<!-- begin footer -->
...
<!-- end footer -->
</body>
</html>
|
如果您想要在会话中储存用户数据,则需要使用 AgaviUser::setAttribute()方法。清单 8显示了如何修改 LoginAction 以存储当前登录用户的用户名。
清单 8. Default/LoginAction 定义
<?php
class Default_LoginAction extends WASPDefaultBaseAction
{
public function getDefaultViewName()
{
return 'Input';
}
public function executeWrite(AgaviRequestDataHolder $rd)
{
try {
// get input parameters
$u = $rd->getParameter('username');
$p = $rd->getParameter('password');
// check user credentials
$q = Doctrine_Query::create()
->from('User u')
->where('u.Username = ? AND u.Password = PASSWORD(?)', array($u,$p));
$result = $q->fetchArray();
// set authentication flag if valid
if (count($result) == 1) {
$this->getContext()->getUser()->setAuthenticated(true);
$this->getContext()->getUser()->setAttribute('username',
$u, 'wasp.user.namespace');
return 'Success';
} else {
return 'Error';
}
} catch (Exception $e) {
return 'Error';
}
}
}
?>
|
清单 9修改 AdminMaster 模板以从会话获取该数据并在管理模板中显示它。
清单 9. AdminMaster 模板
<!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">
<head>
...
</head>
<body>
<!-- begin header -->
...
<!-- end header -->
<!-- begin body -->
<div id="body">
<?php if($this->getContext()->getUser()->isAuthenticated()): ?>
Logged in as:
<?php echo $this->getContext()->getUser()->getAttribute('username',
'wasp.user.namespace'); ?>
<?php endif; ?>
<?php echo $inner; ?>
</div>
<!-- end body -->
<!-- begin footer -->
...
<!-- end footer -->
</body>
</html>
|
图 4显示了这些更改的结果。
图 4. WASP 管理主菜单,包含一些新链接
处理文件上传
到目前为止,销售者上传的所有汽车数据都保存在数据库中。但是一张图片胜过千言万语,对 WASP 应用程序而言,允许销售者添加汽车图片是非常棒的功能。该功能的实现并不难。
首先创建一个目录,以将上传的图片储存在 $WASP_ROOT/pub/usr/ 中。确保 Web 服务器用户能够向该目录写数据。
shell> cd /usr/local/apache/htdocs/wasp/pub
shell> mkdir usr
|
然后,使用额外的文件上传字段更新 CreateInput 模板,如 清单 10所示。
清单 10. Listing/CreateInput 模板
<script src="/js/form.js"></script>
<h3>Add Listing</h3>
<form action="<?php echo $ro->gen(null); ?>" method="post"
enctype="multipart/form-data">
...
<fieldset>
<legend>Vehicle Images</legend>
<label for="Images[1]">Image #1:</label>
<input id="Images[1]" type="file" name="Images[1]" />
<p/>
<label for="Images[2]">Image #2:</label>
<input id="Images[2]" type="file" name="Images[2]" />
<p/>
<label for="Images[3]">Image #3:</label>
<input id="Images[3]" type="file" name="Images[3]" />
<p/>
<p class="note">
<em>Images should be 200x125 px, <br/> JPEG or GIF format only.
</em>
</p>
</fieldset>
<input type="submit" name="submit" class="submit" value="Submit Listing" />
</form>
|
注意,清单 10还为 <form>元素添加了一个 enctype属性以支持多方表单提交。
上传的文件是 Web 应用程序最易受攻击的地方之一,因此在接受存储它们之前一定要进行验证。好消息是,Agavi 附带了一个功能齐全的 AgaviImageFileValidator,它能够验证特定的输入参数是不是有效的图片文件。这个验证器还能确认图片与特定格式、宽度和高度是否相对应。
清单 11显示了通过调用 AgaviImageFileValidator 更新 CreateAction 验证器。
清单 11. Listing/CreateAction 验证器
<?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="write">
...
<validator class="imagefile">
<arguments base="Images[]">
<argument/>
</arguments>
<errors>
<error for="no_image">ERROR: Uploaded file is not an image</error>
<error for="format">ERROR: Image file format is invalid</error>
<error>ERROR: Image size is incorrect</error>
</errors>
<ae:parameters>
<ae:parameter name="required">false</ae:parameter>
<ae:parameter name="format">gif jpeg</ae:parameter>
<ae:parameter name="min_width">200</ae:parameter>
<ae:parameter name="max_width">200</ae:parameter>
<ae:parameter name="min_height">125</ae:parameter>
<ae:parameter name="max_height">125</ae:parameter>
</ae:parameters>
</validator>
</validators>
</ae:configuration>
</ae:configurations>
|
从 清单 11可以看到,仅当上传的文件为 GIF 或 JPEG 格式、并且宽度和高度分别为 200 和 125 像素时,才接收它们。
一旦接受上传文件之后,应用程序将重命名它并保存到指定的目录 $WASP_ROOT/pub/usr/。清单 12修改 CreateAction 以执行这些任务。
清单 12. Listing/CreateAction 定义
<?php
class Listing_CreateAction extends WASPListingBaseAction
{
public function getDefaultViewName()
{
return 'Input';
}
public function executeWrite(AgaviRequestDataHolder $rd)
{
try {
// initialize object
$listing = new Listing();
// populate with validated input
$listing->fromArray($rd->getParameters());
$listing->VehicleAccessoryBit =
array_sum($rd->getParameter('VehicleAccessoryBit'));
// set some values manually
$listing->RecordDate = date('Y-m-d', mktime());
$listing->DisplayStatus = 0;
// save record and get record ID
$listing->save();
$id = $listing->RecordID;
// rename and move uploaded images
$target = AgaviConfig::get('core.app_dir') . '/../pub/usr/';
$x = 1;
foreach ($rd->getFile('Images') as $file) {
switch ($file->getType()) {
case 'image/jpeg':
case 'image/pjpeg':
$name = sprintf('%d_%d', $id, $x) . '.jpg';
$file->move("$target/$name");
break;
case 'image/gif':
$name = sprintf('%d_%d', $id, $x) . '.gif';
$file->move("$target/$name");
break;
}
$x++;
}
return 'Success';
} catch (Exception $e) {
$this->setAttribute('error', $e->getMessage());
return 'Error';
}
}
}
?>
|
AgaviRequestDataHolder 提供的 getFile()方法让以 AgaviUploadedFile 对象的形式获取上传文件非常容易。然后,这些对象公开一些方便的方法,比如 getType()、getSize()和 getName(),以及将文件移动到新位置的 move()方法。
处理文件上传仅是整体的一部分。您还需要在对应清单的细节页面中显示上传的图片。为此,需要使用 清单 13中的代码更新 DisplaySuccess 模板。
清单 13. 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="gallery">
<?php $id = $t['listing']['RecordID']; ?>
<?php $target = AgaviConfig::get('core.app_dir') . '/../pub/usr/'; ?>
<?php foreach (glob($target.$id.'_*.{gif,jpg}', GLOB_BRACE) as $file): ?>
<img width="200" height="125" src="/usr/<?php echo basename($file); ?>"
style="float:left"/>
<p/> <p/>
<?php endforeach; ?>
</div>
<div id="specs">
<table cellspacing="5">
...
</table>
</div>
</div>
|
现在,通过添加一个新的清单试试效果。表单此时应该包含额外的文件上传字段,如 图 5所示。
图 5. 支持图片上传的 WASP 清单表单
尝试上传非图片文件,或者上传不符合指定大小的图片,这时 AgaviImageFileValidator 将抛出错误和消息,如 图 6所示。
图 6. 无效图片上传错误
图片上传成功之后,清单细节页面将显示上传的图片和其他汽车信息。图 7是一个例子。
图 7. 包含图片集的 WASP 清单细节页面
为了保持简洁,您还可以更新 AdminDeleteAction,以在删除一个清单之后删除与之关联的图片。清单 14给出了实现该目的的代码。
清单 14. Listing/AdminDeleteAction 定义
<?php
class Listing_AdminDeleteAction extends WASPListingBaseAction
{
public function getDefaultViewName()
{
return 'Success';
}
public function executeWrite(AgaviRequestDataHolder $rd)
{
try {
// get record ids
$ids = $rd->getParameter('id');
foreach ($ids as $id) {
// delete record from database
$q = Doctrine_Query::create()
->delete('Listing')
->addWhere('RecordID = ?', $id);
$result = $q->execute();
// delete associated image files
$target = AgaviConfig::get('core.app_dir') . '/../pub/usr/';
foreach (glob("$target/$id_*") as $file) {
unlink($file);
}
}
return 'Success';
} catch (Exception $e) {
$this->setAttribute('error', $e->getMessage());
return 'Error';
}
}
final public function isSecure()
{
return true;
}
}
?>
|
为了实现完整性,您可能还希望更新 DisplaySuccessView 的 executeXml()方法,以在 XML 输出中包含图片信息。清单 15显示了实现该目的的代码:
清单 15. 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']);
// add image information if available
$images = $xml->addChild('images');
$id = $record['RecordID'];
$target = AgaviConfig::get('core.app_dir') . '/../pub/usr/';
foreach (glob($target.$id.'_*.{gif,jpg}', GLOB_BRACE) as $file) {
$images->addChild('image',
$this->getContext()->getRouting()->getBaseHref() .
'usr/' . basename($file));
}
// return output
return $xml->asXML();
}
}
?>
|
图 8显示了修改 XML 输出之后的例子。
图 8. 修改 XML 输出之后的例子
创建定制输入验证器
如本系列前面的文章所示,Agavi 附带有各种输入验证器,从而让开发人员能够轻松确保他们的 Actions 仅接受干净的输入。这些内置的验证器对一般的验证任务足足有余,比如验证邮件地址或日期。不过,如果您需要执行 Agavi 没有默认包含的验证检查,那么应该怎么办呢?当然是编写一个定制验证器!
每个应用程序的验证需求都是惟一的,Agavi 允许您通过扩展 AgaviValidator 基类轻松创建自己的验证器。创建之后就可以以常规的的方式调用这些定制的验证器,即对每个 Action 执行 XML 验证文件。
为了演示创建定制验证器的流程,请考虑 CreateInput 表单,其中每个销售者都要求输入其汽车的最低和最高价格。很明显,最低价格应该小于最高价格。不过,Agavi 的所有内置验证器都无法执行这种类型的比较。到目前为止,销售者完全可以输入一个高于最高价格的最低价格(可以亲自试试)。为了修复这个漏洞,您可以创建一个定制的验证器。
首先,使用以下规则更新 CreateAction 验证器(清单 16):
清单 16. Listing/CreateAction 验证器
<?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="write">
...
<validator class="PriceRangeCustomValidator">
<arguments>
<argument name="max">VehicleSalePriceMax</argument>
<argument name="min">VehicleSalePriceMin</argument>
</arguments>
<errors>
<error for="max_min_mismatch">ERROR: Vehicle maximum price
is lower than vehicle minimum price</error>
</errors>
<ae:parameters>
<ae:parameter name="required">true</ae:parameter>
</ae:parameters>
</validator>
</validators>
</ae:configuration>
</ae:configurations>
|
这个规则引用了一个定制的验证器 PriceRangeCustomValidator。这个验证器接受两个参数 —— 最低价格和最高价格 —— 并且包含一条定制错误消息,当捕捉到 max_min_mismatch错误时将生成该消息。
当然,PriceRangeCustomValidator 验证器还没有构建好。要创建它,在 $WASP_ROOT/app/lib/validator/PriceRangeCustomValidator.class.php 打开一个新文件,并使用 清单 17中的代码填充它。
清单 17. Listing/PriceRangeCustomValidator 定义
<?php
class PriceRangeCustomValidator extends AgaviValidator {
protected function validate()
{
$args = $this->getArguments();
$max = $this->getData($args['max']);
$min = $this->getData($args['min']);
if ($min > $max)
{
$this->throwError('max_min_mismatch');
return false;
}
return true;
}
}
?>
|
validate() 方法是所有 AgaviValidator 的核心,该方法执行验证并返回 true(如果输入有效)或 false(如果输入无效)。在 清单 17中,validate() 方法使用 getData() 方法读取传递到验证器的参数,然后执行一个简单的比较测试并将结果返回给调用方。如果输入不能通过测试,那么 validate()方法将抛出 max_min_mismatch错误,该错误进而触发 AgaviFormPopulationFilter 以在请求客户端显示对应的错误消息。
您还需要自动装载这个验证器,方式是使用额外的条目更新 $WASP_ROOT/app/config/autoload.xml,如 清单 18所示。
清单 18. 需要自动装载的类的列表
<?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="WASPBaseAction">
%core.lib_dir%/action/WASPBaseAction.class.php
</autoload>
<autoload name="WASPBaseModel">
%core.lib_dir%/model/WASPBaseModel.class.php
</autoload>
<autoload name="WASPBaseView">
%core.lib_dir%/view/WASPBaseView.class.php
</autoload>
<autoload name="Doctrine">
%core.app_dir%/../libs/doctrine/Doctrine.php
</autoload>
<autoload name="PriceRangeCustomValidator">
%core.lib_dir%/validator/PriceRangeCustomValidator.class.php
</autoload>
</ae:configuration>
</ae:configurations>
|
要看到该验证器的实际运行效果,尝试创建一个新的清单,并输入一个大于最高价格的最低价格。图 9显示了您将看到的结果。
图 9. WASP 价格范围验证器的实际运行
添加自动补全功能
最近几个月以来,自动补全功能在 Web 应用程序中越来越常见,它让应用程序根据用户输入的部分自动建议匹配的词汇。
在 Add Listing 表单内部,Model 字段最适合使用自动补全功能。为什么?因为汽车的型号名称是有限的。随着汽车清单数据库不断增长,用户可能更趋向于输入数据库已经存在的型号名称。您可以借助该功能节省用户输入,并在用户输入时提供匹配的型号名列表,让用户从列表中选择所需的型号。
现在,许多第三方库能够快速向 Web 应用程序添加该特性,包括 PEAR HTML_QuickForm、Dojo 和 Yahoo! User Interface (YUI) 库。YUI 库是实现此类客户端特性改进的最完整工具箱,因此我将使用它为 Add Listing 表单添加自动补全功能。
首先,下载 YUI AutoComplete 小部件,它包含 JavaScript 和 CSS 源文件(见 参考资料部分的链接)。创建 $WASP_ROOT/pub/css/yui 和 $WASP_ROOT/pub/js/yui 目录,并将这些 CSS 和 JavaScript 文件分别复制到这两个目录。然后,编辑您在 第 3 部分:使用 Agavi 添加验证和管理功能中创建的 WASPListingBaseView:: setInputViewAttributes()基础方法,并使用能够从数据库生成惟一型号名列表的 SQL 查询更新该方法(清单 19)。
清单 19. 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());
$q = Doctrine_Query::create()
->select('DISTINCT l.VehicleModel AS VehicleModel')
->from('Listing l');
$this->setAttribute('models', $q->fetchArray());
}
}
?>
|
查询的结果储存在模板变量 $t['models']中。
清单 20更新了 CreateInput 模板,并添加必要的客户端代码来支持 YUI AutoComplete 功能。
清单 20. Listing/CreateInput 模板
<script src="/js/form.js"></script>
<h3>Add Listing</h3>
<form action="<?php echo $ro->gen(null); ?>" method="post">
...
<label for="VehicleModel" class="required">Model:</label>
<input id="VehicleModel" type="text" name="VehicleModel">
<div id="ac1" class="yui-skin-sam yui-ac-container"
style="position:relative; width:300px; margin-left:210px">
</div>
</input>
<p/>
...
</form>
<!-- YUI autocomplete widget -->
<!-- based on example at
http://developer.yahoo.com/yui/examples/autocomplete/ac_basic_array.html
-->
<script src="js/yui/yahoo-min.js"></script>
<script src="js/yui/dom-min.js"></script>
<script src="js/yui/event-min.js"></script>
<script src="js/yui/datasource-min.js"></script>
<script src="js/yui/autocomplete-min.js"></script>
<script>
arrayModels = [
<?php if (isset($t['models']) && count($t['models']) > 0): ?>
<?php foreach ($t['models'] as $m): ?>
<?php echo "\"" . $m['VehicleModel'] . "\",\r\n"; ?>
<?php endforeach; ?>
<?php endif; ?>
];
YAHOO.example.BasicLocal = function() {
// Use a LocalDataSource
var oDS = new YAHOO.util.LocalDataSource(arrayModels);
// Instantiate the AutoCompletes
var oAC = new YAHOO.widget.AutoComplete("VehicleModel", "ac1", oDS);
oAC.prehighlightClassName = "yui-ac-prehighlight";
return {
oDS: oDS,
oAC: oAC,
};
}();
</script>
|
清单 20末尾的 JavaScript 代码首先初始化一个 YUI LocalDataSource 对象,然后使用包含来自 PHP 模板变量 $t['models']的型号名的 JavaScript 数组填充它。然后,LocalDataSource 将附加到 YUI AutoComplete 对象,而后者反过来链接到 ID 为 VehicleModel的表单元素。
所有这些工作换来的结果是,当用户向 VehicleModel 字段开始输入数据时,AutoComplete 小部件将使用 LocalDataSource 对象中的值匹配输入,并抛出一个匹配词汇列表供用户从中选择所需的词汇。图 10显示了一个自动补全的例子。
图 10. Model 字段包含自动补全功能的 WASP Listing 表单
结束语
本系列到此结束。通过本系列的 5 篇文章,我向您快速展示了 Agavi 框架,并介绍了使用它创建 Web 应用程序的基本技巧。如您所见,Agavi 是目前可用的最出色的 MVC 实现之一。它提供:模型、操作和视图的明确划分;严格遵循 OOP 原则;大量关于输入验证、安全、身份验证、数据库集成、输出变体和应用程序配置的工具。所有这些特性共同筑造了一个结果:更安全、更健壮、更灵活和扩展性更强的应用程序!
我希望本系列文章对您有用,并鼓励您在下一次编写 Web 应用程序时采用 Agavi 框架。祝您编程愉快!
下载 | 描述 | 名字 | 大小 | 下载方法 |
|---|
| 具有最新功能的 WASP 应用程序归档 | wasp-05.zip | 3,881KB | HTTP |
|---|
参考资料 学习
获得产品和技术
讨论
关于作者  | 
|  | Vikram Vaswani 是 Melonfire 的创始人和 CEO,该公司是一家专门研究开源工具和技术的咨询服务公司。他还著有 PHP Programming Solutions 和 How to do Everything with PHP and MySQL 等著作。 |
对本文的评价
|  |