状态模式(State):订单状态机(待支付 → 已支付 → 已发货)在 Laravel 中如何实现?是否使用状态模式?
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。Context(上下文):拥有状态的对象(如OrderState(状态接口):定义状态行为(如ConcreteState(具体状态):实现具体行为(如PaidState关键将状态相关的行为封装在状态类中,而非 Context 中。ContextOrder模型;StateOrderState接口;PaidState。if (!/
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 核心未内置状态模式,但在订单、工单、审批流等复杂状态场景中,状态模式是解决“状态爆炸”和“逻辑混乱”的利器。它通过:
状态接口 + 具体状态类 + 上下文委托
实现了:
- 状态行为的封装与隔离;
- 状态转换规则的显式声明;
- 业务逻辑的高内聚、低耦合。
正如你所坚持的:好的架构不是预测所有变化,而是让变化发生时,修改最小化。
状态模式正是这一理念的典范——当你新增一个“已退货”状态,只需写一个类,订单模型一行代码不动。
更多推荐



所有评论(0)