本文目录导读:

针对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复杂性。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播