▪ 前言
做 API 接口时,最土的办法就是按常规 PHP HTML 的写法直接输出 JSON 数据,规则规律完全自己把握。虽然它能很好运行,但是总觉得其不太规范。
好在 Yii2 框架中集成了 Restful API,利用其写 API 接口就像当初学习使用 MVC 写代码一样,还是非常值得一试的。
▪ 概述
在正式开始之前,我们先大概了解下完成一个 RESTful API 需要有哪些工作:
- Apache 或 Nginx 中开启 Rewrite 伪静态模式。
- 在 Yii2 中新建 api 应用主体,专门用来存放 api 相关文件,并配置响应格式为 JSON。
- 改造请求错误提示为 JSON 格式,而非 HTML。
- 测试调试请求。
- 前端进行接收(这部分我们说下前端 Ajax 如何跨域做出请求)。
▪ 准备
1. 新建数据表
新建 admin
表用来开发(也可以用命令行 migrate 去建表):
DROP TABLE IF EXISTS `admin`;
CREATE TABLE IF NOT EXISTS `admin` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `admin` VALUES ('1', '我是一');
INSERT INTO `admin` VALUES ('2', '我是二');
INSERT INTO `admin` VALUES ('3', '我是三');
INSERT INTO `admin` VALUES ('4', '我是四');
2. API 的请求工具
Chrome 中建议安装 postman、Firefox中安装 HttpRequest,其他浏览器自行解决。
由于浏览器被墙,所以推荐使用命令模式下 curl 命令,使用方式参考《Windows 下 curl 的安装和简单使用》
3. JSON 的查看工具
chrome 中建议安装 JSONView,Firefox 中安装 jsonformater,其他浏览器自行解决;
4. 伪静态
开启伪静态 Rewrite、并且配置 api 的二级域名为 api.domain.com
格式,绑定在 api/web
下面。
▪ api 应用主体的建立
- 在项目的根目录下新建名为 api 的文件夹,并在
common/config/bootstrap.php
文件中添加 api 模块的别名:
Yii::setAlias('@api', dirname(dirname(__DIR__)).'/api');
- 拷贝 backend/config 文件夹到 api 的根目录下,并修改配置文件
api/config/main.php
,内容如下:
<?php
$params = array_merge(
require(__DIR__ . '/../../common/config/params.php'),
require(__DIR__ . '/../../common/config/params-local.php'),
require(__DIR__ . '/params.php'),
require(__DIR__ . '/params-local.php')
);
// 初始
$configs = array();
$configs['id'] = 'APP-API';
$configs['params'] = $params;
$configs['basePath'] = dirname(__DIR__);
$configs['bootstrap'] = array('log');
$configs['modules'] = array();
$configs['modules']['v1'] = array('class'=>'api\modules\v1\Module');
$configs['modules']['v2'] = array('class'=>'api\modules\v2\Module');
$configs['components'] = array();
$configs['components']['errorHandler'] = array('errorAction'=>'v1/index/error');
// 用户
$configs['components']['user'] = array();
$configs['components']['user']['loginUrl'] = null;
$configs['components']['user']['identityClass'] = 'common\models\User';
$configs['components']['user']['identityCookie'] = array('name'=>'_identity-api', 'httpOnly'=>true);
$configs['components']['user']['enableSession'] = false;
$configs['components']['user']['enableAutoLogin'] = false;
// 返回
return $configs;
-
新建 api/modules/v1 和 api/modules/v2 文件夹作为我们不同版本的内容,并在各自的文件夹下新建
controllers
、models
、views
目录。 -
新建 api/modules/v1/Config.php 文件(以
v1
为例),内容如下:
<?php
$configs['components'] = array();
$configs['components']['request'] = array('enableCsrfValidation'=>false);
$configs['components']['errorHandler'] = array('errorAction'=>'v1/index/error');
// 返回
return $configs;
- 新建 api/modules/v1/Module.php 文件(以
v1
为例),内容如下:
<?php
namespace api\modules\v1;
use Yii;
/**
* 子模块类
*/
class Module extends \yii\base\Module
{
public function init()
{
// 初始化
parent::init();
$module = $this->id;
// 初始模块配置
$config = require(__DIR__.'/Config.php');
$components = Yii::$app->getComponents();
foreach( $config['components'] AS $k=>$component ){
if( isset($component['class']) && isset($components[$k]) == false ) continue;
$config['components'][$k] = array_merge($components[$k], $component);
}
Yii::configure(Yii::$app, $config);
// 组件重先注册
// 系统核心的几个组件在初始化应用时就创建了对象,而子模块的配置在这之后才被应用
// 所以就导致了这几个核心组件无法根据子模块的配置进行构建,此处将这几个核心组件进行重构
Yii::$app->getErrorHandler()->unregister();
Yii::$app->getErrorHandler()->register();
}
}
- 新建 common/models/Admin.php 模型文件,内容如下:
<?php
namespace common\models;
use Yii;
use yii\db\ActiveRecord;
use yii\data\Pagination;
/**
* 管理员模型
*
* @author DaiYangFan <dmlk31@163.com>
*/
class Admin extends ActiveRecord
{
/**
* Define rules for validation
*/
public function rules()
{
return array(
array(['name'], 'required')
);
}
// ...
}
Admin
模型类中必须重载rules()
来指明必填的字段名称,否则 RESTful API 的内置接口admin/create
和admin/update
将无法使用
- 新建 api/modules/v1/controllers/AdminController.php 控制器用来响应,并重写控制器的行为函数
behaviors()
响应为 JSON:
<?php
namespace api\modules\v1\controllers;
use Yii;
use yii\rest\ActiveController;
class AdminController extends ActiveController
{
// 控制器对应的模型类
// 指定模型类后 Yii2 能自动为该控制器实现 admin/index, admin/view, admin/create, admin/update, admin/delete 等 ActiveController::actions() 里预设的方法
public $modelClass = 'common\models\Admin';
/**
* 绑定控制器行为
*
* @return array
*/
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['contentNegotiator']['formats']['text/html'] = Response::FORMAT_JSON;
$behaviors['contentNegotiator']['formats']['application/json'] = Response::FORMAT_JSON;
return $behaviors;
}
}
common\models\Admin
为管理员模型类,如有问题请百度一下。
-
拷贝 backend/web 文件夹到 api 的根目录下。
-
拷贝 enviroments/dev/backend 并重命名为 enviroments/dev/api,拷贝 enviroments/prod/backend 并重命名为 enviroments/prod/api。
enviroments
里面存放的主要就是一些基础配置的模板文件,供 Yii2 初始化环境变量使用(执行./init
用),具体参考《Yii2 初始化安装》
- 以上我们并快速的完成了一个 api 应用的建立,可以在浏览器中通过一下链接进行访问测试:
# admin list
http://api.domain.com/index.php?r=v1/admin
http://api.domain.com/index.php?r=v1/admin/index
# admin view
http://api.lengdo.com/index.php?r=v1/admin/view&id=2
当然,在浏览器链接的这种方式下(GET 方式),我们只能访问 admin/index
和 admin/view
,如果想操作其他接口,那么就需要使用 API请求工具 了,这里我们以 curl 方式为例:
# 删除 ID 为 2 的管理员
curl -X DELETE "http://api.domain.com/index.php?r=v1/admin/delete&id=2"
# 新增管理员
curl --data "name=xiaosa" "http://api.domain.com/index.php?r=v1/admin/create"
# 更新管理员
curl --data "name=xiaow" "http://api.domain.com/index.php?r=v1/admin/update&id=1" -X PUT
▪ 设置请求错误提示为 JSON
在这个 api 应用中,当出现内部错误时(比如访问的URL没有对应控制器等),系统默认是返回 HTML 错误内容,很明显这是不合适的。所以我们需要将错误内容格式改为 JSON。
在上面的配置中,我们曾在 api/modules/v1/Config.php 中这样设置过:
<?php
// ...
$configs['components']['errorHandler'] = array('errorAction'=>'v1/index/error');
// ...
如果 URL 访问的不是 v1 或 v2
子模块时,其下面的 Config.php 将不起作用,错误处理将使用 Yii2 内置逻辑并返回 HTML 错误内容。所以在api/config/main.php
中也有这样一个错误处理配置,这个配置的作用是当你的 URL 访问的不是 v1 或 v2
子模块时,将默认使用 v1 子模块的错误处理函数返回 JSON 错误内容。
这个配置就是告诉 Yii2 当出现内部错误时那么从 v1 模块->index 控制器->error方法 获取错误内容并返回到客户端。
所以很简单,我们只需要在 IndexController
控制器的 actionError()
里将错误数据格式化为 JSON 即可:
<?php
namespace api\modules\v1\controllers;
use Yii;
use yii\web\HttpException;
use yii\base\Controller;
/**
* 控制器
*
* @author DaiYangFan <dmlk31@163.com>
*/
class IndexController extends Controller
{
// ...
/**
* 错误处理
* 模块所有的错误都将传递到该方法进行处理,可以在每个模块的配置文件 Config.php 中修改
*
* 更多详细的 Yii2 的错误处理请参考文章 《Yii2 之错误处理深入分析》
*
* @return string
*/
public function actionError(){
// 初始化
$exception = Yii::$app->getErrorHandler()->exception;
if( $exception === null ) $exception = new HttpException(404, Yii::t('yii', 'Page not found.'));
// 获取错误码
$error = ($exception instanceof HttpException) ? $exception->statusCode : $exception->getCode();
$error = $error ? $error : 1;
// 获取错误消息
$message = ($exception instanceof Exception) ? $exception->getName() : '';
$message = $message ? $message : (($exception instanceof UserException)?$exception->getMessage():'');
$message = $message ? $message : Yii::t('yii', 'An internal server error occurred.');
// 返回错误数据
echo json_encode(['error'=>$error,'message'=>trim('v1 - '.$message)]);
}
// ...
}
以上代码只是抛砖引用,你可以直接采用,也可以按自己项目的需要重写一些返回的字段和内容。
▪ 测试调试请求
GET http://api.domain.com/index.php?r=v1/admin 列出所有管理员
GET http://api.domain.com/index.php?r=v1/admin/index 列出所有管理员
GET http://api.domain.com/index.php?r=v1/admin/view&id=1 列出id为1的管理员
POST http://api.domain.com/index.php?r=v1/admin 新建管理员
DELETE http://api.domain.com/index.php?r=v1/admin?id=1 删除id为1的管理员
PUT/PATCH http://api.domain.com/index.php?r=/v1/admin?id=1 修改id为1的管理员
我们可以在 postman 或 curl 等工具中做以上 URL 的测试,这些软件的具体使用请自行百度。
▪ 美化 URL 路由
在上面的例程中,可以发现所有的 URL 都是最原始的方式:文件名 + 问号 + 参数;这种方式一般在 API 中不会使用的,所以为了符合主流的使用体验,我们得美化下 URL。
修改配置文件 api/config/main.php
,添如下加内容:
// ...
// URL
$configs['components']['urlManager'] = array();
$configs['components']['urlManager']['class'] = 'yii\web\UrlManager';
$configs['components']['urlManager']['showScriptName'] = false;
$configs['components']['urlManager']['enablePrettyUrl'] = true;
$configs['components']['urlManager']['enableStrictParsing'] = false;
// ...
通过上面的配置后,我们可以重新测试调试请求,URL 例子如下:
GET http://api.domain.com/v1/admin 列出所有管理员
GET http://api.domain.com/v1/admin/index 列出所有管理员
GET http://api.domain.com/v1/admin/view?id=1 列出id为1的管理员
POST http://api.domain.com/v1/admin 新建管理员
DELETE http://api.domain.com/v1/admin?id=1 删除id为1的管理员
PUT/PATCH http://api.domain.com/v1/admin?id=1 修改id为1的管理员
至此还没结束,因为上面的 URL 中的 ?id=1
还是没有完全美化,所以再修改文件 api/config/main.php
,修改内容如下:
// ...
// URL
$configs['components']['urlManager'] = array();
$configs['components']['urlManager']['class'] = 'yii\web\UrlManager';
$configs['components']['urlManager']['rules'] = ['<module>/<controller>/<action>/<id:\d+>'=>'<module>/<controller>/<action>'];
$configs['components']['urlManager']['showScriptName'] = false;
$configs['components']['urlManager']['enablePrettyUrl'] = true;
$configs['components']['urlManager']['enableStrictParsing'] = false;
// ...
通过上面的配置后,我们可以重新测试调试请求,URL 例子如下:
GET http://api.domain.com/v1/admin 列出所有管理员
GET http://api.domain.com/v1/admin/index 列出所有管理员
GET http://api.domain.com/v1/admin/view/1 列出id为1的管理员
POST http://api.domain.com/v1/admin/create 新建管理员
DELETE http://api.domain.com/v1/admin/delete/1 删除id为1的管理员
PUT/PATCH http://api.domain.com/v1/admin/update/1 修改id为1的管理员
至此我们就搭建完成了 Restful api 的接口。
▪ 前端接收、并采取的跨域解决方案:
当我们在 www.domain.com 用 ajax 去请求我们 api.domain.com 的 api 接口时候,你会发现:请求失败,并报错,原因是跨域,浏览器做出了限制;
也就是说我们域名相同、端口相同、协议相同才不算做是跨域,那么该怎么办?不用担心,自有跨域的解决办法、这里介绍两种,其他可百度解决:
1. JsonP跨域解决
在 JQuery 的 ajax 中设定 datatype
的类型为 jsonp
就行了,很简单。
- 优点是:对于一些古老的浏览器很高的支持;例如万恶的ie。。。
- 缺点是:只能发出 GET 请求,其他 method 无法操作,例如 POST、DELETE 等
2. cros同源策略
设置方式,只要在 action 中设置 header 头部即可,也是最简单、最常用的一种(如果不考虑IE的话)。
在 header 中添加 Access-Control-Allow-Origin:*
或者 建立信任的 URL(例如:Access-control-Origin:http://www.domain.com;
)
- 优点:操作方便,只需添加 header。支持新兴浏览器,所有方式都可采用:例如GET、POST、DELETE、PUT、PATCH、OPTIONS 等
Access-Control-Allow-Methods:*
允许操作的方法为所有;
- 缺点:对IE的支持不好,兼容性 IE10 以前的都不能用。