ThinkPHP 的 Model 默认不适合 Value Object,因其本质是可变数据容器,支持动态赋值、批量填充、自动完成及序列化,与值对象“构造即定、禁止修改”原则直接冲突。

ThinkPHP 本身不提供原生的不可变对象(Value Object)支持,也没有内置 readonly 类或属性机制(PHP 8.2+ 的 readonly class 在 ThinkPHP 项目中需手动配合使用)。想在 ThinkPHP 中真正落地值对象设计,**必须绕过框架默认行为,自行封装 + 严格约定**,否则极易因模型赋值、自动填充、序列化/反序列化等环节破坏不可变性。
为什么 ThinkPHP 的 Model 默认不适合 Value Object?
ThinkPHP 的 Model 类本质是可变的“数据容器”:它允许动态设置属性($model->name = 'xxx')、支持批量赋值($model->data($data))、会触发自动完成(setAttr)、还能被 toArray() 或 JSON 序列化直接导出。这些特性与值对象“构造即定、禁止修改”的契约直接冲突。
-
$user = new User(['name' => 'Alice']); $user->name = 'Bob';—— 这行代码在 ThinkPHP 模型里完全合法,但已违反值对象原则 - 数据库查询返回的模型实例,其属性默认可写,且无运行时防护
- 即使你把属性设为
private+readonly(PHP 8.1+),ThinkPHP 的底层反射赋值(如Hydrator)仍可能绕过构造逻辑直接写入属性
PHP 8.1+ readonly 属性 + 手动构造,是最小侵入方案
如果你用的是 PHP 8.1 或更高版本,且项目可控(非全量迁移旧模型),推荐用纯 PHP 值对象类,**完全脱离 ThinkPHP Model 继承链**:
readonly class Email
{
public function __construct(public string $value)
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email format');
}
}
public function toString(): string
{
return $this->value;
}
}
- 绝不继承
\think\Model,避免框架魔改行为 - 所有属性声明为
public readonly,PHP 运行时强制拦截修改 - 校验逻辑放在构造函数内,确保无效状态无法存在
- 在 ThinkPHP 控制器或服务层中,用它接收参数或封装返回值:
$email = new Email($request->post('email'));
ThinkPHP 数据库结果转不可变对象,必须做防御性复制
从数据库查出来的数据(比如 UserModel::find(1))永远是可变模型实例。若需作为值对象使用,不能直接返回模型,而要显式提取并冻结:
- 不要:
return UserModel::find(1);—— 外部可任意改$user->status = 99; - 要:
return new UserValueObject($user->toArray());,其中UserValueObject是你定义的只读类 - 注意嵌套对象:如果
$user->toArray()返回的数组里含对象(如关联模型),需递归转为只读结构,或用json_encode() + json_decode()断开引用(仅适用于简单结构) - 避免用
__clone()防御——ThinkPHP 模型的clone行为不可靠,且不解决深层引用问题
容易被忽略的深坑:JSON 序列化与请求验证器
ThinkPHP 的 Validate 和 Request 类默认返回可变数组或模型。即使你写了只读类,若验证器规则没约束好,依然会传入非法数据:
立即学习“PHP免费学习笔记(深入)”;
-
Validate的scene或rule必须覆盖所有字段,否则new Email($data['email'] ?? '')可能收到空字符串或 null -
Request的param()返回的是引用数组,若后续被其他逻辑修改(如中间件注入字段),你的只读对象构造前就已被污染 - JSON 接口响应中,若直接
return ['email' => $email],PHP 会调用__toString()或抛异常(取决于实现),需统一加->toString()或定义JsonSerializable
真正难的不是写一个 readonly class,而是让整个请求生命周期——从输入验证、DB 查询、业务处理到输出——都尊重“值一旦确立就不该再变”这个契约。ThinkPHP 不帮你守门,得你自己在每道关卡插桩。



















