应将审批逻辑抽离至责任链模式,每个handler实现统一接口并专注自身职责,链由service层初始化、配置文件定义顺序,输入输出统一context结构,事务包裹全链,依赖通过容器注入。

ThinkPHP 里怎么让审批逻辑不堆在控制器里
审批流一复杂,IndexController 就变成 if-else 垃圾场,改一个节点全得重测。责任链不是炫技,是把「谁该审」「审完干啥」「不通过往哪退」从流程里剥出来,每个 Handler 只关心自己那一步。
实操上别手写链式调用,ThinkPHP 的 Loader::import() 或自动加载机制足够支撑 Handler 类的动态注册。关键不是“怎么连”,而是“怎么让链能被配置、被跳过、被记录”:
- 每个 Handler 必须实现统一接口,比如
ApproveHandlerInterface,含handle()和canHandle() - 链的初始化放在 Service 层(如
ApprovalService),别塞进 Controller —— Controller 只负责传参和返回响应 - 用配置文件定义审批顺序:
config/approval_chain.php返回数组,如['DepartmentHeadHandler', 'FinanceHandler', 'AdminHandler'],避免硬编码
为什么不能直接 new Handler 然后 $handler->handle() 串起来
看似简单,但会立刻撞上三个现实问题:状态无法透传、异常难定位、后续 Handler 拿不到前序结果。ThinkPHP 的请求生命周期里,Request 对象是共享的,但 Handler 之间不该靠全局变量或静态属性传数据。
正确做法是统一输入输出结构,比如所有 handle() 接收 $context(数组或轻量 DTO 对象),并返回同样结构的 $context,其中必须含 ['passed' => true, 'reason' => '', 'next' => true] 这类字段:
-
passed决定是否继续向下流转 -
next允许跳过当前 Handler(比如财务审批在金额 - 错误信息统一写入
$context['errors'][],最后由 Service 聚合返回,而不是抛Exception中断整条链
示例片段(非完整类):
public function handle(array $context): array
{
if ($context['amount'] > 10000 && empty($context['dept_head_sign'])) {
$context['errors'][] = '部门负责人未审批';
$context['passed'] = false;
}
return $context;
}
如何让某个审批节点支持“加签”或“驳回重提”而不改链结构
责任链的弹性不在增删节点,而在节点内部行为可配置。ThinkPHP 的 Cache 或数据库配置表可以存节点策略,比如 finance_handler_strategy 字段值为 recheck_on_reject,Handler 读取后自行决定是否把流程推回上一节点。
常见踩坑点:
- 加签不是插入新 Handler,而是让当前 Handler 主动触发另一个 Handler 的
handle(),并把结果合并回原$context - 驳回重提不要清空整个
$context,只重置['passed' => true, 'errors' => []],保留原始单据 ID、申请人等上下文 - 避免在 Handler 里调用
Db::transaction()—— 整个链应包裹在 Service 的事务中,否则部分成功部分失败会导致状态不一致
TP6/TP8 中依赖注入容器怎么配合责任链避免 new 滥用
手动 new Handler 会锁死依赖,比如 FinanceHandler 依赖 FinanceService,而后者又依赖 Db。ThinkPHP 容器支持绑定接口到具体实现,也支持闭包工厂函数动态构造:
- 在
app/provider.php里绑定:$container->bind('ApproveHandlerInterface', \app\handler\FinanceHandler::class); - 更推荐用工厂方式注入依赖:
$container->bind('FinanceHandler', function ($app) { return new \app\handler\FinanceHandler($app->get('FinanceService')); }); - 链式执行时,从容器取实例:
$handler = $this->app->make($handlerClass);,确保每次都是新实例(无状态前提下)
注意:如果 Handler 里用了 $this->app->cache 这类单例,要确认缓存 key 是否包含用户 ID 或单据 ID,否则不同审批混用缓存会出错。
最麻烦的从来不是链怎么搭,而是每个 Handler 怎么判断“这事归我管”——canHandle() 方法里写的条件,往往比 handle() 本身还难测准。上线前一定用真实多角色、多金额、多状态组合跑一遍,别信单元测试里 mock 出来的“理想路径”。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










