卷二介绍
在今天,web开发的重点已经从躲在防火墙后的孤立系统走向各个应用间的互联互通,彼此之间能实时通信。CI的可扩展让它能非常容易地创建API,并允许你的应用分享数据和功能。
本书中,你会了解到现代RESTful API设计背后的原理以及一些有用的实施细节,比如怎样版本控制,怎样使用HTTP定义内容类型以及如何自己去扩展和调试。
CI是轻量,快速,可塑性强,是PHP中建立可扩展高效率API的神器。
PART1
这部分中,我们将讨论设计和创建API的理论方面。我们将研究基于REST平台设计的资源原则。我们会了解HTTP规范,了解怎样能将API设计的合理、可预见,我们还会看一下幂等( idempotence)的重要性。
在卷一中我们讨论了REST基础,资源以及HTTP方法。本章的一些内容你看来或许似曾相识,尤其是如果你已经看过了卷一的话。如果你自信了解HTTP方法和HTTP协议可以跳过这些。
Web浏览器和web开发者通常只熟悉两个主要的HTTP方法,GET和POST。如果你深入到HTTP规范中,会发现只有2/9的方法可以用在HTTP客户端中:
GET 用以接收资源
POST 用以创建新的资源
PUT 和 PATCH 用以更新存在的资源
DELETE 用以删除资源
HEAD 与GET相似,但只返回资源的头
OPTIONS 返回URL的相关信息,请求的参数,返回的格式,可用的其他请求。
TRACE 用以回显请求,通常用于调试。
CONNECT 将请求转为socket,对于代理而言特别有用。
GET,HEAD,TRACE和OPTION是HTTP种的四种安全方法。意思是说,他们不会引起副作用,仅仅从服务器检索信息而不修改任何数据或者改变数据状态。在实践中,可能你要做的都是一些没有副作用的操作,如记录日志,缓存或者处理分析。
对应的,POST,PUT,DELETE,CONNECT和PATCH就是不安全的,是意图修改服务器端状态(或其他服务器,如发送电子邮件或外部API请求)。这些方法执行时可能都需要上下文,例如,让用户填写表单。
PUT和DELETE也被定义为幂等性,即多个相同的请求只影响服务器一次,因此具有和单一请求相同的效果。其结果可能有所不同,但每次服务器的状态是相同的。
POST不是幂等的,所以,在多次提交请求时,它有多种效果。这会存在一些问题,拿支付处理来举例,提交多个请求会多次收取用户信用卡里的钱。这就要解决这个问题,以确保用户一次不能提交多个请求。
HTTP是无状态的协议,就是说,有关请求的东西,服务器不会去记住。请求可以在任意时间任意地方建立,但服务器不会去跟踪他们。为解决这个问题并方便用户进行认证、分析等等,我们可以使用Cookie、服务器端会话或者访问令牌。
这些关于HTTP的信息似乎有些多,但理解这些简单的概念对于设计REST风格的API而言非常有价值。
资源:Resources
在REST风格的API中,资源很简单:一个元素,我们能访问和操纵的一些数据。
一个资源可能会是一本书,一个用户,一个分类,一片博文,列表中的一个条目或者是用于其他资源的一个操作。如果你从资源角度考虑整个应用程序,你可以使路由和资源间的关系更加精准和一致。
本书我们会创建一个用于跟踪统计网站的API。我们能任意创建跟踪器,这些跟踪器包含着不同的值。我们可以写一个前端程序将这些数据可视化。
下面是我们要做的清单:
创建一个新的跟踪器;
在跟踪器内创建新值;
在指定跟踪器内显示所有数值;
更新跟踪器的名称;
删除跟踪器
如何能将这些需求翻译成资源呢?非常简单!
我们拥有一个跟踪器资源,它包含了我们的跟踪器的信息。一个跟踪器可以有多个值的资源。对简单的应用来说这挺好的,但怎样能让这些不那么明显?
拿验证来说,当你登录到一个网站或应用程序中,其实是创建了一个新的session资源。只是看起来不那么明显,但你可以很好地套用这个逻辑。一个重置密码的资源是另一个很好的例子,他可能都不是由数据库支持的,但你还是能创建一个资源。
RESTful路由
提到RESTful路由有些时候了,但它们到底是啥?
REpresentational State Transfer (REST)是构建web服务/API的一种方式。我们很好的了解了HTTP方法及其用处,可以开始用REST结构设计我们的路由了。
REST风格架构的理念是,每个资源有一套基于URI端点和用于请求HTTP动作的常规路由,这些路由是可以重复的,通用的。
标准的RESTful路由被分成两组。
看一下我们为Tracker资源准备的路由。有一个约定是,控制器+路由为复数。读过卷一的人都知道我有多么喜欢这个约定。
关键点:
上面列出了一整组方法,但在现实中,很少需要你一一实现他们。主要实现的方法有下面五个:
• GET /posts
• GET /posts/:id
• PUT /posts/:id
• DELETE /posts/:id
其他的,比如替换/删除整个数据集或者由特定会员创建新的数据集都很少使用。
嵌套
我们前面说过,跟踪器资源可以有多个值,这种关系类型很容易表达为URI,当我们要从跟踪器中查找一个值时,理论上只需跟指定的跟踪器上下文中查询。
http://example.com/trackers/:id/values
/tracker/:id/values 是一个REST风格的URL,我们能够用POST创建新的值并GET到数据集。
我用一个经典的博客系统举例,一个post有多个评论资源,每个评论可以有一个作者资源。
http://example.com/posts/43/comments/1222/author
嵌套可以是无限的。它只是一个你用来直接访问资源的聪明办法。它也非常非常整洁:包含一个id作为$_GET参数或者作为资源本身的一部分。
单数的Resources
有时你只有其中一个资源,也就是说,你不能通过id标识符检索。比如Author,一个Comment永远有一个Author,·[A Comment will only ever have one Author; we won't ever need to look up a comment's authors or select a specific one]
PART2
开始写API之前,我想先提一下PhilSturgeon优秀的CI REST类库。这个库使用简单功能丰富。我已经用它做过大量不同的项目,其代码经得起考验。它包含了一些复杂的功能,如API密钥,登录日志和基本的验证。
那么,为什么我们还要从头建立我们自己的系统呢?有两个原因。
首先,我想要在更高层次的控制核心处理以及请求到响应的周期。在本章末尾我们会学习错误处理时,比如,我们需要对我们的代码做一些合理的变化,用Phil的库就不行。用自己的代码在实现特定REST风格路由和API版本会更加容易。Phil的库过时了,同时我们的系统重新考虑了许多更新的方法。
其次,如果我们自己来写会学到这些事情,从头开始构建系统,可以真正掌握API开发的核心概念。最终,我们将构建一个相当抽象的系统,希望你作为解决方案拖到多个项目中。
路由
基于HTTP方法和URI端点,我们已经看到了一些REST路由理论并映射到资源。CI的路由机制很低级,也不支持基于HTTP方法的路由,那么,我们如何实现呢?
我写过一个聪明的路由库用以简化HTTP和REST风格路由,叫Pigeon 。Pigeon是一个智能库,能轻松地REST风格化路由,嵌套以及其他复杂的路由任务。使用Pigeon,整个路由结构像这样:
Pigeon::map(function($r))
{
$r->resources('trackers',function($r))
{
$r->resources('values');
});
});
$route = Pigeon::draw();
我们从头开始。
首先,映射基本的GET请求:
$route['trackers'] = 'trackers/index';
$route['trackers/(:any)'] = 'trackers/show/$1';
我们直接将/trackers映射到Trackers::index,/trackers/something映射到Trackers::show。问题马上出现了,这不是基于HTTP动作的路由。再来一次,这次使用switch语句基于方法改变路由:
$route = array();
switch (strtoupper($SERVER['REQUESTMETHOD']))
{
case 'GET':
$route['trackers'] = 'trackers/index';
$route['trackers/(:any)'] = 'trackers/show/$1';
break;
}
好极了。我们来扩展switch语句让它支持POST方法
case 'POST':
$route['trackers'] = 'trackers/create';
break;
好的很,现在加上PUT和DELETE
case 'PUT':
$route['trackers/(:any)'] = 'trackers/update/$1';
break;
case 'DELETE':
$route['trackers/(:any)'] = 'trackers/delete/$1';
break;
扩展一下让它支持嵌套的数值资源也很容易--只需要插入嵌套的资源并保证嵌套在URI中即可,比如POST:
$route['trackers/(:any)/values'] = 'values/create/$1';
完整的switch语句如下:
用这种时尚的CI路由另一个好处是,路由只在被请求是定义,如果有人试图请求外部路由,CI会抛出404页面。
可以将这个模式抽象到一个方法中,以便重复使用,或者向上面提的,你可以使用Pigeon。或者,可以简单地复制粘贴这些代码到所有资源中。我们的应用很简单,所以,我们已经写了所有需要的路由。
参数
你可能发现了另一个问题:我们路由基于HTTP方法之上,我们使用着新的方法像PUT和DELETE。如何通过这些方法访问用户的输入呢?
我们需要在application/core目录下创建一个MY_Controller.php文件,CI将为我们自动加载,所以我们可以扩展它然后用我们的API加工。非常像卷一中MY_Model和MY_Controller的创建。
class MY_controller extends CI_Controller
{
}
我们准备用这个控制器的构造函数解析我们从用户那里获取的参数。We'll populate a $this->params array, so we don't have to care about the HTTP method by the time we reach the controller.
public $params = array();
public function __constructor()
{
parent::__construct();
}
这里,我们可以检查请求的方法,并根据请求主体获得正确的参数,而不是穿梭于整个JSON文档中。我们做个假设:我们从API中收到的每个请求的主体都是application/x-www-form-urlencoded-式的类型,和我们提交一个表单一样。
这种假设表明,原始请求主体会是一个可解析的“查询字符串”。依此假设,我们可以写一个合理的通用请求解析器。如果是GET或者POST请求,那好极了,我们已经完成了。如果还有其他的,可以用PHP的parse_str函数:
获取HTTP方法:
$method = strtoupper(_$SERVER['REQUEST_METHOD'])
我们的通用方法:
if ($method=='GET')
{
$this->params = $_GET;
}
elseif ($method=='POST')
{
$this->param = $_POST;
}
获得原始请求主体,我们可以访问php://input,这是一个我们可以读取到的可以排序的文件句柄。
else
{
parse_str(file_get_contents('php://input')),
$this->params);
}
现在做完了。当请求通过控制器路由时,我们会有一个用HTTP方法反馈填充的数组。
响应
我们通过控制器路由请求,现在考虑如何响应给用户。
我们的API需要响应计算机能解析的数据格式。有集中用来传输数据的流行序列化格式:XML,JSON,Yaml甚至是序列化的php。我选用JSON。JSON让人易读,大多数的语言/框架都有快速的原生解析库。
为了让事情变得简单,本书提供了JSON序列化的PHP(两者都需要调用一个PHP函数)
此外,有谁要用XML呢?
你或许发现了URL用一个扩展名(resources/1.json)指定用户响应类型,抑或通过一个查询字符参数(resources/1?format=json)。两者都是很好的解决问题的办法,但也都不是真正通过HTTP规范的。
一个更好些的方法是使用HTTP Access头。