自动摘要
正在生成中……
前言
在推出 lumen-api-starter 以后,收到了不少的关注和反馈,先在此感谢各位朋友萌 ?
关于 lumen-api-starter 的介绍可以参考上一篇是时候使用 Lumen 8 + API Resource 开发项目了!。本篇是在前一篇的基础上,重新整理并独立出了 package,可以同时支持最新版本 Laravel 和 Lumen 项目。
Package 地址:laravel-response
Laravel 版本 Api 开发初始化项目:laravel-api-starter
Lumen 版本 Api 开发初始化项目:lumen-api-starter
回到正题, 在用 Laravel 或 Lumen 写 API 项目前,通常需要先定义一些项目规范,来让后续的开发体验更舒适,包含有:
- 规范统一响应数据结构:成功操作、失败操作以及异常操作响应
- 使用枚举来管理项目中的常量,减少 bug,提高扩展性
- 更有效地记录日志,来提高线上排查问题效率
- 其他。..(规划中)
更新记录
实现过程
RESTful 服务最佳实践 :如何去设计 Http 状态码以及数据返回格式。
思路
功能
- 统一的数据响应格式,固定包含:
code
、status
、data
、message
、error
- 内置 Http 标准状态码支持,同时支持扩展 ResponseCodeEnum 来根据不同业务模块定义响应码
- 响应码 code 对应描述信息 message 支持本地化,支持配置多语言
- 合理地返回 Http 状态码
- 根据 debug 开关,合理返回异常信息、验证异常信息等
- 支持格式化 Laravel 的
Api Resource
、Api Resource Collection
、Paginator
(简单分页)、LengthAwarePaginator
(普通分页)、Eloquent\Model
、Eloquent\Collection
,以及简单的 array
和 string
等格式数据返回
- 分页数据格式化后的结果与使用
league/fractal
(DingoApi 使用该扩展进行数据转换)的 transformer 转换后的格式保持一致,也就是说,可以顺滑地从 Laravel Api Resource 切换到 league/fractal
规范
- 合适的 Http 状态码,可以让客户端 / 浏览器更好地理解 Http 响应
{
"status": "success",// 描述 HTTP 响应结果:HTTP 状态响应码在 500-599 之间为”fail”,在 400-499 之间为”error”,其它均为”success”
"code": 200,// 包含一个整数类型的 HTTP 响应状态码,也可以是业务描述操作码,比如 200001 表示注册成功
"message": "操作成功",// 多语言的响应描述
"data": {// 实际的响应数据
"nickname": "Joaquin Ondricka",
"email": "lowe.chaim@example.org"
},
"error": {}// 异常时的调试信息
}
需求
在不使用任何 package 的情况下,在 Laravel 中响应 API Json 格式数据,通常是下面这个样子:
return response()->json($data, $status, $headers, $options);
但实际开发场景,会有很多种数据返回需求:
- 更多时候只是简单的成功和失败响应,所以需要有快捷的
success
和 fail
格式化方法
- 成功响应可能包含有:
User::all()
、User::first()
、UserResource
、UserCollection
、User::paginate()
、User::simplePaginate()
、 Collection
和普通的 Array
等,希望这些不同类型的数据都能格式化成统一的结构
- 失败的响应,通常就是根据不同的业务场景,返回不同的错误码和错误描述
- 异常响应,对于表单验证、Http 等异常情况,能够针对是否开启 debug 有不同响应,并且格式与前面统一
定义业务操作码
<?php
namespace App\Repositories\Enums;
use Jiannei\Enum\Laravel\Repositories\Enums\ResponseCodeEnum as BaseResponseCodeEnum;
class ResponseCodeEnum extends BaseResponseCodeEnum
{
// 业务操作正确码:1xx、2xx、3xx 开头,后拼接 3 位
// 200 + 001 => 200001,也就是有 001 ~ 999 个编号可以用来表示业务成功的情况,当然你可以根据实际需求继续增加位数,但必须要求是 200 开头
// 举个栗子:你可以定义 001 ~ 099 表示系统状态;100 ~ 199 表示授权业务;200 ~ 299 表示用户业务。..
const SERVICE_REGISTER_SUCCESS = 200101;
const SERVICE_LOGIN_SUCCESS = 200102;
// 客户端错误码:400 ~ 499 开头,后拼接 3 位
const CLIENT_PARAMETER_ERROR = 400001;
const CLIENT_CREATED_ERROR = 400002;
const CLIENT_DELETED_ERROR = 400003;
const CLIENT_VALIDATION_ERROR = 422001; // 表单验证错误
// 服务端操作错误码:500 ~ 599 开头,后拼接 3 位
const SYSTEM_ERROR = 500001;
const SYSTEM_UNAVAILABLE = 500002;
const SYSTEM_CACHE_CONFIG_ERROR = 500003;
const SYSTEM_CACHE_MISSED_ERROR = 500004;
const SYSTEM_CONFIG_ERROR = 500005;
// 业务操作错误码(外部服务或内部服务调用。..)
const SERVICE_REGISTER_ERROR = 500101;
const SERVICE_LOGIN_ERROR = 500102;
}
本地化操作码描述
<?php
// resources/lang/zh-CN/enums.php
use App\Repositories\Enums\ResponseCodeEnum;
return [
// 响应状态码
ResponseCodeEnum::class => [
// 成功
ResponseCodeEnum::HTTP_OK => '操作成功', // 自定义 HTTP 状态码返回消息
ResponseCodeEnum::HTTP_INTERNAL_SERVER_ERROR => '操作失败', // 自定义 HTTP 状态码返回消息
ResponseCodeEnum::HTTP_UNAUTHORIZED => '授权失败',
// 业务操作成功
ResponseCodeEnum::SERVICE_REGISTER_SUCCESS => '注册成功',
ResponseCodeEnum::SERVICE_LOGIN_SUCCESS => '登录成功',
// 客户端错误
ResponseCodeEnum::CLIENT_PARAMETER_ERROR => '参数错误',
ResponseCodeEnum::CLIENT_CREATED_ERROR => '数据已存在',
ResponseCodeEnum::CLIENT_DELETED_ERROR => '数据不存在',
ResponseCodeEnum::CLIENT_VALIDATION_ERROR => '表单验证错误',
// 服务端错误
ResponseCodeEnum::SYSTEM_ERROR => '服务器错误',
ResponseCodeEnum::SYSTEM_UNAVAILABLE => '服务器正在维护,暂不可用',
ResponseCodeEnum::SYSTEM_CACHE_CONFIG_ERROR => '缓存配置错误',
ResponseCodeEnum::SYSTEM_CACHE_MISSED_ERROR => '缓存未命中',
ResponseCodeEnum::SYSTEM_CONFIG_ERROR => '系统配置错误',
// 业务操作失败:授权业务
ResponseCodeEnum::SERVICE_REGISTER_ERROR => '注册失败',
ResponseCodeEnum::SERVICE_LOGIN_ERROR => '登录失败',
],
];
使用示例
成功响应
<?php
public function index()
{
$users = User::all();
return Response::success(new UserCollection($users));
}
public function paginate()
{
$users = User::paginate(5);
return Response::success(new UserCollection($users));
}
public function simplePaginate()
{
$users = User::simplePaginate(5);
return Response::success(new UserCollection($users));
}
public function item()
{
$user = User::first();
return Response::success(new UserResource($user));
}
public function array()
{
return Response::success([
'name' => 'Jiannel',
'email' => 'longjian.huang@foxmail.com'
],'', ResponseCodeEnum::SERVICE_REGISTER_SUCCESS);
}
{
"status": "success",
"code": 200,
"message": "操作成功",
"data": [
{
"nickname": "Joaquin Ondricka",
"email": "lowe.chaim@example.org"
},
{
"nickname": "Jermain D'Amore",
"email": "reanna.marks@example.com"
},
{
"nickname": "Erich Moore",
"email": "ernestine.koch@example.org"
}
],
"error": {}
}
{
"status": "success",
"code": 200,
"message": "操作成功",
"data": {
"data": [
{
"nickname": "Joaquin Ondricka",
"email": "lowe.chaim@example.org"
},
{
"nickname": "Jermain D'Amore",
"email": "reanna.marks@example.com"
},
{
"nickname": "Erich Moore",
"email": "ernestine.koch@example.org"
},
{
"nickname": "Eva Quitzon",
"email": "rgottlieb@example.net"
},
{
"nickname": "Miss Gail Mitchell",
"email": "kassandra.lueilwitz@example.net"
}
],
"meta": {
"pagination": {
"count": 5,
"per_page": 5,
"current_page": 1,
"total": 12,
"total_pages": 3,
"links": {
"previous": null,
"next": "http://laravel-api.test/api/users/paginate?page=2"
}
}
}
},
"error": {}
}
{
"status": "success",
"code": 200,
"message": "操作成功",
"data": {
"data": [
{
"nickname": "Joaquin Ondricka",
"email": "lowe.chaim@example.org"
},
{
"nickname": "Jermain D'Amore",
"email": "reanna.marks@example.com"
},
{
"nickname": "Erich Moore",
"email": "ernestine.koch@example.org"
},
{
"nickname": "Eva Quitzon",
"email": "rgottlieb@example.net"
},
{
"nickname": "Miss Gail Mitchell",
"email": "kassandra.lueilwitz@example.net"
}
],
"meta": {
"pagination": {
"count": 5,
"per_page": 5,
"current_page": 1,
"links": {
"previous": null,
"next": "http://laravel-api.test/api/users/simple-paginate?page=2"
}
}
}
},
"error": {}
}
{
"status": "success",
"code": 200,
"message": "操作成功",
"data": {
"nickname": "Joaquin Ondricka",
"email": "lowe.chaim@example.org"
},
"error": {}
}
其他快捷方法
Response::accepted();
Response::created();
Response::noContent();
失败响应
不指定 meesage
public function fail()
{
Response::fail();// 不需要加 return
}
{
"status": "fail",
"code": 500,
"message": "Http internal server error",
"data": {},
"error": {}
}
{
"status": "fail",
"code": 500,
"message": "操作失败",
"data": {},
"error": {}
}
指定 message
public function fail()
{
Response::fail('error');// 不需要加 return
}
返回数据
{
"status": "fail",
"code": 500,
"message": "error",
"data": {},
"error": {}
}
指定 code
public function fail()
{
Response::fail('',ResponseCodeEnum::SERVICE_LOGIN_ERROR);
}
返回数据
{
"status": "fail",
"code": 500102,
"message": "登录失败",
"data": {},
"error": {}
}
其他快捷方法
Response::errorBadRequest();
Response::errorUnauthorized();
Response::errorForbidden();
Response::errorNotFound();
Response::errorMethodNotAllowed();
Response::errorInternal();
异常响应
对于异常的数据格式化,需额外在 app/Exceptions/Handler.php
中 引入 use Jiannei\Response\Laravel\Support\Traits\ExceptionTrait;
引入以后,对于 ajax 请求产生的异常都会进行格式化数据返回。
(Lumen 中为达到同样效果,还需在 app/Http/Controllers/Controller.php
中引入 ExceptionTrait
)
{
"status": "error",
"code": 422,
"message": "验证失败",
"data": {},
"error": {
"email": [
"The email field is required."
]
}
}
可以直接使用 abort 辅助函数直接抛出 HttpException 异常
abort(ResponseCodeEnum::SERVICE_LOGIN_ERROR);
// 返回数据
{
"status": "fail",
"code": 500102,
"message": "登录失败",
"data": {},
"error": {}
}
开启 debug
{
"status": "error",
"code": 404,
"message": "Http not found",
"data": {},
"error": {
"message": "",
"exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException",
"file": "/home/vagrant/code/laravel-api-starter/vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php",
"line": 43,
"trace": [
{
"file": "/home/vagrant/code/laravel-api-starter/vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php",
"line": 162,
"function": "handleMatchedRoute",
"class": "Illuminate\\Routing\\AbstractRouteCollection",
"type": "->"
},
{
"file": "/home/vagrant/code/laravel-api-starter/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
"line": 646,
"function": "match",
"class": "Illuminate\\Routing\\RouteCollection",
"type": "->"
},
...
]
}
}
关闭 debug
{
"status": "error",
"code": 404,
"message": "Http not found",
"data": {},
"error": {}
}
One more thing?
回顾一下,这些封装全部都是基于 response()->json()
,即返回的是 JsonResponse
对象,所以我们依旧可以继续链式调用该对象上的方法。
// 设置 HTTP 响应码
return Response::success(new UserResource($user))->setStatusCode(ResponseCodeEnum::HTTP_CREATED);
其他
依照惯例,如果对您的日常工作有所帮助或启发,欢迎三连 star + fork + follow
。
如果有任何批评建议,通过邮箱(longjian.huang@foxmail.com)的方式可以联系到我。
总之,欢迎各路英雄好汉。
QQ 群:1105120693