<p>值对象必须用 class 封装而非 array——因其需不可变性、类型安全、字段约束、IDE 提示及可控序列化;PHP 8.2+ 用 readonly 属性,8.1- 用 private 属性+getter+构造赋值;须实现 equals() 显式值比较,禁用 == 和 json_encode 比较;JSON 序列化应实现 JsonSerializable 统一字段名;仅当数据无业务含义、生命周期极短或无需复用时才考虑 array。</p>

值对象该用 class 还是 array?
值对象(Value Object)在 PHP 里本质是「不可变、无身份、靠值判断相等」的数据载体。直接用 array 或 stdClass 看似省事,但很快会暴露问题:类型不安全、无法约束字段、序列化/反序列化行为不可控、IDE 不提示、测试难覆盖。
所以封装成 class 是唯一合理选择——哪怕它只有 public readonly 属性和一个构造函数。
- PHP 8.2+ 强烈推荐用
readonly属性,天然防篡改;8.1 及以下可用private+ getter + 构造时赋值 - 别加 setter、别暴露修改接口,否则就不是值对象了
- 如果字段有业务含义(比如
email、price),在构造时做基础校验(非空、格式、范围),失败抛InvalidArgumentException
怎么实现值相等判断(== 和 === 的区别)
PHP 的 === 比较两个对象永远是 false(地址不同),而值对象需要的是「值相等」。不能依赖 json_encode 或 var_export 做比较——顺序、浮点精度、null 处理都可能翻车。
正确做法是显式定义 equals() 方法,逐字段比对:
立即学习“PHP免费学习笔记(深入)”;
public function equals(self $other): bool
{
return $this->id === $other->id
&& $this->name === $other->name;
}
- 所有字段必须参与比较,漏掉一个就可能误判
- 用
===而非==,避免隐式类型转换(比如'1'==1为 true,但值对象里这通常不是你想要的) - 别在
__toString()或__serialize()里做逻辑判断,那是序列化职责,不是相等性职责
JSON 序列化时字段名怎么对齐 API?
值对象常要转成 JSON 发给前端或存入缓存。默认 json_encode($vo) 会暴露类名、包含不可序列化属性、字段名大小写不匹配 API 规范(比如后端用 userId,API 要 user_id)。
最轻量方案是实现 JsonSerializable 接口:
class UserId implements JsonSerializable
{
public function __construct(public readonly int $value) {}
public function jsonSerialize(): array
{
return ['user_id' => $this->value];
}
}
- 不要重写
__toArray()或搞魔术方法,那会让调用方困惑且 IDE 难推导 - 如果项目用了 Symfony Serializer 或 Laravel 的
toArray(),优先复用其注解或契约,避免多套序列化逻辑并存 - 注意 null 字段:值对象本身不应含 null(构造时就该拒绝),所以
jsonSerialize()返回数组里也不该出现 null 键值
什么时候不该封装成值对象?
不是所有「小数据包」都适合做成值对象。典型误用场景:
- 只在单个方法内临时拼装、生命周期极短(比如
[$a, $b, $c]传给一个私有 helper),用array更清晰 - 字段会频繁变更、需支持部分更新(比如用户资料表单提交的 DTO),那是「传输对象」,不是值对象
- 含资源句柄、闭包、Doctrine Entity 引用等不可序列化内容——值对象必须可自由复制、传递、缓存
- 业务规则极其简单且无复用诉求(如
new Price(99)就是数字包装),先用类型声明(PHP 8+int|float)+ 文档说明,等真出现校验/转换/格式化需求再升级为类
值对象的价值不在“封装”,而在「把隐式约定变成显式契约」。一旦开始重复写校验逻辑、字段命名不一致、测试里不断 mock 数组结构,就是该抽离的信号。



















