本文目录导读:

这是一个涉及工作流引擎、动态表单和状态机设计的复杂问题,在PHP项目中实现“用户自定义流程”(也称为动态工作流、BPM或审批流),核心思路是将流程逻辑从代码中抽离出来,存储到数据库或配置文件中,通过引擎动态解析和执行。
通常有两种主流实现路径:轻量级自研(基于状态机/有限自动机)和引入专业工作流引擎(如Camunda、Flowable的PHP封装,或使用Workflow组件)。
下面我为你详细拆解实现方案、关键步骤以及代码示例。
轻量级自研方案(适合简单审批流,如OA、工单系统)
这种方案不依赖外部引擎,完全基于PHP + 数据库设计,更易掌控,适合功能不太复杂、流程节点数有限(比如不超过10个节点)的常见业务。
核心设计思路:节点(Node)+ 连线(Transition)+ 状态机
-
数据表设计
workflow_defines:流程定义表(如“请假审批”、“报销审批”)。id,name,status,created_at
workflow_nodes:节点表(如“发起”、“部门经理审批”、“总经理审批”、“结束”)。id,workflow_id,node_name,node_type(start, approver, end, condition),approver_type(user/role/department),approver_id
workflow_transitions:连线/转移表(定义节点间的流转方向)。id,workflow_id,from_node_id,to_node_id,condition_expression(可选,如amount > 1000)
workflow_instances:流程实例表(每次发起流程都会生成一条记录)。id,workflow_id,current_node_id,status(running, completed, rejected),initiator_id,form_data(JSON),created_at,updated_at
workflow_logs:审批日志(记录每一步操作)。id,instance_id,node_id,operator_id,action(approve/reject/transfer),comment,created_at
-
核心PHP逻辑(流程引擎核心类) 你需要创建一个类来处理路由、条件判断和状态变更。
<?php
// WorkflowEngine.php 简化示例
class WorkflowEngine
{
private $db; // PDO 连接
public function __construct(PDO $db)
{
$this->db = $db;
}
/**
* 执行动作(提交审批 / 审批通过 / 拒绝)
* @param int $instanceId 流程实例ID
* @param string $action 操作
* @param array $context 上下文,如审批人ID、意见、表单字段值
* @return bool
* @throws Exception
*/
public function processAction(int $instanceId, string $action, array $context = []): bool
{
// 1. 获取当前流程实例
$instance = $this->getInstance($instanceId);
if (!$instance || $instance['status'] !== 'running') {
throw new \Exception('流程不存在或已结束');
}
$currentNodeId = $instance['current_node_id'];
$workflowId = $instance['workflow_id'];
// 2. 根据action执行不同逻辑
if ($action === 'approve') {
// 查找从当前节点出发的默认连线(忽略条件,或实现简单条件判断)
$transition = $this->getNextTransition($workflowId, $currentNodeId, $context);
if (!$transition) {
throw new \Exception('无法找到接下来需要进入的节点');
}
$nextNodeId = $transition['to_node_id'];
} elseif ($action === 'reject') {
// 拒绝:通常回到上一步或发起节点
$nextNodeId = $this->getRejectTargetNode($workflowId, $currentNodeId);
} else {
throw new \Exception('不支持的操作');
}
// 3. 更新流程实例状态(使用事务)
try {
$this->db->beginTransaction();
// 记录日志
$this->insertLog($instanceId, $currentNodeId, $context['operator_id'], $action, $context['comment'] ?? '');
// 更新当前节点
$stmt = $this->db->prepare("UPDATE workflow_instances SET current_node_id = ?, updated_at = NOW() WHERE id = ?");
$stmt->execute([$nextNodeId, $instanceId]);
// 如果下一个节点是"结束"类型,更新流程状态
$nextNode = $this->getNode($nextNodeId);
if ($nextNode && $nextNode['node_type'] === 'end') {
$stmt = $this->db->prepare("UPDATE workflow_instances SET status = 'completed', current_node_id = ?, updated_at = NOW() WHERE id = ?");
$stmt->execute([$nextNodeId, $instanceId]);
}
$this->db->commit();
return true;
} catch (\Exception $e) {
$this->db->rollBack();
throw $e;
}
}
/**
* 获取下一个流转节点(实现条件表达式匹配)
*/
private function getNextTransition(int $workflowId, int $currentNodeId, array $context): ?array
{
$stmt = $this->db->prepare("SELECT * FROM workflow_transitions WHERE workflow_id = ? AND from_node_id = ? ORDER BY priority ASC");
$stmt->execute([$workflowId, $currentNodeId]);
$transitions = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($transitions as $transition) {
// 如果连线没有条件表达式,则直接返回(默认路由)
if (empty($transition['condition_expression'])) {
return $transition;
}
// 如果有条件表达式,进行简单求值(注意安全!)
// 这里仅演示,实际请使用表达式解析库或安全eval
$condition = $transition['condition_expression'];
// 假设context中有金额 amount
// 例如条件表达式为 "amount > 1000"
if ($this->evaluateCondition($condition, $context)) {
return $transition;
}
}
return null; // 没有匹配的连线
}
// ... 其他辅助方法(getInstance, getNode, insertLog, evaluateCondition等)
}
-
“可视化”配置(辅助工具) 你需要一个后台界面(如Laravel Nova、Vue+ECharts等)来让管理员或用户拖拽式或表格输入式地管理
workflow_nodes和workflow_transitions,生成流程定义,这部分是运维态工作。 -
核心难点
- 条件表达式的安全求值:尽量避免
eval(),可以集成symfony/expression-language组件,或自己实现简单的令牌比较(如field.amount >= 5000)。 - 审批人动态计算:节点的是根据
approver_type(用户/角色/部门上级)动态查询用户表计算出来,而不是存死ID。 - 会签(多人审批):需要节点表增加
countersign_type(全部同意/任一同意)和countersign_count。
- 条件表达式的安全求值:尽量避免
集成专业工作流引擎(适合复杂BPM,如ERP、KM)
如果你的流程非常复杂(包含复杂条件、子流程、并行网关、定时器等),自研成本很高,更推荐使用成熟引擎。
后端集成 Camunda BPM / Flowable(通过REST API)
Camunda 和 Flowable 是用Java写的,但他们提供REST API,PHP作为客户端调用。
- 在后台启动一个Camunda/Flowable服务。
- 你的PHP后端通过HTTP请求(如Guzzle)调用
POST /engine-rest/process-definition/key/{key}/start、POST /task/{id}/complete等。 - 流程的BPMN 2.0 XML文件用Camunda Modeler建模,然后部署到引擎。
- 优点:功能极其强大,稳定,有社区支持。
- 缺点:架构复杂,需要维护额外服务,学习曲线较陡。
PHP原生工作流组件:Symfony Workflow / Laravel Workflow
- Symfony Workflow:基于状态机图和工作流图的PHP实现,支持标记、事件、阻塞,适合嵌入到现有Symfony应用中。
- Laravel Workflow(如
zerodahero/laravel-workflow或subfission/statemachine):Laravel下的状态机/工作流包,可以配合数据库事件。
以 Symfony Workflow 为例(概念性示例):
你在PHP代码中用数组定义工作流图:
use Symfony\Component\Workflow\DefinitionBuilder;
use Symfony\Component\Workflow\MarkingStore\MethodMarkingStore;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\Workflow;
use Symfony\Component\Workflow\Registry;
class BlogPost
{
private $currentPlace = 'draft';
private $title;
// ... getters/setters
}
// 定义工作流
$definitionBuilder = new DefinitionBuilder();
$definition = $definitionBuilder
->addPlaces(['draft', 'reviewed', 'rejected', 'published'])
->addTransition(new Transition('publish', 'draft', 'reviewed'))
->addTransition(new Transition('publish', 'reviewed', 'published'))
->addTransition(new Transition('reject', 'reviewed', 'rejected'))
->addTransition(new Transition('reopen', 'rejected', 'draft'))
->build()
;
// 绑定到类
$markingStore = new MethodMarkingStore(false, 'currentPlace');
$workflow = new Workflow($definition, $markingStore);
// 使用
$post = new BlogPost();
$workflow->apply($post, 'publish'); // 从draft -> reviewed
echo $post->getCurrentPlace(); // 输出 'reviewed'
优点:纯PHP,无外部服务,明确可控。 缺点:流程定义是写死在PHP代码里的,不是用户可视化可配的,这不符合“用户自定义”的核心要求。
总结与选择建议
| 方案 | 适用场景 | 用户自定义程度 | 开发成本 | 维护复杂度 |
|---|---|---|---|---|
| 自研状态机+数据库 | 简单的审批流(OA、工单、请假) | 中(用户可通过后台配置节点和连线) | 中(需设计数据结构和引擎核心) | 中(需自行处理条件引擎安全) |
| Camunda/Flowable REST | 复杂的业务BPM(ERP、KM、采购) | 高(用户可使用Camunda Modeler建模) | 高(需单独部署Java服务) | 高(两个服务、两个语言) |
| Symfony/Laravel Workflow | 代码内固化流程 | 低(流程在代码中,不能运行时修改) | 低 | 低 |
如果你的需求是“让业务人员可以拖拽配置流程”,那么核心结论如下:
- 走自研方案:你需要设计好
节点、连线、条件的数据结构,然后编写一个能根据这些数据动态推进的引擎,这是最可行、投入产出比最高(对于非极复杂场景)的方案。 - 不要试图用纯PHP代码去实现一个完整的BPMN 2.0解析器,那会非常庞杂。
- 安全永远是第一位的:条件表达式的
eval()非常危险,务必使用白名单解析或表达式引擎。 - 用户体验是关键:提供一个简单的可视化节点编辑器(可用Vue/React + 流程图库如
vue-flow/jsplumb)能让你的功能更容易被用户接受。