内容


使用 Agavi 进行 MVC 编程简介,第 5 部分

为 Agavi 应用程序添加分页、上传文件和定制输入验证器

学习使用 Agavi 框架构建可伸缩 Web 应用程序

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 使用 Agavi 进行 MVC 编程简介,第 5 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:使用 Agavi 进行 MVC 编程简介,第 5 部分

敬请期待该系列的后续内容。

简介

在本系列的第 4 部分结束之后,您已经具备一个功能齐全的 Web 应用程序,该应用程序包含管理模块、搜索引擎和 XML 输出功能。现在,您可能对本文讨论的主题摸不着头脑,因为 Web Automobiles Sales Platform (WASP) 应用程序的基本需求已经得到满足。

在最后一篇文章中,我将讨论一些您在构建 Web 应用程序时必须使用的额外技术和概念。这些技术覆盖较大的范围,从简单的分页和数据库记录排序,再到复杂的通过 Web 表单支持文件上传和编写定制输入验证器。对于所有这些情况,Agavi 框架都提供一些内置的工具,帮助您更轻松、更快捷、更安全地完成工作。让我们现在开始行动!

数据库记录排序

首先介绍结果集的分页和排序。图 1演示了如何在管理模块的摘要页面 http://wasp.localhost/admin/listing/index 显示结果集。

图 1. WASP 清单摘要页面
WASP 清单摘要页面的屏幕截图,它包含 8 辆汽车的信息
WASP 清单摘要页面的屏幕截图,它包含 8 辆汽车的信息

现在,我们添加一些功能,让用户能够对这些对象进行排序,以根据不同的条件显示结果。首先,编辑 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')); ?>">&uArr;</a> 
        <a href="<?php echo $ro->gen('admin.listing.index', 
         array('s' => 'RecordDate', 'd' => 'desc')); ?>">&dArr;</a> 
      </td> 
      <td class="key">Manufacturer 
        <a href="<?php echo $ro->gen('admin.listing.index', 
         array('s' => 'VehicleManufacturerID', 'd' => 'asc')); 
         ?>">&uArr;</a> 
        <a href="<?php echo $ro->gen('admin.listing.index', 
         array('s' => 'VehicleManufacturerID', 'd' => 'desc')); 
         ?>">&dArr;</a> 
      </td> 
      <td class="key">Model 
        <a href="<?php echo $ro->gen('admin.listing.index', 
         array('s' => 'VehicleModel', 'd' => 'asc')); 
         ?>">&uArr;</a> 
        <a href="<?php echo $ro->gen('admin.listing.index', 
         array('s' => 'VehicleModel', 'd' => 'desc')); 
         ?>">&dArr;</a> 
      </td> 
      <td class="key">Year 
        <a href="<?php echo $ro->gen('admin.listing.index', 
         array('s' => 'VehicleYear', 'd' => 'asc')); ?>">&uArr;</a> 
        <a href="<?php echo $ro->gen('admin.listing.index', 
         array('s' => 'VehicleYear', 'd' => 'desc')); 
         ?>">&dArr;</a> 
      </td> 
      <td class="key">Mileage 
        <a href="<?php echo $ro->gen('admin.listing.index', 
         array('s' => 'VehicleMileage', 'd' => 'asc')); 
         ?>">&uArr;</a> 
        <a href="<?php echo $ro->gen('admin.listing.index', 
         array('s' => 'VehicleMileage', 'd' => 'desc')); 
         ?>">&dArr;</a> 
      </td> 
      <td class="key">Color 
        <a href="<?php echo $ro->gen('admin.listing.index', 
         array('s' => 'VehicleColor', 'd' => 'asc')); 
         ?>">&uArr;</a> 
        <a href="<?php echo $ro->gen('admin.listing.index', 
         array('s' => 'VehicleColor', 'd' => 'desc')); 
         ?>">&dArr;</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; ?>

清单 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 清单摘要页面
WASP 清单摘要页面的屏幕截图,表标题的旁边添加了排序功能
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('&nbsp;'); 
      
      // 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 清单汇总,分为多页
WASP 清单汇总屏幕截图,共分为 2 页
WASP 清单汇总屏幕截图,共分为 2 页

在会话中储存数据

对管理模块的另一个改进(小改进)是仅在用户登录时有选择地显示特定链接(比如 “注销” 链接)。这相当简单;您仅需为 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 管理主菜单的屏幕截图,包含新的 Listings 和 Log Out 链接以及用户名
WASP 管理主菜单的屏幕截图,包含新的 Listings 和 Log Out 链接以及用户名

处理文件上传

到目前为止,销售者上传的所有汽车数据都保存在数据库中。但是一张图片胜过千言万语,对 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/>&nbsp;<p/> 
      <?php endforeach; ?> 
    </div> 
    <div id="specs"> 
      <table cellspacing="5"> 
      ... 
      </table> 
  </div> 
 </div>

现在,通过添加一个新的清单试试效果。表单此时应该包含额外的文件上传字段,如 图 5所示。

图 5. 支持图片上传的 WASP 清单表单
支持图片上传的 WASP 清单表单的屏幕截图
支持图片上传的 WASP 清单表单的屏幕截图

尝试上传非图片文件,或者上传不符合指定大小的图片,这时 AgaviImageFileValidator 将抛出错误和消息,如 图 6所示。

图 6. 无效图片上传错误
无效图片上传引起的错误的屏幕截图
无效图片上传引起的错误的屏幕截图

图片上传成功之后,清单细节页面将显示上传的图片和其他汽车信息。图 7是一个例子。

图 7. 包含图片集的 WASP 清单细节页面
包含图片集的 WASP 清单细节页面的屏幕截图
包含图片集的 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 输出之后的例子
修改 XML 输出之后的例子
修改 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 价格范围验证器的实际运行
WASP 价格范围验证器的实际运行屏幕截图,最低价格大于最高价格时显示错误
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 表单
Model 字段包含自动补全功能的 WASP Listing 表单的屏幕截图
Model 字段包含自动补全功能的 WASP Listing 表单的屏幕截图

结束语

本系列到此结束。通过本系列的 5 篇文章,我向您快速展示了 Agavi 框架,并介绍了使用它创建 Web 应用程序的基本技巧。如您所见,Agavi 是目前可用的最出色的 MVC 实现之一。它提供:模型、操作和视图的明确划分;严格遵循 OOP 原则;大量关于输入验证、安全、身份验证、数据库集成、输出变体和应用程序配置的工具。所有这些特性共同筑造了一个结果:更安全、更健壮、更灵活和扩展性更强的应用程序!

我希望本系列文章对您有用,并鼓励您在下一次编写 Web 应用程序时采用 Agavi 框架。祝您编程愉快!


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML, Open source, Web development
ArticleID=444148
ArticleTitle=使用 Agavi 进行 MVC 编程简介,第 5 部分: 为 Agavi 应用程序添加分页、上传文件和定制输入验证器
publish-date=11042009