PHP 流程引擎轻量实现

wen PHP项目 5

PHP流程引擎轻量实现:告别重框架,用50行代码掌控复杂业务状态机


目录导读

  1. 为什么你需要一个“轻量”流程引擎? —— 重框架之痛与轻量之美
  2. 核心概念拆解:状态、事件、动作与转移 —— 把业务逻辑图纸化
  3. 手写核心代码:一个极简的可运行引擎 —— 状态机核心类与转移逻辑
  4. 实战案例:从“待审核”到“已发布”的文章流转 —— 代码即文档
  5. 进阶技巧:条件分支、循环回退与持久化策略 —— 让引擎变“聪明”
  6. 高频问答(FAQ) —— 解决你最后的疑虑

为什么你需要一个“轻量”流程引擎?

在复杂的业务系统(如OA审批、订单状态、工单流转)中,传统的 if-else 嵌套会让代码变成“意大利面条”,而引入重型工作流引擎(如Activiti、Camunda)又面临学习成本高、部署笨重(动辄需要数据库表几十张)、环境要求苛刻(Java栈)等现实问题。

PHP 流程引擎轻量实现

对于PHP开发者,特别是基于ThinkPHP或Laravel构建的中小型项目,轻量级状态机(State Machine) 是性价比极高的解法,它不需要外部服务,不引入新的存储引擎,仅仅通过纯PHP数组和面向对象设计,就能优雅地管理所有流程节点,本文的核心思路是:用数据驱动逻辑,而非用代码堆砌逻辑。

核心概念拆解:状态、事件、动作与转移

在动笔前,必须先统一词汇,一个标准的流程引擎包含四个元素:

  • 状态(State) :业务当前所处的位置(如:待审核、草稿、已驳回)。
  • 事件(Event) :触发了什么操作导致流转(如:提交、通过、驳回)。
  • 动作(Action) :状态流转后执行的副作用(如:发送邮件、写日志)。
  • 转移(Transition) :定义“在X状态,发生Y事件,会去往Z状态”。

这就像一个电子游戏地图,状态是房间,事件是钥匙,转移是门。

手写核心代码:一个极简的可运行引擎

以下是一个不依赖任何框架的PHP核心类,利用数组映射完成90%的工作。

<?php
class LightStateMachine
{
    /** @var array 状态转移表 */
    protected array $transitions = [];
    /** @var array 状态变更后的动作回调 */
    protected array $actions = [];
    /** @var Closure 状态变更后的钩子 */
    protected ?Closure $onTransition = null;
    /**
     * 注册一条流转规则
     * @param string $fromState 初始状态
     * @param string $event     触发事件
     * @param string $toState   目标状态
     */
    public function allow(string $fromState, string $event, string $toState): self
    {
        $this->transitions[$fromState][$event] = $toState;
        return $this;
    }
    /** 注册动作回调(对应事件触发前执行) */
    public function on(string $event, callable $callback): self
    {
        $this->actions[$event][] = $callback;
        return $this;
    }
    /** 设置全局状态变更钩子 */
    public function setTransitionHook(Closure $hook): self
    {
        $this->onTransition = $hook;
        return $this;
    }
    /**
     * 执行事件转移
     * @param string $currentState
     * @param string $event
     * @param mixed  $payload 业务数据
     * @return string 新状态
     * @throws \Exception
     */
    public function apply(string $currentState, string $event, mixed $payload = null): string
    {
        // 1. 校验当前状态是否存在该事件
        if (!isset($this->transitions[$currentState][$event])) {
            throw new \Exception("非法转移:状态 [{$currentState}] 无法响应事件 [{$event}]");
        }
        // 2. 执行前置动作(如权限校验、数据预处理)
        foreach ($this->actions[$event] ?? [] as $callback) {
            call_user_func($callback, $payload);
        }
        // 3. 计算新状态
        $newState = $this->transitions[$currentState][$event];
        // 4. 触发全局钩子(用于日志记录或事件广播)
        if ($this->onTransition) {
            ($this->onTransition)($currentState, $newState, $event, $payload);
        }
        return $newState;
    }
}

代码解读:这段代码的核心灵魂在于 $transitions 二维数组,第一维是“当前状态”,第二维是“事件”,值就是“下一状态”,无需百行代码,即实现了状态机的核心约束能力。

实战案例:从“待审核”到“已发布”的文章流转

假设CMS系统文章状态:draft(草稿)、pending(审核中)、published(已发布)、rejected(已驳回)。

// 1. 实例化引擎
$fsm = new LightStateMachine();
// 2. 配置规则(状态机表)
$fsm->allow('draft', 'submit', 'pending')    // 草稿提交
    ->allow('pending', 'approve', 'published') // 审核通过
    ->allow('pending', 'reject', 'rejected')   // 审核驳回
    ->allow('rejected', 'resubmit', 'pending') // 驳回后重新提交
    ->allow('published', 'archive', 'archived'); // 发布后归档
// 3. 添加业务动作(例如审核通过时清缓存)
$fsm->on('approve', function ($payload) {
    echo "log: 文章 ID={$payload['id']} 审核通过,清除缓存。\n";
});
// 4. 设置全局钩子(写审计日志)
$fsm->setTransitionHook(function ($old, $new, $event) {
    echo "审计日志:状态从 [{$old}] 变为 [{$new}],事件 [{$event}]。\n";
});
// 5. 业务调用
try {
    $newState = $fsm->apply('draft', 'submit', ['id' => 100]);
    echo "当前状态:{$newState}\n"; // 输出 pending
    $newState = $fsm->apply($newState, 'approve', ['id' => 100]);
    echo "当前状态:{$newState}\n"; // 输出 published
} catch (\Exception $e) {
    echo "错误:".$e->getMessage();
}

运行逻辑:当状态变为 published 时,前置动作自动执行,钩子打印了完整的路径,这种写法让业务逻辑完全声明式——新增一个状态只需加一行 allow,删除一个流转不会影响其他分支。

进阶技巧:条件分支、循环回退与持久化策略

  • 条件分支(Guard条件) :在动作回调中无法阻止转移,但可以在 onTransition 钩子中抛出异常来强行中断,或者用 apply 前手动判断业务对象状态。
  • 循环回退:对于审批驳回,allow('pending','reject','rejected')allow('rejected','resubmit','pending') 构成了环回,避免死循环只需定义好业务终止条件(如驳回次数上限)。
  • 持久化:不要序列化整个引擎对象!只需将当前状态的字符串存到数据库,下次操作时读出来传入 apply,引擎本身是无状态的,这保证了集群部署时(不同机器)的可靠性。
// 持久化建议代码
$articleModel->status = $fsm->apply($articleModel->status, $event, $payload);
$articleModel->save();

高频问答(FAQ)

Q1:这个轻量引擎和Laravel的 spatie/laravel-model-states 有什么区别? A:spatie 更偏重于模型属性映射,并且基于单个模型类设计,而我们的实现更偏向独立的服务层,不强行绑定ORM,可以在任何PHP环境中复用,且代码量极小易二次开发。

Q2:如果我想让同一个事件在不同状态下执行不同动作怎么办? A:引擎默认动作绑定的是“事件”,这意味着 approve 事件在 pendingreviewing 状态下触发相同逻辑,如果你需要状态+事件的组合动作,你可以把动作数组的键改为 "{fromState}.{event}",只需修改 on() 方法内部实现即可(这是扩展点之一)。

Q3:如何防止流程被非法直接篡改(比如从草稿直接改成发布)? A:永远不要直接写 $model->status = 'published',必须通过 apply 方法,这是纪律问题,你可以启用框架的 $fillable 或只允许通过服务层修改状态字段,在模型 boot() 中禁止 update 修改该字段。

Q4:性能如何?我每秒有1000次状态变更。 A:这是一个纯内存计算,开销仅有数组查找和函数调用,比数据库行锁要快几个数量级,瓶颈通常在数据库写入(保存状态字段)而非引擎本身。


流程引擎不是大企业专属,用20%的复杂度解决80%的状态流转需求,才是敏捷开发的正道,本文的核心代码全量展示,可直接复制进你的项目 app/Services 目录,当业务日益复杂时,再考虑引入Event Sourcing或分布式协调,但初期保持轻盈,方能快跑。

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