Eloquent Attribute Value Object States(VO状态)是社区实践的建模模式:将模型字段(如status)封装为不可变Value Object类,通过Eloquent访问器实现类型安全与语义清晰的状态管理,避免字符串魔法值、拼写错误及非法赋值,并支持行为扩展。

什么是Eloquent Attribute Value Object States(VO状态)
它不是 Laravel 内置功能,而是社区实践出的一种建模模式:把模型某个字段(如 status)的取值,封装成不可变的 PHP 类(Value Object),再通过 Eloquent 的 get*/set* 访问器,实现类型安全、语义清晰的状态管理。
核心目的不是炫技,而是避免散落在代码各处的字符串魔法值(比如 'pending'、'shipped'),防止拼写错误、误赋非法值,也方便后续扩展行为(如 $order->status->canBeRefunded())。
怎么定义一个不可变的状态 VO 类
以订单状态为例,先创建 Status 类。重点在「不可变」和「有限枚举」:
class Status
{
private string $value;
private function __construct(string $value)
{
if (!in_array($value, ['draft', 'confirmed', 'shipped', 'cancelled'], true)) {
throw new InvalidArgumentException("Invalid status: {$value}");
}
$this->value = $value;
}
public static function draft(): self { return new self('draft'); }
public static function confirmed(): self { return new self('confirmed'); }
public static function shipped(): self { return new self('shipped'); }
public static function cancelled(): self { return new self('cancelled'); }
public function value(): string { return $this->value; }
public function equals(self $other): bool { return $this->value === $other->value; }
// 可选:支持 JSON 序列化(存数据库/返回 API 时用)
public function __toString(): string { return $this->value; }
}
- 构造函数私有 + 静态工厂方法,确保只能通过预设方式创建实例
- 不提供 setter,也不暴露可变属性,天然不可变
-
__toString()是关键——Eloquent 存取时会自动调用它,否则会报Object of class Status could not be converted to string - 别忘了加
#[\Serializable](PHP 8.2+)或实现Serializable接口,否则队列或缓存可能失败
如何在 Eloquent 模型中挂载 VO 状态
靠访问器(accessor/mutator)桥接数据库字段与 VO 实例。假设订单表有 status 字符串字段:
立即学习“PHP免费学习笔记(深入)”;
class Order extends Model
{
protected $casts = [
'status' => 'string', // 保持原始字段为 string,VO 仅用于逻辑层
];
public function getStatusAttribute(?string $value): Status
{
return match ($value) {
'draft' => Status::draft(),
'confirmed' => Status::confirmed(),
'shipped' => Status::shipped(),
'cancelled' => Status::cancelled(),
default => Status::draft(), // 或抛异常,按需决定
};
}
public function setStatusAttribute(Status|string $value): void
{
$this->attributes['status'] = $value instanceof Status ? $value->value() : $value;
}
}
- getter 返回
Status实例,业务代码可直接调用$order->status->shipped()这类语义方法 - setter 支持传入
Status实例或原始字符串(兼容旧代码、表单提交等场景) -
$casts仍设为'string',因为数据库只存字符串;VO 不替代字段类型,只增强逻辑表达 - 别在
$casts里写Status::class——Laravel 的 cast 机制不支持自定义 VO,硬写会报错Class "Status" not found
常见踩坑点和边界情况
实际落地时,这几个地方最容易出问题:
- 数据库迁移没设默认值?插入新记录时
status为NULL,而 getter 中未处理null分支 → 报Match expression does not handle all possible values。务必在 getter 里加?? 'draft'或显式判断 - API 返回 JSON 时,
Status默认序列化成空对象{}。解决:给类加JsonSerializable接口,或在资源类(Resource)中手动转$this->status->value() - 查询作用域(scope)里不能直接用
where('status', Status::shipped())——数据库不认识对象。得写成where('status', Status::shipped()->value()) - 测试时 mock 状态?别 mock
Status类本身(它是值对象,不该被 mock),而是直接 new 实例或用已有的静态方法
VO 状态真正的复杂点不在定义,而在整个应用层的一致性:所有读写入口(API、命令、事件监听器)都得走模型访问器,而不是绕过它直操作 $model->status = 'xxx'。一旦漏掉一处,类型安全就破防了。



















