本文目录导读:

针对PHP项目的工作流与审批引擎,这是一个比较成熟的领域,在PHP生态中,虽然不像Java有Activiti或Flowable那样重量级的BPMN标准引擎,但针对业务流程管理(BPM)和审批流(OA/ERP常见需求),有非常实用的解决方案。
下面从核心概念、主流方案、自研架构设计、代码示例以及选型建议五个方面进行详细拆解。
核心概念
在选型或自研前,需要明确两个核心对象:
- 工作流(Workflow):更广义,指一系列任务或活动的自动化编排(如订单处理、CI/CD流水线),在PHP中,常指状态机或流程节点的流转。
- 审批流(Approval Flow):工作流的子集,核心特点是:
- 节点类型:单人审批、会签(多人必须都通过)、或签(一人通过即可)、抄送。
- 条件分支:金额>1000走总监审批,否则走经理审批。
- 驳回与回退:驳回到上一节点、驳回到发起人、任意回退。
主流PHP方案对比
基于数据库的状态机模式(最常用,推荐用于业务流程相对固定的场景)
- 原理:不依赖第三方引擎,用数据库表(
workflow_node,workflow_log,workflow_transition)记录状态变化。 - 优点:
- 轻量级,无外部依赖,部署简单。
- 逻辑容易被团队成员理解。
- 性能可控,适合高并发。
- 缺点:
- 不支持复杂的并行网关、子流程。
- 修改流程需要改代码(除非做可视化配置)。
- 适用:大多数OA、ERP、工单系统。
Symfony Workflow 组件(最规范的现代PHP方案)
- 原理:Symfony框架的官方组件,基于有限状态机(FSM)或工作流(Workflow)理论,通过YAML/PHP配置定义状态、转换和守卫。
- 优点:
- 工业级:稳定、测试充分。
- 可视化:可以生成状态图(dot格式)。
- 解耦:支持事件监听(
guard,enter,leave),可以在节点进出时触发业务逻辑。
- 缺点:
- 强依赖Symfony框架,或至少需要Composer集成。
- 对于非常复杂的审批(如动态多级会签),原生配置较难实现,可能需要辅以自定义逻辑。
- 典型应用:Symfony商城订单状态机、CMS内容审核。
可视化流程引擎(适合需要给用户拖拽配置的SaaS产品)
- PHP生态代表:Camunda BPM(通过REST API调用)、FlowEngine(基于jsPlumb + Laravel)。
- 原理:前端通过图形化拖拽定义BPMN 2.0流程(JSON或XML),后端解析引擎执行。
- 优点:
- 非技术人员可配置:运维、业务人员可以自己改流程。
- 标准化:完全支持并行、会签、子流程、定时器。
- 缺点:
- 部署复杂(通常需要Java运行环境,如Camunda)。
- 系统开销大,对于简单审批是“杀鸡用牛刀”。
- 需要专业的前后端工程师维护。
自研架构设计(以PHP + MySQL为例)
如果你决定自研一个审批引擎,这是比较常见的分层架构:
数据模型设计
-- 流程定义表(模版)
CREATE TABLE `workflow_definition` (
`id` INT UNSIGNED AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL COMMENT '流程名称,如请假审批',
`description` TEXT,
`config` JSON NOT NULL COMMENT '流程节点和线的JSON配置',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0禁用',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
-- 流程实例表(每个具体申请)
CREATE TABLE `workflow_instance` (
`id` INT UNSIGNED AUTO_INCREMENT,
`definition_id` INT UNSIGNED NOT NULL,
`initiator_id` INT UNSIGNED NOT NULL COMMENT '发起人',
`current_node` VARCHAR(50) NOT NULL COMMENT '当前所在节点ID',
`status` TINYINT NOT NULL COMMENT '0进行中 1通过 2拒绝 3撤销',
`form_data` JSON COMMENT '表单提交数据',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
-- 审批记录表(谁、什么时间、做了什么)
CREATE TABLE `workflow_log` (
`id` INT UNSIGNED AUTO_INCREMENT,
`instance_id` INT UNSIGNED NOT NULL,
`from_node` VARCHAR(50),
`to_node` VARCHAR(50),
`approver_id` INT UNSIGNED NOT NULL,
`action` ENUM('提交', '通过', '拒绝', '驳回', '转交') NOT NULL,
`remark` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
核心引擎方法(伪代码)
<?php
class ApprovalEngine
{
private WorkflowDefinition $definition;
// 1. 初始化流程:创建实例,记录第一条日志
public function start(int $definitionId, int $userId, array $formData): WorkflowInstance
{
$definition = WorkflowDefinition::find($definitionId);
$instance = WorkflowInstance::create([...]);
// 记录日志
WorkflowLog::create([
'instance_id' => $instance->id,
'action' => '提交',
'approver_id' => $userId,
]);
return $instance;
}
// 2. 审批动作(核心)
public function approve(int $instanceId, int $approverId, string $action, ?string $remark): void
{
$instance = WorkflowInstance::find($instanceId);
$config = json_decode($this->definition->config, true);
// 检查权限:当前用户是否在当前节点的审批人列表中
$currentNode = $config['nodes'][$instance->current_node];
if (!in_array($approverId, $currentNode['approvers'])) {
throw new \Exception('您无权审批当前节点');
}
switch ($action) {
case 'reject':
// 直接结束流程,状态置为拒绝
$instance->status = 2;
$instance->save();
break;
case 'reject_to_initiator':
// 驳回到发起人
$instance->current_node = 'start';
$instance->save();
break;
case 'approve':
// 1. 计算下一个节点
$nextNode = $this->calculateNextNode($config, $currentNode, $instance->form_data);
// 2. 如果是多节点(会签),检查是否所有人都已通过
if ($currentNode['type'] === 'countersign') {
if (!$this->checkCountersignComplete($instance, $currentNode)) {
return; // 等待其他审批人通过
}
}
// 3. 如果已经是最后一个节点,通过
if ($nextNode === null) {
$instance->status = 1;
} else {
$instance->current_node = $nextNode;
}
$instance->save();
break;
}
// 记录日志
WorkflowLog::create([
'instance_id' => $instance->id,
'from_node' => $instance->current_node,
'action' => $action,
'approver_id' => $approverId,
'remark' => $remark,
]);
}
// 3. 条件计算 (决策节点)
private function calculateNextNode(array $config, array $currentNode, array $formData): string
{
foreach ($currentNode['transitions'] as $transition) {
// 执行条件表达式(form_data.amount > 1000)
$conditionResult = $this->evaluateCondition($transition['condition'], $formData);
if ($conditionResult) {
return $transition['target_node'];
}
}
return null; // 结束
}
}
流行的PHP包推荐
如果你不想完全自己造轮子,可以考虑以下开源包:
symfony/workflow
- Stars: 1.5k+
- 特点:Symfony官方,状态机设计,完美对接框架。
- 使用场景:任何PHP项目(通过Composer),尤其是已有Symfony项目。
spatie/state-machine
- Stars: 1.2k+
- 特点:轻量,无框架依赖,配置简单(数组或类)。
- 使用场景:小型项目,需要简单的状态流转控制。
camunda/workflow-engine-php (非官方)
- 特点:通过REST API调用Camunda BPM(Java)引擎。
- 使用场景:企业级,需要真正的BPMN 2.0标准支持。
zookeeper/approval-flow (自研参考)
- 一些国产包专门针对中国式审批(逐级审批、加签、会签、驳回),建议在GitHub搜索
php approval workflow。
选型建议
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 小项目 / MVP | 自研状态机 + workflow_log 表 |
快速迭代,无需学习第三方。 |
| 中大型项目(如多租户SaaS) | Symfony Workflow 或 自研JSON配置引擎 | 支持复杂的条件分支,可扩展。 |
| 需要非技术人员配置流程 | Camunda BPM(Java)+ PHP做API层 | 满足拖拽配置BPMN的需求。 |
| 简单订单状态 | spatie/state-machine |
代码清晰,测试方便。 |
关键难点与注意事项
- 并行分支(并行网关):PHP单线程,处理并行审批时,需要利用数据库锁或消息队列(如RabbitMQ)来保证并发安全。
- 驳回逻辑:常见误区是只能“驳回到上一节点”,灵活的引擎应该支持:
- 退回修改:驳回到发起人,修改后重新提交,流程回到当前节点。
- 退回重审:驳回到某个历史节点,从那个节点重新走流程。
- 超时与催办:需要配合定时任务(Cron)或延时队列(如Redis
ZADD)实现。 - 权限与数据隔离:审批人列表通常不是写死的ID,而应该是
user_id或role_id或department_id,需要动态解析。
- 业务固定:自研状态机 + 数据库记录 > 最实用。
- 业务多变(内部OA):
Symfony Workflow> 配置驱动。 - 外部产品(需客户自定义):独立流程引擎(Camunda)或专业SaaS化方案。
建议从简单的状态机开始,逐步抽象出“节点-条件-动作”模型,避免一开始就追求BPMN复杂性。