项目的约束
项目约束用于统一代码结构、命名方式和开发规范,降低后期维护成本。
后端约束
1. 业务异常和全局异常处理器
项目中需要区分业务异常和系统异常。
业务异常是可以预料的异常,例如:
- 用户不存在
- 商品库存不足
- 订单状态不允许修改
- 用户余额不足
- 参数不符合业务要求
业务异常统一使用自定义异常类抛出:
throw new BusinessException('商品库存不足');项目需要配置全局异常处理器,统一处理异常并返回固定格式:
{
"code": 400,
"message": "商品库存不足",
"data": null
}全局异常处理器需要遵守以下约束:
- 业务异常直接返回对应的错误信息。
- 系统异常需要记录完整日志。
- 生产环境不能向前端返回数据库错误、文件路径和调用栈。
- Controller 中不要重复编写大量
try...catch。 - 不允许捕获异常后不处理,导致事务无法正常回滚。
2. Controller、Contract、Service、Repository、Model、第三方进行Adapter
项目按照 Controller、Service、Repository、Model、Adapter、Contract 进行分层。
调用关系为:
Controller
-> Service
-> Contract
<- Repository 实现 -> Model -> 数据库
<- Adapter 实现 -> 第三方 SDK / APIController
Controller 负责:
- 接收请求参数
- 调用参数验证器
- 调用 Service
- 返回统一格式的数据Controller 中不应该编写复杂业务逻辑,也不应该直接进行多表数据库操作。
public function create()
{
$params = request()->post();
$result = $this->orderService->createOrder($params);
return json($result);
}Contract
- Contract使用PHP的interface定义模块需要提供的能力。
Contract需要写清楚:
可以执行什么操作
输入参数是什么
返回结果是什么
可能抛出什么异常
调用时需要遵守什么规则Contract不负责具体技术实现,Contract应该表达业务能力,而不是第三方SDK的使用方式。
- 对象储存接口
interface ObjectStorage
{
/**
* 检查正式对象并生成短期只读下载授权。
*
* @return array{
* download_url: string,
* expires_at: string
* }
*/
public function createDownloadGrant(
string $objectKey,
string $originalName
): array;
/**
* 读取对象大小、MIME、URL和ETag。
*
* @return array{
* file_size: int,
* mime_type: string,
* public_url: string,
* etag: string
* }
*/
public function inspect(string $objectKey): array;
public function promote(
string $pendingObjectKey,
string $finalObjectKey,
string $sourceEtag
): void;
public function delete(string $objectKey): void;
}Service
Service 负责:
- 处理业务规则
- 调用多个 Model
- 组织完整业务流程
- 管理数据库事务
public function createOrder(array $params): array
{
// 业务处理
}Repository
Repository 负责封装数据持久化操作,为 Service 提供具有业务含义的数据访问能力。
Repository 负责:
查询、创建、更新和删除业务数据。
封装排序、分页及查询条件。
使用 Model 操作数据库。
将数据库结果转换为业务需要的结构。
interface VideoRepository
{
public function findById(int $id): ?Video;
public function findByObjectKey(string $objectKey): ?Video;
public function create(array $attributes): Video;
public function paginate(int $page, int $pageSize): array;
}Model
Model 负责:
- 数据库查询
- 数据新增、修改和删除
- 定义数据表关联关系
- 数据类型转换Model 中不应该处理请求参数和返回接口响应。
Adapter
Adapter是Contract的具体技术实现。
final class AliyunOssStorage implements ObjectStorage
{
public function createDownloadGrant(
string $objectKey,
string $originalName
): array {
// 使用阿里云OSS SDK检查对象并生成签名
}
public function inspect(string $objectKey): array
{
// 使用阿里云HeadObject读取对象信息
}
public function promote(
string $pendingObjectKey,
string $finalObjectKey,
string $sourceEtag
): void {
// 使用ETag条件复制并禁止覆盖正式对象
}
public function delete(string $objectKey): void
{
// 调用阿里云OSS删除接口
}
}第三方SDK细节不能扩散到Service和Controller。
为什么使用接口抽象
重要
接口抽象的核心作用是:让业务代码依赖项目定义的能力,而不是依赖某个具体技术或第三方 SDK。
- 例如,视频业务只需要知道对象存储具备以下能力:
interface ObjectStorage
{
public function inspect(string $objectKey): array;
public function promote(
string $pendingObjectKey,
string $finalObjectKey,
string $sourceEtag
): void;
public function delete(string $objectKey): void;
}- VideoService 依赖 ObjectStorage:
final class VideoService
{
public function __construct(
private ObjectStorage $storage
) {
}
public function completeUpload(
string $pendingKey,
string $finalKey,
string $etag
): void {
$this->storage->promote($pendingKey, $finalKey, $etag);
}
}提示
Service 不需要知道底层使用的是阿里云 OSS、腾讯云 COS,还是本地文件系统。
优点
- 降低业务代码和第三方技术的耦合
Service 只依赖 ObjectStorage,不直接依赖阿里云的:
Client
CopyObjectRequest
ServiceException
OSS 错误码
Bucket 和 Endpoint 配置
第三方 SDK 升级或调用方式变化时,主要修改 Adapter。- 更换实现时影响更小
例如从阿里云 OSS 更换为腾讯云 COS,只需要增加新的实现:
final class TencentCosStorage implements ObjectStorage
{
// 使用腾讯云 COS SDK 实现接口
}
然后修改容器绑定:
ObjectStorage::class =>
static fn (): ObjectStorage => new TencentCosStorage();
VideoService 和 VideoController 通常不需要修改。- 便于测试
测试 Service 时,可以使用假的对象存储,不需要真正连接 OSS:
final class FakeObjectStorage implements ObjectStorage
{
public array $promotedObjects = [];
public function promote(
string $pendingKey,
string $finalKey,
string $etag
): void {
$this->promotedObjects[] = compact(
'pendingKey',
'finalKey',
'etag'
);
}
}
避免测试消耗真实 OSS 流量。
避免依赖网络和第三方服务状态。
主动模拟上传失败、文件不存在、目标冲突等异常。
提高测试速度和稳定性。- 明确模块边界
Contract 明确规定模块能做什么,Adapter 决定具体怎么做。
Service:决定什么时候晋升视频
Contract:规定对象存储必须提供晋升能力
Adapter:使用阿里云 CopyObject 实现晋升
SDK:真正向阿里云发送请求
这样可以避免 Service 同时负责业务流程和第三方技术细节。- 支持模块独立开发
Contract 确认后,业务模块和第三方接入模块可以分别开发:
开发人员 A:按照 ObjectStorage 编写 VideoService
开发人员 B:实现 AliyunOssStorage
测试人员:使用 FakeObjectStorage 测试业务流程
各模块通过 Contract 协作,不需要等待所有具体实现完成。3. 代码拆分约束
单个业务类文件原则上不能超过 500 行。
当代码超过 500 行时,需要按照职责进行拆分,例如:
OrderService
├── OrderCreateService
├── OrderPayService
├── OrderCancelService
└── OrderRefundService单个方法也不能包含过多业务步骤。方法过长时,应拆分成多个职责明确的私有方法:
public function createOrder(array $params): array
{
$this->validateOrder($params);
$order = $this->saveOrder($params);
$this->saveOrderItems($order, $params);
return $order;
}方法拆分需要遵守以下要求:
- 一个方法只处理一个明确职责。
- 方法名称应能直接说明用途。
- 不要为了减少行数进行没有意义的拆分。
- 相同逻辑出现多次时,应提取为公共方法。
- 不要在一个 Service 中堆积所有业务逻辑。
4. 复杂业务流程需要添加注释
当 Service 中调用的方法较多时,需要使用注释标明业务阶段:
public function createOrder(array $params): array
{
// 1. 验证用户和商品
$this->validateOrder($params);
// 2. 创建订单
$order = $this->saveOrder($params);
// 3. 保存订单商品
$this->saveOrderItems($order, $params);
// 4. 扣减库存
$this->decreaseStock($params);
return $order;
}注释应该说明“为什么这样处理”或者“当前属于哪个业务阶段”,不要简单重复代码内容。
5. 数据命名规则
项目中的命名必须统一,并且能够直接表达业务含义。
PHP 类名
使用大驼峰命名:
OrderService
UserController
ProductModel
BusinessExceptionPHP 方法和变量
使用小驼峰命名:
$userId
$orderList
$productInfo
createOrder()
getUserInfo()
updateOrderStatus()数据库表和字段
使用小写字母和下划线:
user_orders
order_items
product_stock
user_id
order_status
created_at
updated_at其他要求
- 布尔值使用
is、has、can等开头。 - 集合和列表使用复数形式。
- 禁止使用拼音命名。
- 禁止使用
$a、$data1、$temp2等无明确含义的名称。 - 同一个数据在不同模块中的命名应保持一致。
$isEnabled = true;
$hasPermission = false;
$orderItems = [];
$userIds = [];6. 多表写入必须使用事务
只要一个业务同时写入两张或多张数据表,就必须使用数据库事务。
例如创建订单时,需要同时写入:
- 订单表
- 订单商品表
- 库存表
- 操作记录表
这些操作必须全部成功后才能提交。
Db::transaction(function () use ($params) {
$order = Order::create($params);
OrderItem::insertAll($params['items']);
ProductStock::where('product_id', $params['product_id'])
->dec('stock', $params['quantity']);
OrderLog::create([
'order_id' => $order->id,
'action' => 'create',
]);
});事务需要遵守以下要求:
- 所有数据库操作成功后才能提交。
- 任意一步失败都必须回滚。
- 不允许捕获异常后直接忽略。
- 事务中避免执行耗时的远程接口调用。
- 库存、余额等并发数据需要配合行锁或其他并发控制。
- 事务范围不能过大,只包含本次业务必须完成的数据库操作。
7.容器注入
8. 总结
项目开发需要统一遵守以下约束:
- 使用业务异常表示可预料的业务错误。
- 使用全局异常处理器统一返回和记录异常。
- 项目按照
Controller、Service、Repository、Model、Adapter、Contract进行分层。 进行分层。 - 单个业务类文件原则上不超过 500 行。
- 复杂方法需要按照职责进行拆分。
- 业务步骤较多时,需要添加阶段性注释。
- 类名、方法名、变量名和数据库字段必须统一。
- 多表写入必须放在同一个数据库事务中完成。
版权所有
版权归属:念宇
