翻阅了网上很多的API 开发规范文档,参考了不少大佬们总结的经验,决定尝试使用最新版本的 Lumen(当下最新版本是 Lumen 7.x)来构建一个基础功能完备,规范统一,能够快速应用于实际的 API 项目开发启动模板。同时,也希望通过合理的应用架构设计为中大型应用保驾护航。
少许的依赖安装,遵循 Laravel 的思维进行扩展,不额外增加“负担”。
开箱即用,加速 Api 开发。
社区讨论传送:是时候使用 Lumen 7 + API Resource 开发项目了!
[TOC]
├── app
│ ├── Console
│ │ ├── Commands
│ │ └── Kernel.php
│ ├── Contracts // 定义 interface
│ │ ├── Enums
│ │ └── Repositories
│ ├── Events
│ │ ├── Event.php
│ │ └── ExampleEvent.php
│ ├── Exceptions // 异常处理
│ │ ├── Handler.php
│ │ ├── InvalidEnumKeyException.php
│ │ └── InvalidEnumValueException.php
│ ├── Http
│ │ ├── Controllers // Controller 任务分发,返回响应
│ │ ├── Middleware
│ │ └── Resources // Api Resource 数据转换
│ ├── Jobs
│ │ ├── ExampleJob.php
│ │ └── Job.php
│ ├── Listeners
│ │ └── ExampleListener.php
│ ├── Providers
│ │ ├── AppServiceProvider.php
│ │ ├── AuthServiceProvider.php
│ │ ├── EloquentUserProvider.php
│ │ ├── EnumServiceProvider.php
│ │ ├── EventServiceProvider.php
│ │ ├── QueryLoggerServiceProvider.php
│ │ └── RepositoryServiceProvider.php
│ ├── Repositories
│ │ ├── Criteria // 数据查询条件的组装拼接
│ │ ├── Eloquent // 处理无关业务的数据维护逻辑(传说中的 Repository)
│ │ ├── Enums // 系统中的枚举/常量定义
│ │ ├── Models // 定义数据实体,以及实体之间的关系(Laravel 原始的 Eloquent Model)
│ │ ├── Presenters // 数据显示前的处理,需要引入 transformer(配合 Repository 使用)
│ │ ├── Transformers // 数据转换
│ │ └── Validators // 数据维护前的参数校验(配合 Repository 使用)
│ ├── Services
│ │ └── UserService.php // 具体的业务需求处理逻辑
│ └── Support // 对框架的扩展,或者实际项目中需要封装一些与业务无关的通用功能(你或许会发现,这里 Support 中的实现其实放到 Laravel 项目中也能用)
│ ├── Enum // 扩展常量/枚举的定义和使用
│ ├── Logger // 扩展 Lumen 的日志支持记录到 Mongodb
│ ├── Response.php // 统一 API 响应格式(data、code、status、message),同时支持 Api Resource 与 Transformer
│ ├── Traits // class 中常用到的方法
│ └── helpers.php // 全局会用到的函数
其他规划讨论中。(Laravel 7 的对应实现版本?生成 API 文档?支持单元测试?异步业务逻辑的拆分?消息队列、缓存的高效使用?swoole?)
- code——包含一个整数类型的HTTP响应状态码。
- status——包含文本:"success","fail"或"error"。HTTP状态响应码在500-599之间为"fail",在400-499之间为"error",其它均为"success"(例如:响应状态码为1XX、2XX和3XX)。
- message——当状态值为"fail"和"error"时有效,用于显示错误信息。参照国际化(il8n)标准,它可以包含信息号或者编码,可以只包含其中一个,或者同时包含并用分隔符隔开。
- data——包含响应的body。当状态值为"fail"或"error"时,data仅包含错误原因或异常名称。
整体响应结构设计参考如上,相对严格地遵守了 RESTful 设计准则,返回合理的 HTTP 状态码。
考虑到业务通常需要返回不同的“业务描述处理结果”,在所有响应结构中都支持传入符合业务场景的message。
在需要用到的地方使用 \App\Traits\Helpers对\App\Http\Response中封装的响应方法进行调用,通常是在 Controller 层中根据业务处理的结果进行响应,所以 \App\Http\Controllers基类中已经引入了 Helperstrait,可以直接在 Controller 中进行如下调用:
// 操作成功情况
$this->response->success($data,$message);
$this->response->success(new UserCollection($resource), '成功');// 返回 API Resouce Collection
$this->response->success(new UserResource($user), '成功');// 返回 API Resouce
$user = ["name"=>"nickname","email"=>"[email protected]"];
$this->response->success($user, '成功');// 返回普通数组
$this->response->created($data,$message);
$this->response->accepted($message);
$this->response->noContent();
// 操作失败或异常情况
$this->response->fail($message);
$this->response->errorNotFound();
$this->response->errorBadRequest();
$this->response->errorForbidden();
$this->response->errorInternal();
$this->response->errorUnauthorized();
$this->response->errorMethodNotAllowed();
{
"data": {
"nickname": "Jiannei",
"email": "[email protected]"
},
"status": "success",
"code": 200,
"message": "成功"
}
{
"data": [
{
"nickname": "Jiannei",
"email": "[email protected]"
},
{
"nickname": "Qian",
"email": "[email protected]"
},
{
"nickname": "Turbo",
"email": "[email protected]"
}
// ...
],
"status": "success",
"code": 200,
"message": "成功"
}
{
"status": "success",
"code": 200,
"message": "操作成功",
"data": {
"data": [
{
"nickname": "Jiannei",
"email": "[email protected]"
},
{
"nickname": "Turbo",
"email": "[email protected]"
},
{
"nickname": "Qian",
"email": "[email protected]"
}
],
"meta": {
"pagination": {
"total": 13,
"count": 3,
"per_page": 3,
"current_page": 1,
"total_pages": 5,
"links": {
"previous": null,
"next": "http://lumen-api.test/users?page=2"
}
}
}
}
}
{
"status": "fail",
"code": 500,
"message": "Service error",
"data": {}
}
整体格式与业务操作成功和业务操作失败时的一致,相比失败时,data 部分会增加额外的异常信息展示,方便项目开发阶段进行快速地问题定位。
ValidationException 的响应结构:{
"status": "error",
"code": 422,
"message": "Validation error",
"data": {
"email": [
"The email has already been taken."
],
"password": [
"The password field is required."
]
}
}
NotFoundException 异常捕获的响应结构关闭 debug 时:
{
"status": "error",
"code": 404,
"message": "Service error",
"data": {
"message": "No query results for model [App\\Models\\User] 19"
}
}
开启 debug 时:
{
"status": "error",
"code": 404,
"message": "Service error",
"data": {
"message": "No query results for model [App\\Models\\User] 19",
"exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException",
"file": "/var/www/lumen-api-starter/vendor/laravel/lumen-framework/src/Exceptions/Handler.php",
"line": 107,
"trace": [
{
"file": "/var/www/lumen-api-starter/app/Exceptions/Handler.php",
"line": 55,
"function": "render",
"class": "Laravel\\Lumen\\Exceptions\\Handler",
"type": "->"
},
{
"file": "/var/www/lumen-api-starter/vendor/laravel/lumen-framework/src/Routing/Pipeline.php",
"line": 72,
"function": "render",
"class": "App\\Exceptions\\Handler",
"type": "->"
},
{
"file": "/var/www/lumen-api-starter/vendor/laravel/lumen-framework/src/Routing/Pipeline.php",
"line": 50,
"function": "handleException",
"class": "Laravel\\Lumen\\Routing\\Pipeline",
"type": "->"
}
// ...
]
}
}
{
"status": "fail",
"code": 500,
"message": "syntax error, unexpected '$user' (T_VARIABLE)",
"data": {
"message": "syntax error, unexpected '$user' (T_VARIABLE)",
"exception": "ParseError",
"file": "/var/www/lumen-api-starter/app/Http/Controllers/UsersController.php",
"line": 34,
"trace": [
{
"file": "/var/www/lumen-api-starter/vendor/composer/ClassLoader.php",
"line": 322,
"function": "Composer\\Autoload\\includeFile"
},
{
"function": "loadClass",
"class": "Composer\\Autoload\\ClassLoader",
"type": "->"
},
{
"function": "spl_autoload_call"
}
// ...
]
}
}
拿「登录成功返回用户信息」举个栗子:
第一种:指定 message
使用
return $this->response->success($user,'注册成功');
返回
{
"status": "success",
"code": 200,
"message": "注册成功",
"data": {
"nickname": "Jiannei",
"email": "[email protected]"
}
}
第二种:message 参数为空,使用 ResponseConstant 中自定义的业务操作码,读取 resources/lang/zh-CN/response.php中的业务描述信息,也就说明支持多语言了
return $this->response->success($user,'',ResponseConstant::SERVICE_LOGIN_SUCCESS);
{
"status": "success",
"code": 200101,
"message": "注册成功",
"data": {
"nickname": "Jiannei",
"email": "[email protected]"
}
}
注意:两种的返回数据有中的 code 不同,第二种返回的是自定义的操作码,具体定义规则可以查看 app/Constants/ResponseConstant.php
直接抛出 HttpException,使用自定义的错误码就可以了,如此简单。
使用
abort(ResponseConstant::SERVICE_LOGIN_ERROR);
// 等价于
throw new \Symfony\Component\HttpKernel\Exception\HttpException(ResponseConstant::SERVICE_LOGIN_ERROR);
返回
{
"status": "fail",
"code": 500102,
"message": "登录失败",
"data": {
"message": ""
}
}
使用 Postman 等 Api 测试工具的使用需要添加 X-Requested-With:XMLHttpRequest或者Accept:application/jsonheader 信息来表明是 Api 请求,否则在异常捕获到后返回的可能不是预期的 JSON 格式响应。
在添加这部分描述的时候,联想到了 Vue 中的 Vuex,熟悉 Vuex 的同学可以类比一下。
Controller => dispatch,校验请求后分发业务处理
Service => action,具体的业务实现
Repository => state、mutation、getter,具体的数据维护
Controller 岗位职责:
__construct()依赖注入多个 Service。比如 UserController 中可能会注入 UserService(用户相关的功能业务)和 EmailService(邮件相关的功能业务)$this->response调用sucess或fail方法来返回统一的数据格式return $this->response->success(new UserCollection($resource));或return $this->response->success(new UserResource($user));
Service 岗位职责:
handleListPageDisplay和handleProfilePageDisplay,分别对应用户列表展示和用户详情页展示的需求。EmailService中或许就只有调用第三方 API 的逻辑,不需要更新维护系统中的数据,就不需要注入 Repository;OrderService中实现了订单出库逻辑后,还需要生成相应的财务结算单据,就需要注入 OrderReposoitory和FinancialDocumentRepository,财务单据中的原单号关联着订单号,存在着数据关联。Repository 岗位职责:
searchUsersByPage、searchUsersById和insertUser。$this->model 实际就是绑定的 Model 实例,所以就有了这样的写法$this->model::all(),与原先的 ORM 写法User::all()是完全等价的。Model 岗位职责:
经过前面的 Service 和 Repository 「分层」,剥离了可能存在于 Model 中的很多逻辑,比如校验参数,拼接查询,处理业务和转换数据结构等。所以,现如今的 Model 只需要相对简单地数据定义就可以了。比如,对数据表的定义,字段的映射,以及数据表之间关联关系等,提供给 Repository 中使用就够了。
完整的执行顺序:Criteria -> Validator -> Presenter
Constants:
这个是 lumen-api-starter 新增的部分,用来定义应用系统中常量的数据。
Criteria:l5-repository criteria
作用类似 Eloquent Model 中的 Scope 查询,把常用的查询提取出来,但是比 Scope 更强大。
可以省去 Model 中大量的根据请求参数判断并拼接查询条件的代码,与此同时,能够做到将多种数据之间存在的通用筛选条件剥离出来。
比如 make:repository创建生成的 Repository 中默认包含以下代码,就是给 Repository 默认配置了一个 RequestCriteria,就可以直接使用下面的方式来过滤数据,难道不香吗,嗯?
public function boot()
{
$this->pushCriteria(app(RequestCriteria::class));
}
http://prettus.local/users?search=age:17;email:[email protected]&searchJoin=and
Filtering fields
http://prettus.local/users?filter=id;name
[
{
"id": 1,
"name": "John Doe"
},
{
"id": 2,
"name": "Lorem Ipsum"
},
{
"id": 3,
"name": "Laravel"
}
]
Sorting the results
http://prettus.local/users?filter=id;name&orderBy=id&sortedBy=desc
[
{
"id": 3,
"name": "Laravel"
},
{
"id": 2,
"name": "Lorem Ipsum"
},
{
"id": 1,
"name": "John Doe"
}
]
Presenter:L5-repository presenters
可选,使用 Api Resource 的同学可以略过。需要安装 composer require league/fractal,Dingo Api 中的 transformer 也是使用了这个扩展包。
作用类似 Laravel 的 Api Resource,或者可以说 Api Resource 是 Transformer 的轻量实现。
L5-repository 认为你将数据表结构的数据转换后是为了用来展示的,所以它将数据转换相关的逻辑独立出来,称为 Presenter。本质是整合了 fractal 中的 transformer 功能。
Transformer 的优秀之处这里暂不做讨论,因为这里的主角是 Presenter。传送门
先对比一下几种数据转换方式:
在 Controller 中调用 Response 中的 item 返回数据时传入 transformer 来转换数据
return $this->item($user, new UserTransformer, ['key' => 'user']);
在 Controller 中调用 Resource 或者 ResourceCollection 转换数据
//return $this->response->success(new UserResource($user));// 使用 lumen-api-starter 统一 code\status\message\data
return new UserResource($user);// 未统一响应结构
需要先定义 transformer,然后在 Presenter 中「注册」,最后在调用 Repository 时使用。
举例:
定义 UserTransformer
// app/Repositories/Transformers/UserTransformer.php
<?php
namespace App\Repositories\Transformers;
use App\Repositories\Models\User;
use League\Fractal\TransformerAbstract;
class UserTransformer extends TransformerAbstract
{
public function transform(User $user)
{
return [
'nickname' => $user->name,
'email' => $user->email,
];
}
}
「注册」到 UserPresenter
// app/Repositories/Presenters/UserPresenter.php
<?php
namespace App\Repositories\Presenters;
use App\Repositories\Transformers\UserTransformer;
use League\Fractal\TransformerAbstract;use Prettus\Repository\Presenter\FractalPresenter;
class UserPresenter extends FractalPresenter
{
/**
* Prepare data to present
*
* @return TransformerAbstract
*/
public function getTransformer()
{
return new UserTransformer();
}
}
在调用 repository 的时候使用
// app/Services/UserService.php
public function listPage(Request $request)
{
$this->repository->pushCriteria(new UserCriteria($request));
$this->repository->setPresenter(UserPresenter::class);
return $this->repository->searchUsersByPage();
}
看得出 Dingo Api 和 Api Resource 都是在最后响应数据的环节来转换数据,而 Repository 模式中认为但凡是与数据有关的处理逻辑都应该被「装进 Repository中」,应用系统中的其他部分不需要关心数据如何去查询(Criteria),如何去校验(Validator),以及如何去转换后提供显示(Presenter)。其他部分做好相应的职责就行,但凡与数据打交道的地方都交给 Repository。
controller:
service:
UserService、EmailService和OrderService
动词+名词,描述能够实现的业务需求。比如:handleRegistration表示实现用户注册功能。repository
make:repository命令可以直接生成。searchUsersByPage
依照惯例,如对您的日常工作有所帮助或启发,欢迎三连 star + fork + follow。
如果有任何批评建议,通过邮箱([email protected])的方式(如果我每天坚持看邮件的话)可以联系到我。
总之,欢迎各路英雄好汉。
The Lumen Api Starter is open-sourced software licensed under the MIT license.