Laravel 本身并未内置状态机组件,但“订单状态流转”这类场景正是状态模式(State Pattern)的经典用武之地。虽然 Laravel 核心未强制使用状态模式,但在复杂业务系统中,通过状态模式实现订单状态机是推荐的最佳实践,它能有效避免“巨型 if-else”或“状态硬编码”,使状态转换逻辑清晰、可扩展、可测试。


一、状态模式的核心思想(GoF 定义)

允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类

  • Context(上下文):拥有状态的对象(如 Order);
  • State(状态接口):定义状态行为(如 handlePayment());
  • ConcreteState(具体状态):实现具体行为(如 PendingState, PaidState);
  • 关键将状态相关的行为封装在状态类中,而非 Context 中

在订单系统中:

  • Context = Order 模型;
  • State = OrderState 接口;
  • ConcreteState = PendingState, PaidState, ShippedState

二、Laravel 中实现订单状态机的两种方式

方式 1:简单状态字段(反模式)
// app/Models/Order.php
class Order extends Model
{
    public function markAsPaid()
    {
        if ($this->status !== 'pending') {
            throw new LogicException('Order is not pending');
        }
        $this->status = 'paid';
        $this->save();
    }

    public function ship()
    {
        if ($this->status !== 'paid') {
            throw new LogicException('Order is not paid');
        }
        $this...status = 'shipped';
        $this->save();
    }
}
问题:
  • 状态逻辑散落在模型中
  • 新增状态需修改模型(违反开闭原则);
  • 状态转换规则隐式(需阅读代码才能知道 paid → shipped 是否合法);
  • 难以测试(需构造特定状态)。

这是“状态字段 + 行为方法”的混合体,不是状态模式


方式 2:状态模式实现(推荐)
步骤 1:定义状态接口
// app/Orders/OrderState.php
interface OrderState
{
    public function markAsPaid(Order $order): void;
    public function ship(Order $order): void;
    public function cancel(Order $order): void;
}
步骤 2:实现具体状态
// app/Orders/States/PendingState.php
class PendingState implements OrderState
{
    public function markAsPaid(Order $order): void
    {
        $order->status = 'paid';
        $order->state = new PaidState(); // ← 关键:状态对象变更
        $order->save();
    }

    public function ship(Order $order): void
    {
        throw new LogicException('Cannot ship pending order');
    }

    public function cancel(Order $order): void
    {
        $order->status = 'cancelled';
        $order->state = new CancelledState();
        $order->save();
    }
}

// app/Orders/States/PaidState.php
class PaidState implements OrderState
{
    public function markAsPaid(Order $order): void
    {
        throw new LogicException('Order is already paid');
    }

    public function ship(Order $order): void
    {
        $order->status = 'shipped';
        $order->state = new ShippedState();
        $order->save();
    }

    public function cancel(Order $order): void
    {
        // 已支付订单取消需退款
        RefundService::process($order);
        $order->status = 'cancelled';
        $order->state = new CancelledState();
        $order->save();
    }
}
步骤 3:Order 模型持有状态对象
// app/Models/Order.php
class Order extends Model
{
    protected $casts = [
        'state' => StateCaster::class, // ← 自定义 caster 序列化状态
    ];

    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);
        $this->state = $this->state ?? new PendingState();
    }

    public function markAsPaid(): void
    {
        $this->state->markAsPaid($this);
    }

    public function ship(): void
    {
        $this->state->ship($this);
    }
}
步骤 4:自定义 Caster(序列化状态对象)
// app/Orders/StateCaster.php
class StateCaster implements CastsAttributes
{
    public function get($model, string $key, $value, array $attributes)
    {
        if (! $value) return new PendingState();
        $class = 'App\\Orders\\States\\' . ucfirst($attributes['status']) . 'State';
        return new $class();
    }

    public function set($model, string $key, $value, array $attributes)
    {
        return $value::class; // 存储类名
    }
}

状态行为由状态类封装,Order 模型仅委托调用


三、状态模式 vs 简单状态字段

特性 简单状态字段 状态模式
状态行为 散落在模型中 封装在状态类中
新增状态 修改模型(违反 OCP) 新增状态类(符合 OCP)
状态规则 隐式(需读代码) 显式(状态类即文档)
可测试性 需构造状态 直接测试状态类
复杂逻辑 模型臃肿 逻辑分散到状态类

状态模式让“状态转换”成为一等公民


四、Laravel 生态中的状态机包

虽然可手动实现,但 Laravel 社区有成熟的状态机包:

1. spatie/laravel-model-states(推荐)
  • 专为 Eloquent 设计;
  • 支持状态转换、守卫、事件;
  • 无需手动管理状态对象。
示例:
// app/Models/Order.php
use Spatie\ModelStates\HasStates;

class Order extends Model
{
    use HasStates;

    protected function registerStates(): void
    {
        $this->addState('status', OrderStatus::class)
             ->allowTransition(Pending::class, Paid::class)
             ->allowTransition(Paid::class, Shipped::class)
             ->allowTransition([Paid::class, Shipped::class], Cancelled::class);
    }
}

// app/Models/States/OrderStatus.php
abstract class OrderStatus extends State
{
    abstract public function label(): string;
}

class Pending extends OrderStatus
{
    public function label(): string { return 'Pending'; }

    public function pay(): void
    {
        $this->model->transitionTo(Paid::class);
    }
}

这是状态模式的现代化、Laravel 化实现

2. winzou/state-machine
  • 更通用的状态机库;
  • 需手动集成到模型。

五、与你工程理念的深度对齐

你的原则 在状态模式中的体现
关注点分离 状态行为与模型数据分离
可扩展性 新增状态无需修改现有代码
可测试性 状态类可独立单元测试
避免硬编码 状态转换规则显式声明
SOLID 遵循 符合开闭原则(OCP)、单一职责(SRP)

六、何时使用状态模式?

场景 推荐方式
简单状态(< 3 个,无复杂逻辑) 简单状态字段 + 守卫方法
复杂状态机(> 3 个状态,有转换规则、副作用) 状态模式spatie/laravel-model-states
需要审计日志、事件、守卫 使用状态机包

状态模式不是银弹,但在复杂业务中是必要解耦手段


七、完整最佳实践示例(使用 Spatie 包)

1. 安装
composer require spatie/laravel-model-states
2. 定义状态
// app/Models/States/OrderStatus.php
abstract class OrderStatus extends State
{
    abstract public function canBePaid(): bool;
    abstract public function pay(Order $order): void;
}

class Pending extends OrderStatus
{
    public function canBePaid(): bool { return true; }
    public function pay(Order $order): void
    {
        $order->transitionTo(Paid::class);
        Mail::to($order->user)->send(new OrderPaidMail($order));
    }
}

class Paid extends OrderStatus
{
    public function canBePaid(): bool { return false; }
    public function pay(Order $order): void
    {
        throw new LogicException('Already paid');
    }
}
3. 模型集成
class Order extends Model
{
    use HasStates;

    protected function registerStates(): void
    {
        $this->addState('status', OrderStatus::class)
             ->default(Pending::class)
             ->allowTransition(Pending::class, Paid::class);
    }
}
4. 使用
$order = Order::find(1);
if ($order->status->canBePaid()) {
    $order->status->pay($order);
}

状态行为、转换规则、副作用全部封装,模型保持纯净


结语

虽然 Laravel 核心未内置状态模式,但在订单、工单、审批流等复杂状态场景中,状态模式是解决“状态爆炸”和“逻辑混乱”的利器。它通过:

状态接口 + 具体状态类 + 上下文委托

实现了:

  • 状态行为的封装与隔离
  • 状态转换规则的显式声明
  • 业务逻辑的高内聚、低耦合

正如你所坚持的:好的架构不是预测所有变化,而是让变化发生时,修改最小化
状态模式正是这一理念的典范——当你新增一个“已退货”状态,只需写一个类,订单模型一行代码不动

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐