前言
demo 代码链接:github 代码
基本架构
代码分层
应用的基本架构主要包含以下 5 个部分:
- Controller Layer(控制器层)
- Transformer Layer(转换层)
- Service Layer(服务层)
- Repository Layer(仓库层)
- Model Layer(模型层)
各个层次的主要职责如下图所示
详细说明
- 基本的程序流程如上图所示,从 1 到 8。若业务逻辑比较简单,可以直接跳过 Service 层,由 Controller 层直接调用 Repository 层。
- 各层次之间可以通过依赖注入联系起来。
- 业务逻辑主要分布在 Service 层和 Model 层。Service 层负责工作流逻辑,即任务的具体执行流程,如事务处理等;Model 层负责领域逻辑,领域逻辑包括了业务规则、业务计算等。
- 通常情况下,Service 层由于包含了主要的工作流逻辑,其可复用性比较差,但当 Service 层的业务逻辑积累到一定程度的时候,会沉淀一些通用的业务逻辑(工作流逻辑),最好将通用的业务逻辑提取出来,形成一个 Service 层内的子层,称为“通用处理层”(General Process Layer),可以将这部分代码放到当前 Services 目录下的 General 目录中。
- Service 层的返回值: 1. 业务对象(model 等业务数据)2.bool 值,指示处理结果。
- 当 Service 层的业务逻辑无法正常执行时,需要抛出业务处理异常 BusinessException(注意,不是程序执行异常。业务处理异常例子:如账户余额不足,无法转账)。通过业务处理异常,将不正常的业务处理结果返回给调用者(eg:Controller 或其他 Service)。而在正常执行业务逻辑的情况下,则返回 Service 层的正常返回值,即上面第 5 点。
- 在每一层中,当新开一个子分类时,最好建立一个子分类的基类。以 Controller 层为例子,当需要在 app/Api/Controllers/V1 目录建立一个 Insurance 子目录时,最好在建好后的目录中添加一个 BaseController,作为该目录下的基类。
-
Model 层可以细分为 AR(ActiveRecord)层和 Domain 层。Domain 层通常是基于 AR 层。AR 层中每个类对应一张数据库表,而 Domain 类中包含的数据可以来自多个 AR 类。
- 通常会在 AR 层中写与数据库相关的代码,如表的关联关系,表属性的可取值等。
- 通常会在 Domain 层中写相应的领域逻辑。eg:领域模型某些值的取值规则
- Domain 类代表一个完整的领域模型,而 AR 类则不一定构成一个完整的领域模型。eg:产品的数据存放在多张张表内:product_a 和 product_b 等,因此会有多个 AR 类对应这些表;同时,可以引入一个名为“Product”的 Domain 类,它代表了一个完整的产品(领域模型)。Domain 类可以基于底层 AR 类中一个(一般来说是基于主表)。
目录结构
目录结构如下所示:
详细说明
- 如上图所示,各个层次 Controller、Service、Transformer、Model、Repository 都有自己相应的目录
-
Controllers 目录说明(Controller 层)
- Controller 层,所有 api 的控制器放在该目录下,按版本分类(V1,V2…),版本目录下按照业务分类
-
Controller 层的职责:
- 校验输入
- 处理请求 & 构造响应
- 调用 Transformer 层、Service 层、Repository 层,但不应该在 Controller 中包含任何业务逻辑
- 在各个版本目录之下(V1,V2…), 按照业务将 Controller 分到不同的子目录中(eg:Blog,Marketing…),而不是按照数据库进行划分,虽然按照业务划分与按照数据库划分的结果可能一样
- 每个版本目录下有一个版本控制器(eg:V1Controller), 该版本下的所有控制器需要继承自该控制器。版本控制器必须继承自 AppHttpControllersApiController
- 按照业务划分的控制器子目录中应该有一个控制器基类(eg:BaseController),所有该目录下的控制器继承自该基类控制器
-
Common 目录说明
- Common 目录用于放置一些在整个项目中都可以使用的通用代码,通常这些代码不应该包含特定的业务逻辑
- 子目录 Components 用于放置组件代码(注意:这些组件代码不应该继承自框架代码 / 第三方代码,否则应该将其放置到 Extensions 目录)。通常这些代码能提供一个特定的功能,但又不依赖框架本身,可以作为其他项目的第三方包使用
- 子目录 Extensions 用于放置扩展了框架代码 / 第三方代码原有功能的代码(通常意味着继承自框架代码 / 第三方代码),注意与 Components 区分
- 子目录 Enum 用于放置“常量定义”的代码
- 子目录 Helpers 用于放置一些工具类,工具类中通常会提供一些静态方法,方便调用
- 子目录 Scopes 用于放置与 Eloquent ORM 相关的 Scopes 定义
- 子目录 Lib 用于存放一些底层的库文件
-
Models 目录说明(Model 层)
- Model 层,所有的模型类放置在该目录下。通常按数据库进行分类(eg: DbBlog)
-
Model 层的职责(继承自 Eloquent class 时):
- 对应一张数据库表,一个 model 实例表示表中一条记录
- 处理 property,如 $db, $table,$fillable 等;处理 scope
- Accessors & Mutators : 在从 model 实例中获取或存储属性时对其进行格式化
- 关联关系配置:使用 hasMany()、belongsTo()等
- model 本身行为的代码(即领域逻辑代码,属于业务逻辑的一部分),包括了执行 model 在运行时的状态变化,如 status 由 valid 变换成 invalid
-
Model 层的职责(不继承自 Eloquent class 时):
- 作为一个领域类,包含领域逻辑
- 当一个完整的领域类被分割成多个数据库表存储在数据库中时,可以在各数据库目录(eg:DbBlog)下创建 Domain 目录,用于存放完整的领域类。
- 所有对应数据库表的 Model 应该间接继承自 AppModel。每个数据库目录下(eg: DbBlog)应该包含一个 BaseModel(代表该数据库),其他 Model 继承自该 BaseModel
- 注意:对数据库表进行“增删改查”的操作代码请不要放置到 Model,应该将“增删改查”的代码放置到 Repository 层
-
Repositories 目录说明(Repository 层)
- Repository 层,所有仓库类放置在该目录下。通常按照业务 / 数据库进行划分
-
Repository 层的职责:
- 仅包含对数据库直接进行增删改查操作的代码,辅助 Model 层(除此之外请不要放置其他代码;通常增删改的逻辑比较单一,而查则会有多种情况,将各种查询逻辑在此处实现)
- Repository 层仅包含直接对数据库进行操作的代码,其他涉及外部调用等功能的代码应该考虑放置在 Service 层中。
- 所有的仓库类应该继承自 AppRepository 类。
-
Services 目录说明(Service 层)
- Service 层,所有的服务类放置在该目录下。通常按业务进行分类
-
Service 层的职责:
- 处理牵涉到的外部行为:如发送邮件,使用外部 API(如使用队列,调用 thrift,调用其他团队的服务等)
- 包含业务逻辑(主要是工作流逻辑(workflow logic), 即完成某个任务的具体流程):service 层是业务逻辑存在的主要地方,辅助 Controller 层;当需要对数据库进行增删改查时,则应该调用相应的 Repository 层
- 所有的服务类都应该继承自 AppService 类
-
Transformers 目录说明(Transformer 层)
- Transformer 层,所有的转换类放置在该目录下。通常按照业务进行分类。
-
Transformer 层的职责:
- 处理显示逻辑
- 管理 API 接口的输出(使接口的输出与底层的 Service,Repository,Model 等解耦,这样即使底层数据库表进行了修改,也可以不影响接口的使用)
- 所有的转换类都应该继承自 AppTransformer 类
响应
注意 : 这里讨论的响应格式指的是应用业务相关的响应,由第三方提供的 api 接口的响应不纳入处理范围(eg:laravel passport 提供的响应,swagger 提供的响应)
响应分类
- 成功类响应:http 响应码介于 200~300。返回此类响应表示服务器完整处理了该请求,没有未捕捉处理的异常或错误。(除了正常情况,在业务逻辑处理失败时,也会返回此类响应,同时会带上相应的业务处理失败信息)
- 失败类响应:http 响应码不介于 200~300。返回此类响应表示服务器抛出了未捕捉处理的异常或错误。
响应例子
成功类响应
1. 业务逻辑处理成功
2. 业务逻辑处理失败
结构如上图所示:结构与业务逻辑处理成功是一样。区别在于成功时的 code 为 0,失败时则为相应的错误码,code 的取值为为 app\Common\Enum\ErrorCode.php 中的 业务级错误码(见下面的错误码)。
失败类响应
失败响应的格式配置在文件 config/api.php 中(关键词为:errorFormat)。主要包括了 message、errors、code、status_code、debug。有些信息在生产环境不会展示。
响应格式化处理的思路
响应格式化处理的大致思路:对特定的请求(对此类请求做标记)的处理结果,在返回给用户时进行拦截(使用事件机制),对原有响应进行格式化处理。
响应的代码:
- App\Http\Middleware\BusinessFormatOutput : 路由中间件,在某些路由放置该中间件,则标记该请求,表明其响应需要进行格式化处理
- App\Listeners\AddBusinessStatusToResponse : 事件 handler,处理由 dingo 触发的 ResponseWasMorphed 事件,对响应进行格式化处理
- App\Http\Controllers\ApiController.php 文件中的常量 BusinessStatusHeader,通过响应中的 header 为中介,将业务逻辑处理结果传递到 2 中的事件 handler 中,并最终构成格式化响应。
错误码
错误码相关的代码文件为:app\Common\Enum\ErrorCode.php
错误码格式:A-BB-CCC
- A : 表示错误级别,0 代表成功,1 代表系统级错误,2 代表服务(业务)级错误;
- B : 表示项目 / 模块 / 分类;
- C : 具体错误编号;
不同错误级别错误码的使用:
-
业务级错误码用于表示业务处理结果。
- Service 层业务处理失败,抛出 BusinessException 时使用业务级状态码
- Controller 层构造响应时,定义响应的业务处理结果,eg:return $this->response->array($validator->errors()->toArray())->withHeader(self::BusinessStatusHeader, [ErrorCode::BUSINESS_INVALID_PARAM, ‘ 业务处理结果信息 ’]);
- 用于日志记录(业务相关的日志)
-
系统级错误码用于表示代码运行异常。
- 用于记录系统性异常日志,Controller、Service、Transformer、Repository、Model 各个层皆可
注意:
- 错误码文件不能重写,若有新的错误码,请按现有分类添加,不能删除或修改旧的错误码。
异常与异常处理
异常相关的代码:app/Exceptions 目录。在应用代码中,只能抛出 BusinessException 或者是 SystemException。请不要抛出其他的异常,不同异常通过异常的 code 来区分(code 的定义在 app/Common/Enum/ErrorCode.php)。
当业务逻辑执行失败时,抛出 BusinessException,常见可能情况如下:
- Controller 层校验输入失败,抛出 BusinessException
- Service 层业务逻辑执行失败,直接抛出 BusinessException(如账户余额不足,无法转账)
- Service 层业务逻辑执行失败(但没有抛出异常,而是通过返回值指明执行失败),则接受到该返回值的调用者抛出 BusinessException
Controller 必须捕捉 BusinessException(因此即使抛出了 BusinessException,依然要返回一个成功类响应(见上文)),并根据 BusinessException 的相应信息构造响应。建议所有 Controller 的 action 以下面的格式进行编写。
public function add(Request $request, ReserveService $reserveService){
try{// 将所有的控制器逻辑放到 try 块中
$postData = $request->post();
// 校验数据有效性
/** @var \Illuminate\Validation\Validator $validator*/
$validator = Validator::make($postData, [
'orderName' => 'required',
'reservePhone' => 'required',
]);
if($validator->fails()){// 校验失败
new BusinessException(ErrorCode::BUSINESS_INVALID_PARAM, "", $validator->errors()->toArray());
}
$result = $reserveService->addReservation($postData);
if(true === $result){
// 业务逻辑执行成功
return $this->response->array([]);
}else{
// 通过返回值指示业务逻辑执行失败
throw new BusinessException(ErrorCode::BUSINESS_BUSY);
}
} catch (BusinessException $e){// 捕捉 BusinessException,根据异常的信息构造响应,下面这段代码可以通用
return $this->response->array($e->getExtra())
->withHeader(self::BUSINESS_STATUS_HEADER, [$e->getCode(), $e->getMessage()]);
}
}
当发生底层系统异常时,抛出 SystemException。没有捕捉处理的 SystemException 会造成一个失败类响应(见上文)。
日志与预警
日志组件与预警组件的存在是为了更好的维护项目,及时处理 bug。应该根据自己的需要添加相应的日志组件和预警组件。
文档
可以选择集成一个成熟的文档工具,如 swagger,blueprint 等。