前言
在推出 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 狀態碼以及資料返回格式。
思路
- 儘可能地遵循 Laravel 思維進行擴充套件,符合一定規範
- 儘量少的依賴安裝,最好是 0 依賴,不額外增加負擔
- 儘量完善的單元測試,保證程式碼質量(關於使用示例可以跳過下面的介紹直接檢視 github.com/Jiannei/laravel-respons... 測試用例)
- 實現需要簡潔,使用需要優雅
功能
- 統一的資料響應格式,固定包含:
code
、status
、data
、message
、error
- 內建 Http 標準狀態碼支援,同時支援擴充套件 ResponseCodeEnum 來根據不同業務模組定義響應碼
- 響應碼 coed 對應描述資訊 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 有不同響應,並且格式與前面統一
定義業務操作碼
namespace App\Repositories\Enums;
use Jiannei\Response\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;
}
本地化操作碼描述
// 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 => '登入失敗',
],
];
使用示例
成功響應
- 示例程式碼
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."
]
}
}
- Controller 以外丟擲異常返回
可以直接使用 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@foxmial.com)的方式可以聯絡到我。
總之,歡迎各路英雄好漢。
QQ 群:1105120693
本作品採用《CC 協議》,轉載必須註明作者和本文連結