music unfamous original game design efficient software wtf
life ui algorithm fix programming

利用 ThinkPHP3.2 开发 REST API 接口

作者:trinity  PHP    2015-9-28  标签:  programming 

一,背景

    项目中部署一组数据库提供查询,用户接口有桌面版,Web版,手机版,还有一个后台服务。这是很常见的场景。都需要直接或间接的访问数据库,此时用REST 来开发一个访问 API 算是中规中矩的做法。

    因为一直用 ThinkPHP3.2 来做 Web,所以这次也用它来做 。

    题外话:PHP 的框架有很多,Yii,Symfony,CI,SlimPHP,eagerPHP,Phalcon yaf,Laravel,speedPHP,CakePHP... 用ThinkPHP 的原因在于它的集成度高,社区开发的支持包“极具中国特色”。

    1,什么是 REST

        原始论文    维基百科

    2,关于 ThinkPHP3.2

         ThinkPHP3.2 文档

    3,测试工具

        curl command

        chrome 插件 Postman

二,ThinkPHP3.2 对于 REST 的支持

    在 ThinkPHP3.2 中多了一个 RestController 类,继承自普通的 Controller,里面简单实现了几个函数,定义了几个成员。这也正说明了 REST 不复杂。其实典型的 ThinkPHP3.2 实现 REST 有一个网友写了篇博客,这也和官方文档的介绍差不太多。

    可以非常明显的看到 ThinkPHP3.2 的对 REST 的支持的 URI ,GET 操作:

 GET /Home/Info/read/id/1.json

    此时会调用 InfoController:RestController 的 read_get_json() 函数。

    这能运行,而且某种程度上表达了逻辑,再来看 DELETE :

DELETE /Home/Info/read/id/1.json

    略显浮夸,如果追求完美,或者说“REST 设计” ,那么 read 在这里无疑是多余的,DELETE,read,你到底要干什么?URI 里再包含一个动词会变得难以理解。就不说后面丑陋的 1.json 了,这用于表示请求的扩展名是 json,好映射到 read_get_json(),而不是 read_get_html()。

    那为什么非要来一个 read 呢?当删除了 read,比如 GET 请求:

GET /Home/Info  //标准 REST,返回全部 Info

    这时候,映射到 Index() Action,好了,回到常见的应用场景了,可见,read 更像一个占位符,用于兼容非 REST 模式。

三,ThinkPHP3.2 实现 REST

    解决办法也很简单,当然最好不要动源代码,实现一个 REST Controller,叫ManagerController:RestController,实现一个 Action :user_get_json()

namespace Api\Controller;
use Think\Controller\RestController;
class ManagerController extends RestController {
	protected $allowType=array("json");
    protected $defaultType="json";

    public function user_get_json(){
    	echo 'hello '.I('get.id');
    }
    public function user_post_json(){
    	echo 'post  '.J();
    }
    public function user_put_json(){
    	echo 'put '.J();
    }
}

    其中的 J() 在 common/function.php 中定义,用于返回 body 中的 json 字符串。

function J(){
	return file_get_contents("php://input");
}
     此时的 URL ,GET 请求。
GET /Api/Manager/user/id/1 

    用 curl 测试 ManagerController::user_get_json() [GET 方法]

curl -XGET -H "Accept:Application/json" http://www.pinruan.net/Api/v1/Manager/user/id/1

    可以看到输出正确。重点是这个 URI 看上去要比文档上介绍的要“标准”多了,而且没有做任何路由规则。

hello 1

    再测试 POST 方法:

curl -XPOST -H "Accept:Application/json" http://www.pinruan.net/Api/v1/Manager/user -d "{"id":2,"name":"Trinity"}"

    其他 PUT,DELETE 类似。

四,系统升级的前后兼容

    考虑Web 和 后台服务 部署在服务器,很容易就升级了,但手机版装在别人的手机里,你不能保证他们每次跟着你升级。这需要每次发布都设置 API 版本号。在后台,现在考虑到的是使用路由规则。

    比如,现在我们一切开发好了,API URI 是:

/Api/Info...

     在 Conf/config.php 中添加

'URL_ROUTER_ON'=>true,
'URL_ROUTE_RULES'=>array(
'v1/Manager/user'=>'Manager/user'
)

    实际部署的 URI 为

/Api/v1/Info....

    以后升级,再添加条路由比如 /Api/v2/Info 就可以了。

五,各个客户端 API 的隔离

    出于某种目的,有时候会需要让手机,Web 通过不同的路径访问 API,这个实现手法有很多(ThinkPHP3.2),可以多模块,可以定义多路由。或者直接就是不同的函数(action)。

六,弊端

    用 ManagerController 这种妥协的做法不是很得当,如果有 users,posts,comments,privilege....都属于 Manager 模块,那是不是都在 ManagerController 类里写代码?如果再开一个类是不是又有点混乱了?

    另外,看了 SlimPHP 的方式,特别 Straightforward,在 ThinkPHP3.2 里,“路由-闭包支持”这一节里,也有类似的实现:

'blog/:year/:month' =>
      function($year,$month){
          echo 'year='.$year.'&month='.$month;
      }

    当然底层机制可能不大一样,我怀疑 ThinkPHP 这种方式是不是不能完整支持各种请求类型,未测试。