RESTful API 初体验
阅读了阮一峰的 理解 RESTful 架构 和 RESTful API 设计指南,对一些关键的概念做一个简单的备忘。
RESTful 概念理解
REST,全称 “Representational State Transfer”,阮翻译成 “表现层状态转化”,补全主语就是 “资源表现层状态转化”。主要特性:功能强、性能好、适宜通信。符合这样规范的 API 架构就可以称为 RESTful API。
资源是网络服务(HTTP)的主体,表现层是资源的存在方式和表现形式,状态变化是网络服务对资源的影响。
常见的表示 http 操作方式的动词有四种:GET、POST、PUT、DELETE。GET 用来获取资源,POST 用来新建资源(也可以用于更新资源),PUT 用来更新资源,DELETE 用来删除资源。
常见的 RESTful 设计误区
URI 中包含动词。RESTful 规范将动作放到请求类型中表示,所以 URI 中应该只包含资源信息。
可能之前的请求是这样的:
# 文章列表第一页
/api/article/list/1
# 文章编号为 1 的文章
/api/article/view/1
# 修改文章编号为 1 的文章
/api/article/update/1
# 删除文章编号为 1 的文章
/api/article/delete/1
这是请求 URI 经过(.htaccess 或者框架)改写之后的形式,最原始的请求可能直接就在 GET 中加几个参数,m
表示模块,c
表示控制器,a
表示方法。
RESTful 规范的 URI 应该是这样的:
# 文章列表第一页
/api/article ?page=1 GET
# 文章编号为 1 的文章
/api/article/1 GET
# 修改文章编号为 1 的文章
/api/article/1 PUT
# 删除文章编号为 1 的文章
/api/article/1 DELETE
php 框架在处理 uri 时会提供一个 pathinfo 模式,从服务端几个变量中获取 pathinfo,并自动解析处理 URI 中的参数。
URI 中包含版本号。这个我感觉跟 RESTful 规范并没有冲突,v2 版本的资源和 v1 版本的资源可能在 “状态转化” 方面是有所不同的。从这个角度考虑,可以认为这是两种不同的服务。且添加了版本号的 URI 比较直观。如果觉得版本号冗余也可以将其添加到 header 的 accept 中。
我在处理多个版本的 API 时,小版本的修改直接做代码兼容,不去定义版本号;大的变动则直接做一个新的 git 版本分支,然后去解析一个新的二级域名,指向新的 API,客户端只需要修改请求 API 地址即可。
简单的无框架 RESTful API 实现
使用的是菜鸟教程中的 PHP RESTful,测试时发现一些小的 bug,做了一点改动。
主要有这些文件:RestController.php
、SimpleRest.php
、Site
、SiteRestHandler.php
以及 .htaccess
。
因为是无框架,所以需要改写请求 URI。
.htaccess
RewriteEngine On
# URI rewrite
RewriteRule ^site/list/?$ /api/restful/RestController.php?view=all [nc,qsa]
RewriteRule ^site/list/([0-9]+)/?$ /api/restful/RestController.php?view=single&id=$1 [nc,qsa]
.htaccess
一般只作用于 Apache 服务器。
控制器 RestController.php
require_once("SiteRestHandler.php");
$view = "";
if (isset($_GET["view"]))
$view = $_GET["view"];
/**
* RESTful service 控制器
* URL 映射
*/
switch ($view) {
case "all":
// 处理 REST Url /site/list/
$siteRestHandler = new SiteRestHandler();
$siteRestHandler->getAllSites();
break;
case "single":
// 处理 REST Url /site/show/<id>/
$siteRestHandler = new SiteRestHandler();
$siteRestHandler->getSite($_GET["id"]);
break;
case "":
// 404 - not found
break;
default:
break;
}
逻辑处理层 SiteRestHandler.php
require_once("SimpleRest.php");
require_once("Site.php");
class SiteRestHandler extends SimpleRest {
function getAllSites() {
$site = new Site();
$rawData = $site->getAllSite();
if(empty($rawData)) {
$statusCode = 404;
$rawData = array('error' => 'No sites found!');
} else {
$statusCode = 200;
}
$contentType = $this->getContentType($_SERVER['HTTP_ACCEPT']);
$this -> setHttpHeaders($contentType, $statusCode);
if(strpos($contentType,'application/json') !== false){
$response = $this->encodeJson($rawData);
echo $response;
} else if(strpos($contentType,'text/html') !== false){
$response = $this->encodeHtml($rawData);
echo $response;
} else if(strpos($contentType,'application/xml') !== false){
$response = $this->encodeXml($rawData);
echo $response;
}
}
public function encodeHtml($responseData) {
$htmlResponse = "<table border='1'>";
foreach($responseData as $key=>$value) {
$htmlResponse .= "<tr><td>". $key. "</td><td>". $value. "</td></tr>";
}
$htmlResponse .= "</table>";
return $htmlResponse;
}
public function encodeJson($responseData) {
$jsonResponse = json_encode($responseData);
return $jsonResponse;
}
public function encodeXml($responseData) {
// 创建 SimpleXMLElement 对象
$xml = new SimpleXMLElement('<?xml version="1.0"?><site></site>');
foreach($responseData as $key=>$value) {
$xml->addChild($key, $value);
}
return $xml->asXML();
}
public function getSite($id) {
$site = new Site();
$rawData = $site->getSite($id);
if(empty($rawData)) {
$statusCode = 404;
$rawData = array('error' => 'No sites found!');
} else {
$statusCode = 200;
}
$contentType = $this->getContentType($_SERVER['HTTP_ACCEPT']);
$this -> setHttpHeaders($contentType, $statusCode);
if(strpos($contentType,'application/json') !== false){
$response = $this->encodeJson($rawData);
echo $response;
} else if(strpos($contentType,'text/html') !== false){
$response = $this->encodeHtml($rawData);
echo $response;
} else if(strpos($contentType,'application/xml') !== false){
$response = $this->encodeXml($rawData);
echo $response;
}
}
}
逻辑处理层基础类 SimpleRest.php
class SimpleRest {
private $httpVersion = "HTTP/1.1";
public function setHttpHeaders($contentType, $statusCode) {
$statusMessage = $this->getHttpStatusMessage($statusCode);
header($this->httpVersion . " " . $statusCode . " " . $statusMessage);
header("Content-Type:" . $contentType);
}
public function getContentType($requestContentType, $selectedContentType = '') {
$contentTypes = array('application/json','text/html','application/xml');
if ($selectedContentType && in_array($selectedContentType, $contentTypes)) return $selectedContentType;
$defaultContentType = $contentTypes[0];
foreach ($contentTypes as $contentType) {
if (strpos($requestContentType, $contentType) !== false) {
$defaultContentType = $contentType;
break;
}
}
return $defaultContentType;
}
public function getHttpStatusMessage($statusCode){
$httpStatus = array(
100 => 'Continue',
101 => 'Switching Protocols',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
306 => '(Unused)',
307 => 'Temporary Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested Range Not Satisfiable',
417 => 'Expectation Failed',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported'
);
return isset($httpStatus[$statusCode]) ? $httpStatus[$statusCode] : $httpStatus[500];
}
}
数据层 - 模型 site.php
/*
* 菜鸟教程 RESTful 演示实例
* RESTful 服务类
*/
class Site {
private $sites = array(
1 => 'TaoBao',
2 => 'Google',
3 => 'Runoob',
4 => 'Baidu',
5 => 'Weibo',
6 => 'Sina',
7 => '海滨擎蟹'
);
public function getAllSite(){
return $this->sites;
}
public function getSite($id) {
return isset($this->sites[$id]) ? array($id => $this->sites[$id]) : array();
}
}
因为展示内容比较简单,且 API 一般只返回数据,所以不需要视图层。
框架一般不需要通过改写去设置路由,而是有独立的路由设置,tp 中路由定义 可以单条注册,亦可以数组形式批量注册路由规则,yii2、Laravel 都有类似的用法,其中 Laravel 甚至可以 Route::resource()
定义所有的 RESTful 规范的路由。