本文目录导读:

在 Laravel 工作流设计中,状态机是一个非常推荐且常用的核心模式,尤其是处理复杂的业务流转(如订单状态、审批流程、工单系统)。
简单直接的回答:不一定强制使用,但强烈建议使用。
为了让你更清晰地理解,我从以下几个角度分析:
什么是状态机?为什么适合 Laravel?
状态机是一种数学模型,它定义了一个对象只能处于有限数量的“状态”之一,并且通过“事件”或“动作”在状态之间进行“转换”。
- 状态:
pending,approved,rejected,shipped。 - 转换:
submit动作将pending转换为approved。 - 守卫(Guard):转换发生的条件(只有管理员才能执行
approve)。 - 动作(Action):转换发生时执行的副作用(发送邮件、记录日志)。
为什么适合 Laravel?
- 业务清晰:用代码显式定义所有允许的状态和转换,避免数据库中出现非法状态(例如直接从
pending跳到shipped)。 - 可维护性:状态和转换逻辑集中管理,而不是分散在控制器、模型事件中。
- 可扩展性:添加新状态或转换只需修改状态机配置,无需大改业务代码。
Laravel 中实现状态机的常用方案
Laravel 生态中有两个非常流行的包,它们本质上都是状态机的实现,但使用方式不同:
A. 专业状态机包:spatie/laravel-model-states
- 核心思想:将“状态”本身定义为一个 类(Class),每个状态都是一个独立的类,拥有自己的行为、属性和转换规则。
- 优点:
- 面向对象:状态可以拥有方法(
isTransitionsAllowed()、serialize())。 - 类型安全:IDE 自动补全强。
- 职责清晰:状态转换逻辑封装在状态类内部。
- 面向对象:状态可以拥有方法(
- 适用场景:非常复杂的工作流,例如审批流(每个状态都有不同的表单、权限、通知逻辑)。
示例代码(简化):
// 1. 定义状态类
class PendingState extends OrderState
{
public function transitionTo(OrderState $nextState): OrderState
{
if ($nextState instanceof PaidState) {
// 执行支付逻辑
Mail::to($this->model->user)->send(new OrderPaidMail());
return $nextState;
}
throw new InvalidTransitionException($this, $nextState);
}
}
// 2. 在 Model 中使用
class Order extends Model
{
use HasStates;
// 定义状态列和对应的状态类映射
protected $casts = [
'status' => OrderState::class, // 自动反序列化
];
}
// 3. 使用
$order->status->transitionTo(new PaidState($order));
B. 枚举 + 业务逻辑层:spatie/laravel-enum + 自定义 Service
- 核心思想:用 枚举(Enum) 定义状态列表,用 Service 类 封装转换逻辑。
- 优点:
- 简单、轻量,不需要额外依赖太大的包。
- 对于状态数少、转换逻辑简单的场景非常合适。
- 适用场景:中等复杂度的工作流,例如文章发布状态(草稿、已发布、已删除)。
示例代码(简化):
// 1. 定义枚举
class OrderStatus extends Enum
{
const PENDING = 'pending';
const PAID = 'paid';
const SHIPPED = 'shipped';
}
// 2. 在 Service 中定义转换规则
class OrderService
{
public function pay(Order $order): void
{
$this->ensureTransitionAllowed($order->status, OrderStatus::PAID);
DB::transaction(function () use ($order) {
$order->status = OrderStatus::PAID;
$order->save();
// 执行动作
event(new OrderPaidEvent($order));
});
}
private function ensureTransitionAllowed(string $from, string $to): void
{
$allowedMap = [
OrderStatus::PENDING => [OrderStatus::PAID],
OrderStatus::PAID => [OrderStatus::SHIPPED],
// ...
];
if (!in_array($to, $allowedMap[$from] ?? [])) {
throw new \LogicException("Cannot transition from {$from} to {$to}");
}
}
}
什么时候应该用状态机?
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 简单状态(如:是否启用,草稿/发布) | 枚举 + Service | 状态机方案过于沉重,枚举足够。 |
| 中等复杂(如:订单状态) | 两种均可 | 如果未来可能变复杂,建议直接上状态机。 |
| 非常复杂(如:工作流审批、多级审核、状态行为差异大) | spatie/laravel-model-states |
状态类的多态性可以很好地处理每个状态独有的逻辑。 |
| 审批流(Workflow) | 状态机 + 工作流引擎(如 symfony/workflow) |
如果涉及并行、回滚、历史记录等高级功能,可以考虑 Symfony Workflow 包(有 Laravel 适配版本)。 |
不使用状态机可能遇到的问题
- 状态膨胀:
if-else if或switch-case在多个控制器、队列、事件监听器中重复出现。 - 隐形漏洞:
$order->update(['status' => 'shipped'])直接修改数据库,绕过了所有业务逻辑(如发货前必须已支付)。 - 难以审计:无法轻易知道一个状态为何可以转换到另一个状态,以及转换时发生了什么。
在 Laravel 工作流中,强烈推荐使用状态机模式,尤其是当你的业务逻辑包含明确的、有限的状态和转换规则时。
- 选
spatie/laravel-model-states:如果你希望状态本身拥有自己的方法、逻辑,并且喜欢强类型和面向对象。 - 选
Enum + Service:如果你的状态转换逻辑简单,团队规模小,希望快速开发。 - 不处理:如果工作流极其复杂(涉及并行、分支、合并),可以考虑专门的工作流引擎(如 Ruby 的
Workflow或 PHP 的Symfony Workflow),但 Laravel 生态中spatie/laravel-model-states已经能覆盖 90% 的业务场景了。
建议你从 spatie/laravel-model-states 入手,它的文档清晰,社区活跃,且设计优雅。