Service层应放在app/service目录下,与app/controller平级,命名用PascalCase;通过容器获取实例,专注业务编排,不写SQL、不处理HTTP状态;方法单一职责,事务由Controller或Command层控制,参数用数组或DTO封装,依赖通过构造函数注入,配置由调用方传入。

Service层该放在哪?目录结构别乱套
ThinkPHP默认不强制 Service 层,但项目一复杂,控制器里塞满数据库操作、第三方调用、状态校验,很快变成“面条代码”。官方推荐的 app/service 是合理位置,不是随便建个 app/common 或 app/logic 就行。
- 必须和
app/controller平级,且命名统一用 PascalCase(如UserOrderService),避免和模型名冲突 - 不要直接 new 实例,一律通过容器获取:
app()->make(UserOrderService::class),否则无法自动注入依赖 - 别把 Service 当“万能工具箱”,它只负责业务编排,不写 SQL(交给 Model/Query),也不处理 HTTP 状态(那是 Controller 的事)
Service 方法怎么设计?别把事务和校验混在一起
常见错误是写一个 createOrder(),里面既查库存、又扣余额、又发消息、还手动 Db::transaction()——结果一出错就难定位,测试也写不了。
- 一个方法只做一件事:比如
checkStockAndLock($skuId, $quantity)负责库存预占,deductBalance($userId, $amount)负责余额扣减 - 事务控制必须上提到 Controller 或更高层(如 Command),Service 内部不要自己开事务,否则嵌套调用时容易死锁或漏回滚
- 参数尽量用数组或 DTO 对象传入,别堆一堆
$userId, $productId, $addressId, $couponCode, $remark,后期加字段会崩
// 推荐:用数组封装,语义清晰
$orderData = [
'user_id' => $userId,
'items' => $cartItems,
'address' => $address,
];
$result = app()->make(OrderService::class)->submit($orderData);
Service 怎么复用?别重复注入同一个类
多人协作时容易出现:A Service 里 app()->make(PaymentService::class),B Service 里再 app()->make(PaymentService::class),看着没问题,但每次 new 都绕过容器生命周期管理。
- 在构造函数里声明依赖,让容器自动注入:
public function __construct(private PaymentService $paymentService) {} - 如果 Service 之间有强依赖(比如
RefundService必须用到OrderService),就在构造函数里写明,别在方法里临时 get - 避免在 Service 里调用
config()或env(),配置应由调用方传入,方便单元测试 mock
为什么有些逻辑还是写进 Controller?边界在哪?
不是所有东西都得塞进 Service。比如“用户扫码后跳转支付页”这个动作,核心是组装 URL、设置 session、重定向——没业务规则,没状态变更,没跨系统协调,就别硬拆。
- Controller 负责:协议适配(HTTP/CLI/WebSocket)、输入过滤(
input())、响应包装(json())、跳转控制 - Service 负责:规则判断(是否可退款)、一致性保障(订单+库存+积分同步)、外部协同(调微信支付 API + 记本地日志 + 发 MQ)
- 容易被忽略的是异常处理粒度:Controller 捕获
BusinessException返回友好提示,而 Service 里只抛具体异常(InsufficientStockException),别用throw new Exception('库存不足')这种裸异常
真正卡住人的,从来不是“要不要加 Service”,而是每个方法的职责半径划在哪——划窄了调用链太长,划宽了又变回老样子。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











