PHP项目如何实现用户自定义流程?

wen java案例 2

本文目录导读:

PHP项目如何实现用户自定义流程?

  1. 方案一:轻量级自研方案(适合简单审批流,如OA、工单系统)
  2. 方案二:集成专业工作流引擎(适合复杂BPM,如ERP、KM)
  3. 总结与选择建议

这是一个涉及工作流引擎、动态表单和状态机设计的复杂问题,在PHP项目中实现“用户自定义流程”(也称为动态工作流、BPM或审批流),核心思路是将流程逻辑从代码中抽离出来,存储到数据库或配置文件中,通过引擎动态解析和执行

通常有两种主流实现路径:轻量级自研(基于状态机/有限自动机)和引入专业工作流引擎(如Camunda、Flowable的PHP封装,或使用Workflow组件)。

下面我为你详细拆解实现方案、关键步骤以及代码示例。


轻量级自研方案(适合简单审批流,如OA、工单系统)

这种方案不依赖外部引擎,完全基于PHP + 数据库设计,更易掌控,适合功能不太复杂、流程节点数有限(比如不超过10个节点)的常见业务。

核心设计思路:节点(Node)+ 连线(Transition)+ 状态机

  1. 数据表设计

    • 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
  2. 核心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等)
}
  1. “可视化”配置(辅助工具) 你需要一个后台界面(如Laravel Nova、Vue+ECharts等)来让管理员或用户拖拽式表格输入式地管理workflow_nodesworkflow_transitions,生成流程定义,这部分是运维态工作。

  2. 核心难点

    • 条件表达式的安全求值:尽量避免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}/startPOST /task/{id}/complete 等。
  • 流程的BPMN 2.0 XML文件用Camunda Modeler建模,然后部署到引擎。
  • 优点:功能极其强大,稳定,有社区支持。
  • 缺点:架构复杂,需要维护额外服务,学习曲线较陡。

PHP原生工作流组件:Symfony Workflow / Laravel Workflow

  • Symfony Workflow:基于状态机图和工作流图的PHP实现,支持标记、事件、阻塞,适合嵌入到现有Symfony应用中。
  • Laravel Workflow(如 zerodahero/laravel-workflowsubfission/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 代码内固化流程 (流程在代码中,不能运行时修改)

如果你的需求是“让业务人员可以拖拽配置流程”,那么核心结论如下:

  1. 走自研方案:你需要设计好节点连线条件的数据结构,然后编写一个能根据这些数据动态推进的引擎,这是最可行、投入产出比最高(对于非极复杂场景)的方案。
  2. 不要试图用纯PHP代码去实现一个完整的BPMN 2.0解析器,那会非常庞杂。
  3. 安全永远是第一位的:条件表达式的eval()非常危险,务必使用白名单解析或表达式引擎。
  4. 用户体验是关键:提供一个简单的可视化节点编辑器(可用Vue/React + 流程图库如vue-flow / jsplumb)能让你的功能更容易被用户接受。

抱歉,评论功能暂时关闭!