PHP readonly 对象适合配置值还是领域实体
来源:17golang原创
时间:2026-10-08 21:53:13 178浏览 收藏
PHP readonly 对象更适合配置快照和值对象,而不是默认用来承载会不断改变状态的领域实体。判断标准不是“这个类重要不重要”,而是它创建后是否应该保持同一组值:连接配置、金额、日期范围、标识符通常适合;订单、购物车、工单这类有身份和生命周期的实体,通常保留可变状态更自然,再把其中的金额、编号等局部类型设计成 readonly 值对象。
PHP 官方手册:https://www.php.net/manual/en/language.oop5.basic.php
- 创建后只读取、修改时整体替换:优先
readonly class。 - 需要
pay()、cancel()等状态迁移:实体本身通常不要整体只读。 - 属性里保存普通对象时只是“引用不能替换”,对象内部仍可能变化。
- PHP 8.2 开始支持只读类;PHP 8.3 可在
__clone()中重新初始化只读属性以完成深克隆。
先确认 readonly class 真正限制了什么
从 PHP 8.2 起,类可以声明为 readonly。它会让所有实例属性都具有只读约束,并禁止动态属性。类中的属性必须有类型,不能声明静态属性;属性初始化后不能再次赋值、递增、通过引用修改或 unset。
timeoutSeconds = 30;
只读类还限制继承关系。编写库代码时,不要只考虑当前类能否工作,还要考虑调用方是否需要通过子类增加可变属性。对稳定的小类型,这个限制通常有利;对扩展点较多的框架基类,它可能过早锁死设计。
配置和值对象为什么更适合 readonly
配置对象的职责通常是把一组已经解析、校验过的参数交给服务使用。业务运行期间如果要改变配置,更清晰的方式是重新生成一个新对象并替换旧快照,而不是让多个组件共享一个会被悄悄修改的实例。

baseUrl, $seconds, $this->retryPolicy);
}
}
withTimeout() 的优点是调用点能看出“创建新值”,测试也不必担心其他对象持有的配置被联动修改。金额、邮箱地址、坐标、日期范围和业务编号同样符合这个特征:相等性主要由值决定,修改意味着得到另一个值。
readonly 是属性槽只读,不是递归冻结
最容易误判的地方是对象属性。只读属性保存一个对象时,PHP 限制的是属性槽不能改绑到另一个对象;被引用对象的内部状态仍可能变化。官方手册把这种情况称为内部可变性。

roles[] = 'admin'; // Address 引用没有被替换,但普通对象内部仍可修改。 $view->address->city = 'Shanghai';
因此,如果配置对象需要真正接近深度不可变,它引用的子对象也应设计成 readonly,或使用 DateTimeImmutable 这类不可变类型。仅把最外层类加上关键字,不能自动冻结整张对象图。
PHP 8.3 增加了一个实用边界:在 __clone() 中可以重新初始化只读属性,便于克隆时把内部对象也克隆一份。这个能力适合“复制后调整”的值对象,但依然要明确哪些子对象需要深克隆,不能把普通 clone 当成自动深复制。
有身份和生命周期的领域实体通常保留可变状态
领域实体与值对象的差别在于身份和生命周期。订单即使状态从“待支付”变成“已支付”,仍然是同一个订单。若把整个 Order 声明为只读,每次状态变化都要创建新实例,还要重新处理 ORM 跟踪、事件、关联集合和并发版本,成本往往高于收益。
status !== OrderStatus::Pending) {
throw new DomainException('当前订单不能支付');
}
if ($received != $this->total) {
throw new DomainException('支付金额不匹配');
}
$this->status = OrderStatus::Paid;
}
}
这是一种更实用的组合:实体的 status 可以在受控方法内变化,身份和金额使用只读值对象,外部仍不能直接篡改关键属性。它既保留领域行为,也把真正不该变化的部分锁住。
什么时候领域实体也可以整体 readonly
整体只读并不是绝对错误。如果项目采用事件溯源、函数式建模,或实体更新本来就通过“旧对象 + 事件 = 新对象”完成,那么返回新实例可能正是设计目标。只要持久化层、集合关联和并发控制都按不可变模型实现,readonly 可以帮助语言层表达约束。
采用前至少回答四个问题:
- 状态变化是否天然返回新实例,而不是依赖 ORM 脏检查?
- 关联对象是否同样不可变,还是会造成浅只读错觉?
- 新实例如何保留同一业务身份和并发版本?
- 事件发布、序列化和缓存键是否能正确处理对象替换?
如果这些问题没有明确答案,优先选“可变实体 + readonly 值对象”,通常更容易与现有 PHP 框架和 ORM 协作。
选型速查表
| 对象类型 | 整体 readonly | 推荐方式 |
|---|---|---|
| 环境配置、HTTP 客户端参数 | 适合 | 构造时校验,变更时生成新快照 |
| Money、OrderId、Email、DateRange | 适合 | 让值对象自校验并保持不可变 |
| DTO、查询结果视图 | 通常适合 | 注意内部对象的浅只读问题 |
| Order、Cart、Ticket 等实体 | 通常不适合 | 用行为方法控制状态,把关键字段做成 readonly 值对象 |
| 事件溯源中的不可变聚合快照 | 可以 | 确认持久化、版本和关联都支持对象替换 |
| 框架扩展基类 | 谨慎 | 先评估继承、Trait 属性和扩展需求 |
常见误区
readonly 等于 const 对象吗?
不完全等同。它约束属性初始化后的重新赋值,但对象属性指向的普通对象仍可能修改内部状态,所以不是递归常量。
readonly 属性可以保存数组吗?
可以,但初始化后不能追加、删除或修改数组元素,因为这些操作属于对只读属性的间接修改。
readonly class 可以有方法吗?
可以。方法可以执行计算、校验并返回新对象,只是不能在初始化完成后重新写入只读属性。
使用 readonly 后还需要 private 吗?
仍然需要按封装目标选择可见性。只读解决“能否重新赋值”,可见性解决“谁能读取或在允许的初始化作用域内设置”。两者职责不同。
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
文章 · php教程 | 3小时前 | PHP · php-fpm · PHP OPcache opcache_reset validate_timestamps revalidate_freq opcache_invalidate382 收藏
-
117 收藏
-
167 收藏
-
448 收藏
-
341 收藏
-
305 收藏
-
284 收藏
-
413 收藏
-
135 收藏
-
246 收藏
-
162 收藏
-
417 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习