利用 Agavi 创建 REST API

红薯 发布于 2010/04/26 13:46
阅读 495
收藏 1

简介: Agavi 是一个开源的、灵活且可伸缩的应用程序开发框架。它的一个关键特性是内置对 REST 路由的支持,使我们可以向现有或新的 Web 应用程序快速添加用于第三方开发的 REST API。本文将详细探讨这一特性,以及如何利用对 XML 和 JSON 格式的支持来构建一个 REST API。

如今,每个像样点的 Web 应用程序都有一个 REST API。Flickr 有,Google、Bit.ly 和 NetFlix 也有,以及很多其他流行的应用程序都有。REST 作为一种架构模式很流行,因为它直观地将现有 HTTP 动词映射到普通数据操作,并且提供现有基于 SOAP 和 RPC 的架构的轻量级替代物,具有更少的数据录入和顺应性需求。这可带来时间和成本节约:基于 REST 的 API 相比对应的基于 SOAP 和 RPC 的 API 来说,通常实现速度更为快速,在其上进行开发更为容易。

在前面的一组文章中,我介绍了 Agavi MVC 框架,举例说明可以使用它来快速有效地构建可伸缩的 Web 应用程序。Agavi 具有很多优点:

  • 成熟的输入过滤和验证
  • OOP-顺应的架构
  • 定制的 URL 路由
  • 可扩展的基于角色的访问控制

它还提供两个对 REST API 开发人员来说价值无法估量的特性:内置支持 REST HTTP 方法,以及支持多种输出类型(比如 XML 和 JSON)。

本文将详细介绍利用 Agavi 构建一个简单 REST API 的过程。如果您已经有了一个基于 Agavi 的应用程序,本文将解释如何利用现有框架协定以及将您的应用程序内部代码暴露给第三方开发人员。如果您是创建新的基于 REST 的应用程序,本文将演示使用 Agavi 会如何使得整个过程更为简单高效。

首先,简单介绍一下 REST,即 Representational State Transfer(具象状态传输)。REST 不同于 SOAP,它基于资源和操作,而不是基于方法和数据类型。资源就是一个 URL,它引用对象或实体(例如 /users/photos), 这些实体上可以执行操作,而操作是以下四个 HTTP 动词之一 — GET(检索)、POST(创建)、PUT(更新)和 DELETE(删除)。

为了更好地理解这一点,我们来看一个简单的例子。假设您有一个照片共享应用程序,您需要一些 API 方法,以便开发人员可以向应用程序数据存储库远程地添加新照片或者检索现有照片。使用 SOAP 方式时,您通常具有诸如 createPhoto()getPhoto() 之类的 SOAP API 方法,这些方法接受 XML 编码的请求(请求中包含照片参数作为输入),负责创建或检索照片记录,并返回 XML 编码的响应(指出成功或失败)。SOAP WSDL 定义请求和响应信息包的格式、各种输入参数的数据类型以及可能的响应值范围。

REST 方式要简单得多。使用该方式时,您暴露一个 URL 端点(比如 /photos),并检查用于访问该 URL 的 HTTP 方法,以理解所需的操作。所以,比如说您可以向 /photos POST 一个 HTTP 信息包,以创建一个新照片,或者发送一个请求到 GET /photos,以获得可用照片的列表。该方式容易理解得多,因为它 将现有 HTTP 动词映射到 CRUD 操作,并且资源消耗也更低,因为不要正式定义所需请求/响应头的数据类型。

对 URL 请求的典型 REST 协定及其含义如下:

  • GET /items:检索一系列条目
  • GET /items/123:检索条目 #123
  • POST /items:创建新条目
  • PUT /items/123:更新条目 #123
  • DELETE /items/123:删除条目 #123

Agavi 对这些 REST 协定具有内置的支持。如果看了本系列以前的文章,您就已经看到,该框架自动将 GET 和 POST 请求映射到操作的 executeRead()executeWrite() 方法。类似地,PUT 和 DELETE 请求也将自动映射到操作的 executeCreate()executeRemove() 方法。因此,定义新的 REST API 很简单,就是定义这些操作方法,用代码填充它们,并正确地将请求路由到它们。这正是您下面在本文要做的事情。

开始实现 REST API 之前,有几点需要注意。在整篇文章中,我假设您有一个可以工作的(Apache、PHP 和 MySQL)开发环境,并且熟悉 SQL 和 XML 的基础知识。我还要假设您熟悉利用 Agavi 进行应用程序开发的基本原理,理解操作、视图、模型和路由之间的交互,并且熟悉在 Agavi 应用程序中使用 Doctrine 模型。最后,我还要假设您的 Apache Web 服务器被配置为支持虚拟宿主、URL 重写以及 PUT 和 DELETE 请求。万一您不熟悉这些主题,那么应该在继续往下阅读本文之前,先去阅读介绍性的 Agavi 系列文章(参见 参 考资料 中的链接)。

本例中的示例应用程序是一个简单的书名和作者数据库。REST API 将允许第三方开发人员使用常规 REST 协定检索、添加、删除和更新该数据库中的书籍。本文主要假定为 XML 请求和响应主体;但是在末尾的一个小节中,也介绍了如何处理 JSON 请求和响应。

步骤 1:初始化一个新应用程序

作为开始,首先搭建一个简单的 Agavi 应用程序,它将充当本文开发目标的实验基地。使用 Agavi 构建脚本初始化一个新项目,除了以下突出显示的地方之外,都接受默认值。

shell> agavi project-wizard
Project name [New Agavi Project]: ExampleApp
Project prefix (used, for example, in the project base action) [Exampleapp]: ExampleApp
Should an Apache .htaccess file with rewrite rules be generated (y/n) [n]? y
...

 

完成之后,在 Apache 配置中为测试应用程序定义一个新的虚拟主机,比如 http://example.localhost/,然后将浏览器指向它。应该会看到默认的 Agavi 欢迎页面,如 图 1 所示。


图 1. 默认的 Agavi 欢迎页面
默认的 Agavi 欢迎页面的屏幕截图

步骤 2:添加一个新的模块和相应的操作

为简单起见,我假设您希望保护的所有操作都位于另一个不同于 Default 模块的模块中。回到命令提示符下,使用 Agavi 构建脚本创建新的 Books 模块,如下所示:

shell> agavi module-wizard
Module name: Books
Space-separated list of actions to create for Books: Index Book
Space-separated list of views to create for Index [Success]: Success
Space-separated list of views to create for Book [Success]: Success Error
...

 

这是一些您马上将向它们添加 REST 方法的操作。

此时,您也应该删除 Welcome 模块,就像 Agavi 文档所推荐的那样。

shell> rm -rf app/modules/Welcome
shell> rm -rf app/pub/welcome

 

步骤 3:更新应用程序路由表

接下来,利用对应于 前 一节 讨论的标准 REST 路由的路由更新应用程序的路由表($ROOT/app/config/routing.xml)。清单 1 中是必需的路由定义。


清单 1. REST 路由定义

				
<?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>

<!-- default action for "/" -->
<route name="index" pattern="^/$" module="Default" action="Index" />

<!-- REST-style routes -->
<route name="books" pattern="^/books" module="Books">
<route name=".index" pattern="^/$" action="Index" />
<route name=".book" pattern="^/(id:\d+)$" action="Book" />
</route>

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

 

现在应该能够用 清单 1 中的路由访问新创建的操作了。为了验证这一点,请尝试浏览到 http://example.localhost/books/ 并确认看到类似于 图 2 中的存根视图。


图 2. 一个 Agavi 存根 HTML 视图
一个 Agavi 存根 HTML 视图的屏幕截图,带有 Index 标题

步骤 4:初始化书籍数据库和模型

下一步是初始化应用程序数据库。所以,创建一个新的 MySQL 表来存放书籍记录,如下所示:

mysql> CREATE TABLE IF NOT EXISTS book (
-> id int(11) NOT NULL AUTO_INCREMENT,
-> title varchar(255) NOT NULL,
-> author varchar(255) NOT NULL,
-> PRIMARY KEY (`id`)
-> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.13 sec)

 

向表中添加一些记录,以便可以进行操作:

mysql> INSERT INTO `book` (`id`, `title`, `author`) 
-> VALUES (1, 'Wolf Hall', 'Hilary Mantel');
Query OK, 1 row affected (0.08 sec)

mysql> INSERT INTO `book` (`id`, `title`, `author`)
-> VALUES (2, 'Prayers for Rain', 'Dennis Lehane');
Query OK, 1 row affected (0.08 sec)

 

然后,下载 Doctrine 对象关系映射器(参见 参 考资料 中的链接)并将 Doctrine 库添加到 $ROOT/libs/doctrine。您也必需更新应用程序设置(在 $ROOT/app/config/settings.xml 中),以激活数据库支持,然后再更新数据库配置文件(通常在 $ROOT/app/config/databases.xml 中),以使用 Agavi 的 Doctrine 适配器。清单 2 中是一个示例配置:


清单 2. Doctrine ORM 配置

				

<?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://user:pass@localhost/example
</ae:parameter>
<ae:parameter name="load_models">%core.lib_dir%/model
</ae:parameter>
</database>
</databases>
</ae:configuration>
</ae:configurations>

 

此时,可以使用 Doctrine 为这些表生成模型。记住将结果模型类手动复制到 $ROOT/app/lib/model/ 目录。

shell> cp /tmp/models/Book.php app/lib/model/
shell> cp /tmp/models/generated/BaseBook.php app/lib/model/

 

有关利用 Agavi 迭代 Doctrine 的过程以及使用它来从数据库表生成模型,已在介绍性的 Agavi 系列文章的第 3 部分中详细讨论了(参见本文 参 考资料 中的链接)。

步骤 5:定义 XML 输出类型

默认情况下,Agavi 只配置为支持 HTML 输出。由于该示例 REST API 将初始地支持 XML,所以有必要定义这种输出类型,指定相关的响应头并将之标记为默认。为此,利用 清单 3 中的代码更新输出类型配置文件($ROOT/app/config/output_types.xml)。


清单 3. 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="xml">

<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>

 

万一您不能像上面这样让一切正常运转,请记住,以上步骤在介绍性的 Agavi 系列文章的第 1 部分(参见本文 参 考资料 中的链接)有更详细的描述。另外,可以从 下载 部分下载示例应用程序的完整代码归档文件。

通常的 REST API 必须支持两种类型的 GET 请求,一种针对一系列资源(GET /books/),另一种 针对某个特定的资源(GET /books/123)。使用 前 一节 中讨论的路由表,Agavi 会自动将这些请求分别路由到 Books_IndexAction::executeRead()Books_BookAction::executeRead() 方法。

IndexAction 的 executeRead() 方法应该用 200 (OK) 状态码和所有可用的书籍记录响应 GET /books/ 请求。清单 4 给出了代码的样子:


清单 4. GET 请求的 IndexAction 处理程序

				
<?php
class Books_IndexAction extends ExampleAppBooksBaseAction
{
public function executeRead(AgaviRequestDataHolder $rd)
{
$q = Doctrine_Query::create()
->from('Book b');
$result = $q->execute(array(), Doctrine::HYDRATE_ARRAY);
$this->setAttribute('result', $result);
return 'Success';
}
}
?>

 

清 单 4 使用 Doctrine 来执行一个查询,旨在得到书籍数据库表中的记录,将结果设置为 IndexSuccessView 的一个视图变量。下一步是将 executeXml() 方法添加到 IndexSuccessView,以将查询到的信息输出为 XML 文档。清单 5 展示了此代码:


清单 5. IndexSuccess XML 视图

				
<?php
class Books_IndexSuccessView extends ExampleAppBooksBaseView
{
public function executeXml(AgaviRequestDataHolder $rd)
{
$result = $this->getAttribute('result');
$dom = new DOMDocument('1.0', 'utf-8');
$root = $dom->createElement('result');
$dom->appendChild($root);
$xml = simplexml_import_dom($dom);
foreach ($result as $r) {
$item = $xml->addChild('item');
$item->addChild('id', $r['id']);
$item->addChild('author', $r['author']);
$item->addChild('title', $r['title']);
}
return $xml->asXml();
}
}
?>

 

这里没有特别复杂的东西。executeXml() 方法生成一个新的 DOM 文档,创建根元素,然后使用 SimpleXML 来构建 XML 树的其余部分,用 Doctrine 结果集中的信息填充它。

要看到实际效果,可使用 Web 浏览器来请求 URL http://example.localhost/books。最终得到 图 3 所示的结果。(查看 图 3 的文本版本。)


图 3. 针对所有书籍的 GET 请求的 XML 响应
针对所有书籍的 GET 请求的 XML 响应的屏幕截图

类似地,BookAction 的 executeRead() 方法应该用一个包含所请求书籍的详细信息的 XML 文档响应 GET /books/{id} 请求。清单 6 给出了代码:


清单 6. GET 请求的 BookAction 处理程序

				
<?php
class Books_BookAction extends ExampleAppBooksBaseAction
{
public function executeRead(AgaviRequestDataHolder $rd)
{
$q = Doctrine_Query::create()
->from('Book b')
->addWhere('id = ?', $rd->getParameter('id'));
$result = $q->execute(array(), Doctrine::HYDRATE_ARRAY);
if (count($result) == 0) {
return 'Error';
}
$this->setAttribute('result', $result);
return 'Success';
}
}
?>

 

清 单 7 中有相应的验证器定义,清单 8 中有相应的 BookSuccess 视图。


清单 7. BookAction 验证器

				
<?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%/Books/config/validators.xml"
>
<ae:configuration>
<validators>
<validator class="number">
<arguments>
<argument>id</argument>
</arguments>
<ae:parameters>
<ae:parameter name="required">true</ae:parameter>
<ae:parameter name="min">1</ae:parameter>
</ae:parameters>
</validator>
</validators>
</ae:configuration>
</ae:configurations>



清单 8. BookSuccess XML 视图

				
<?php
class Books_BookSuccessView extends ExampleAppBooksBaseView
{
public function executeXml(AgaviRequestDataHolder $rd)
{
$result = $this->getAttribute('result');
$dom = new DOMDocument('1.0', 'utf-8');
$root = $dom->createElement('result');
$dom->appendChild($root);
$xml = simplexml_import_dom($dom);
foreach ($result as $r) {
$item = $xml->addChild('item');
$item->addChild('id', $r['id']);
$item->addChild('author', $r['author']);
$item->addChild('title', $r['title']);
}
return $xml->asXml();
}
}
?>

 

要看到实际效果,可使用 Web 浏览器来请求 URL http://example.localhost/books/1。最终应该得到 图 4 所示的结果。(查看 图 4 的文本版本。)


图 4. 针对单个书籍的 GET 请求的 XML 响应
针对单个书籍的 GET 请求的 XML 响应的屏幕截图

万一指定的资源不可用,返回 404 (Not Found) 状态码是一个不错的主意。这很容易做到,只需将 BookErrorView 重定向到应用程序的默认 Error404SuccessView,并用会返回 404 消息主体的 executeXml() 方法更新该视图。清单 9 给出了代码。


清单 9. Error404Success XML 视图

				
<?php
class Default_Error404SuccessView extends ExampleAppDefaultBaseView
{
public function executeXml(AgaviRequestDataHolder $rd)
{
$this->getResponse()->setHttpStatusCode('404');
$dom = new DOMDocument('1.0', 'utf-8');
$root = $dom->createElement('error');
$dom->appendChild($root);
$message = $dom->createElement('message', '404 Not Found');
$root->appendChild($message);
return $dom->saveXml();
}
}
?>

处理 POST 请求稍微复杂一点。通常的 REST 协定是,POST 请求创建一个新资源,请求主体包含针对该资源的所有必要的输入(本例中是作者和书名)。现在,Agavi 可以自动读取 URL 编码的请求主体并将之转换为单个请求参数。然而,当请求主体包含 XML 文档时(跟 POST 和 PUT 请求一样),还需要额外的处理,以将 XML 数据转换成请求参数,从而适合于用在操作方法中。

清单 10 是此类 XML 文档的一个例子,表示新的书籍项:


清单 10. 一个表示新书籍项的 XML 文档

				
<book>
<title>The Da Vinci Code</title>
<author>Dan Brown</author>
</book>

 

达到此目的最简单的方式是创建 AgaviWebRequest 类的子类,以检查入站请求的 Content-Type 头,如果 Content-Type 头指出是 XML 请求主体,则在 XML 文档上执行必要的处理。清单 11 是此类子类的一个例子:


清单 11. 一个自定义的 HTTP 请求处理程序类

				
<?php
// credit: David Zuelke
class ExampleAppWebRequest extends AgaviWebRequest {
public function initialize(AgaviContext $context, array $parameters = array()) {
parent::initialize($context, $parameters);
$rd = $this->getRequestData();
if (stristr($rd->getHeader('Content-Type'), 'text/xml')) {
switch ($_SERVER['REQUEST_METHOD']) {
case 'PUT': {
$xml = $rd->removeFile('put_file')->getContents();
break;
}
case 'POST': {
$xml = $rd->removeFile('post_file')->getContents();
break;
}
default: {
$xml = '';
}
}
$rd->setParameters((array)simplexml_load_string($xml));
}
}
}
?>

 

PUT 和 POST 数据如果不是 URL 编码的,将被存储在请求的 put_filepost_file 文件变量中。清单 11 将该数据拖出请求并使用 SimpleXML 将之转换成一个对象,然后将这个对象强制类型转换为一个适合与 AgaviRequestDataHolder 的 setParameters() 方法一起使用的数组。现在可以利用 AgaviRequestDataHolder 的 getParameter() 方法从操作方法中以平常的方式访问该数据了。

可以将更新后的类定义保存到 $ROOT/app/lib/request/ExampleAppWebRequest.class.php,然后通过将它添加到 $ROOT/app/config/autoload.xml 而加载它。清单 12 给出了该文件增加的部分:


清单 12. Agavi autoloader 配置

				
<?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="ExampleAppBaseAction">
%core.lib_dir%/action/ExampleAppBaseAction.class.php</autoload>
<autoload name="ExampleAppBaseModel">
%core.lib_dir%/model/ExampleAppBaseModel.class.php</autoload>
<autoload name="ExampleAppBaseView">
%core.lib_dir%/view/ExampleAppBaseView.class.php</autoload>
<autoload name="ExampleAppWebRequest">
%core.lib_dir%/request/ExampleAppWebRequest.class.php</autoload>
<autoload name="Doctrine">
%core.app_dir%/../libs/doctrine/Doctrine.php</autoload>
</ae:configuration>
</ae:configurations>

 

这还没完。通常的 REST 协定是,POST 请求创建新资源,PUT 请求更新现有资源。然而,Agavi 的默认设置将 POST 请求映射到 executeWrite() 方法,将 PUT 请求映射到 executeCreate() 方法。这让 REST 用户有些混淆,因此,切换这些映射通常是一个好主意,只要更新 $ROOT/app/config/factories.xml 以反映新的映射即可。清单 13 给出了代码:


清单 13. HTTP 请求方法到 Agavi 操作方法的 Factory 重映射

				
<?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/factories/1.0">
<ae:configuration>
...

<request class="ExampleAppWebRequest">
<ae:parameter name="method_names">
<ae:parameter name="POST">create</ae:parameter>
<ae:parameter name="GET">read</ae:parameter>
<ae:parameter name="PUT">write</ae:parameter>
<ae:parameter name="DELETE">remove</ae:parameter>
</ae:parameter>
</request>
...
</ae:configuration>

 

一切就位后,现在可以定义 Books_IndexAction::executeCreate() 方法来处理 POST 请求了。清单 14 给出了代码。


清单 14. POST 请求的 IndexAction 处理程序

				
<?php
class Books_IndexAction extends ExampleAppBooksBaseAction
{
public function executeCreate(AgaviRequestDataHolder $rd)
{
$book = new Book;
$book->author = $rd->getParameter('author');
$book->title = $rd->getParameter('title');
$book->save();
$this->setAttribute('result', array($book->toArray()));
return 'Success';
}
}
?>

 

记住也更新操作验证器,以允许这些请求变量(清单 15):


清单 15. IndexAction 验证器

				
<?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%/Books/config/validators.xml"
>
<ae:configuration>

<validators method="create">
<validator class="string">
<arguments>
<argument>author</argument>
</arguments>
<ae:parameters>
<ae:parameter name="required">true</ae:parameter>
</ae:parameters>
</validator>
<validator class="string">
<arguments>
<argument>title</argument>
</arguments>
<ae:parameters>
<ae:parameter name="required">true</ae:parameter>
</ae:parameters>
</validator>
</validators>

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

 

REST 协定规定,对成功 POST 的响应应该在响应主体中包含 201 (Created) 状态码、位置头(指出新资源的 URL)和资源的表示。这一切只需对 IndexSuccessView 稍加修改就可做到,如 清单 16 所示。


清单 16. 修订后的 IndexSuccess XML 视图

				
<?php
class Books_IndexSuccessView extends ExampleAppBooksBaseView
{
public function executeXml(AgaviRequestDataHolder $rd)
{
$result = $this->getAttribute('result');
if ($this->getContext()->getRequest()->getMethod() == 'create') {
$this->getResponse()->setHttpStatusCode('201');
$this->getResponse()->setHttpHeader(
'Location',
$this->getContext()->getRouting()->gen(
'book',
array('id' => $result[0]['id']
)));
}
$dom = new DOMDocument('1.0', 'utf-8');
$root = $dom->createElement('result');
$dom->appendChild($root);
$xml = simplexml_import_dom($dom);
foreach ($result as $r) {
$item = $xml->addChild('item');
$item->addChild('id', $r['id']);
$item->addChild('author', $r['author']);
$item->addChild('title', $r['title']);
}
return $xml->asXml();
}
}
?>

正如前面所提到的,PUT 请求用于指出对现有资源的修改,并且同样在请求字符串中包含资源标识符。成功的 PUT 意味着,现有资源已被 PUT 请求主体中指定的资源替代。对成功 PUT 的响应要么是状态码 200 (OK),并且响应主体中包含已更新资源的表示,要么是状态码 204 (No Content),并且响应主体是空的。

清单 17 中是更新后的验证器定义:


清单 17. BookAction 验证器

				
<?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%/Books/config/validators.xml"
>
<ae:configuration>

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

<validators method="write">
<validator class="string">
<arguments>
<argument>author</argument>
</arguments>
<ae:parameters>
<ae:parameter name="required">true</ae:parameter>
</ae:parameters>
</validator>
<validator class="string">
<arguments>
<argument>title</argument>
</arguments>
<ae:parameters>
<ae:parameter name="required">true</ae:parameter>
</ae:parameters>
</validator>
</validators>

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

 

清单 18Books_BookAction::executeWrite() 操作方法的代码,该方法将处理 PUT 请求:


清单 18. PUT 请求的 BookAction 处理程序

				
<?php
class Books_BookAction extends ExampleAppBooksBaseAction
{
public function executeWrite(AgaviRequestDataHolder $rd)
{
$book = Doctrine::getTable('book')->find($rd->getParameter('id'));
if (!is_object($book)) {
return 'Error';
}
$book->author = $rd->getParameter('author');
$book->title = $rd->getParameter('title');
$book->save();
$this->setAttribute('result', array($book->toArray()));
return 'Success';
}
}
?>

 

类似地,executeRemove() 方法将处理 DELETE 请求,从数据仓库删除指定的资源。清单 19 显示了该方法的代码:


清单 19. DELETE 请求的 BookAction 处理程序

				
<?php
class Books_BookAction extends ExampleAppBooksBaseAction
{
public function executeRemove(AgaviRequestDataHolder $rd)
{
$q = Doctrine_Query::create()
->delete('Book')
->addWhere('id = ?', $rd->getParameter('id'));
$result = $q->execute();
$this->setAttribute('result', null);
return 'Success';
}
}
?>

 

对成功 DELETE 请求的响应可以是 200 (OK),状态包含在响应主体中,也可以是 204 (No Content),响应主体是空的。只要对 BookSuccessView 做一处更改,就很容易实现后一种情况,如 清单 20 所示:


清单 20. BookSuccess XML 视图

				
<?phpclass Books_BookSuccessView extends ExampleAppBooksBaseView
{
public function executeXml(AgaviRequestDataHolder $rd)
{
if ($this->getContext()->getRequest()->getMethod() == 'remove') {
$this->getResponse()->setHttpStatusCode('204');
return false;
}
$result = $this->getAttribute('result');
$dom = new DOMDocument('1.0', 'utf-8');
$root = $dom->createElement('result');
$dom->appendChild($root);
$xml = simplexml_import_dom($dom);
foreach ($result as $r) {
$item = $xml->addChild('item');
$item->addChild('id', $r['id']);
$item->addChild('author', $r['author']);
$item->addChild('title', $r['title']);
}
return $xml->asXml();
}
}
?>

前面几节演示了如何设置一个简单的基于 XML 的 REST API。但是,JSON 作为一种数据交换格式日渐流行,所以通常有必要在 REST API 中也支持 JSON 请求和响应主体。Agavi 灵活的输出类型使得这很容易处理。

步骤 1:激活 JSON 输出类型

首先,将 JSON 请求的处理程序添加到路由表,如 清单 21 所示:


清单 21. JSON 路由处理程序

				
<?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 JSON requests -->
<route name="json" pattern=".json$" cut="true" stop="false"
output_type="json" />
...
</routes>
</ae:configuration>
</ae:configurations>

 

该路由放置在路由表的顶端,匹配所有以 .json 后缀结尾的请求,并将它们设置为使用 JSON 输出类型。但是还不止这些:

  • cut 属性指出是否在继续之前从请求 URL 中删除匹配的子字符串片段。在本例中,它设置为 true, 所以一旦出现匹配,就从请求 URL 中删除 .json 后缀。
  • 路由定义中的 stop 属性指出,在第一个匹配之后路由处理是否应该继续。在本例中,它设置为 false, 以确保请求沿列表继续前进,直到匹配了请求 URL 的其余部分并调用了适当的操作。

该配置的实际效果是,Agavi 在接收到一个比如说针对 http://example.localhost/books/1.json 的请求时,它会检查路由表,找到一个与顶级万能路由的直接匹配。Agavi 然后从请求 URL 中去掉 .json 后缀,并将请求的输出类型设置为 JSON。它然后针对列出的路由继续检查请求的剩余部分 http://example.localhost/books/1,直到找到一个与 books.book 路由的匹配,并调用 BookAction。一旦 BookAction 完成,Agavi 将在视图中寻找 executeJson() 方法,执行该方法并将其输出返回客户端。

接下来,定义一种新的 JSON 输出类型(清单 22):


清单 22. JSON 输出类型定义

				
<?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="xml">
<output_type name="json">
<ae:parameter name="http_headers">
<ae:parameter name="Content-Type">application/json</ae:parameter>
</ae:parameter>
</output_type>
...
</output_types>
</ae:configuration>
</ae:configurations>

 

步骤 2:从 JSON Web 请求提取参数

跟 XML 一样,Agavi 不会自动将 JSON 信息包转换成请求参数。所以,更新自定义的 ExampleAppWebRequest 类来处理该任务,重点是 Content-Type 头,以及使用 PHP 的 json_decode() 函数将 JSON 值提取到一个 PHP 数组,该数组可被传递到 AgaviRequestDataHolder 的 setParameters() 方法。清 单 23 给出了代码:


清单 23. 修订后的 Web 请求处理程序

				
<?php
class ExampleAppWebRequest extends AgaviWebRequest {
public function initialize(AgaviContext $context, array $parameters = array()) {
parent::initialize($context, $parameters);
$rd = $this->getRequestData();
// handle XML requests
if (stristr($rd->getHeader('Content-Type'), 'text/xml')) {
switch ($_SERVER['REQUEST_METHOD']) {
case 'PUT': {
$xml = $rd->removeFile('put_file')->getContents();
break;
}
case 'POST': {
$xml = $rd->removeFile('post_file')->getContents();
break;
}
default: {
$xml = '';
}
}
$rd->setParameters((array)simplexml_load_string($xml));
}
// handle JSON requests
if (stristr($rd->getHeader('Content-Type'), 'application/json')) {
switch ($_SERVER['REQUEST_METHOD']) {
case 'PUT': {
$json = $rd->removeFile('put_file')->getContents();
break;
}
case 'POST': {
$json = $rd->removeFile('post_file')->getContents();
break;
}
default: {
$json = '{}';
}
}
$rd->setParameters(json_decode($json, true));
}
}
}
?>

 

步骤 3:更新应用程序视图

最后一步是更新各种应用程序视图以支持 JSON 输出。为此,将 executeJson() 方法附加到 IndexSuccessView(清单 24) 和 BookSuccessView(清单 25):


清单 24. IndexSuccess JSON 视图

				
<?php
class Books_IndexSuccessView extends ExampleAppBooksBaseView
{
public function executeJson(AgaviRequestDataHolder $rd)
{
$result = $this->getAttribute('result');
if ($this->getContext()->getRequest()->getMethod() == 'create') {
$this->getResponse()->setHttpStatusCode('201');
$this->getResponse()->setHttpHeader('Location',
$this->getContext()->getRouting()->gen(
'book',
array('id' => $result[0]['id']
)));
}
return json_encode($result);
}
}
?>



清单 25. BookSuccess JSON 视图

				
<?php
class Books_BookSuccessView extends ExampleAppBooksBaseView
{
public function executeJson(AgaviRequestDataHolder $rd)
{
if ($this->getContext()->getRequest()->getMethod() == 'remove') {
$this->getResponse()->setHttpStatusCode('204');
return false;
}
return json_encode($this->getAttribute('result'));
}
}
?>

 

要想看到实际效果,将 Web 浏览器指向 http://example.localhost/books/.json 或 http://example.localhost/books/1.json,应该会收到一个 JSON 编码的响应信息包,带有相应的数据。图 5 是该信息包的一个例子,跟 Firebug 调试器中看到的一样。(查看 图 5 的文本版本。)


图 5. 针对所有书籍的 GET 请求的 JSON 响应
针对所有书籍的 GET 请求的 JSON 响应的屏幕截图

一定要注意的是,上面描述的 JSON 支持很容易激活,只需在各个视图中做更改,操作代码保持不变。通过允许开发人员在视图中而不是在操作中决定应该如何处理不同的输出类型,Agavi 最小化了代码重复,却仍然遵守 MVC 原理和 DRY (Don't Repeat Yourself) 原则。

结束语

Agavi 为构建 REST API 提供一个简单易用的框架,使得应用程序开发人员可以使用一个轻量级的、直观的架构模式轻松地允许第三方访问应用程序函数。Agavi 对 REST 路由的内置支持,以及能够快速支持新的输出类型,使其适合于快速的 API 开发和部署。Agavi 的 MVC 实现也意味着您可以在实现阶段的任何时候,甚至在应用程序部署之后,向应用程序添加 REST API,对现有业务逻辑的影响却保持在最小限度。

下载 部分,下载本文中实现的所有代码,以及一个简单的基于 jQuery 的测试脚本,您可以使用该脚本在示例 API 上执行 GET、POST、PUT 和 DELETE 请求。我推荐您下载代码,开始做各种尝试,甚至自己动手向其中添加新内容。我保证您不会出任何问题,对学习无疑是有帮助的。祝您开心!

加载中
返回顶部
顶部