PHP readonly 属性克隆对象时的状态复制边界
结论先说:PHP 的 clone 会创建新的外层对象,但对象属性中的对象引用默认仍按浅复制处理。readonly 只限制属性槽位被重新赋值,并不会自动把它指向的可变对象深复制。因此,原对象和克隆对象可能各自拥有一个只读槽位,却共同指向同一个可变对象。
从 PHP 8.3 开始,可以在 __clone() 执行期间把已初始化的 readonly 属性重新初始化一次。这正是隔离嵌套可变对象的窗口;离开这个窗口、对同一属性写第二次,或者在 PHP 8.1/8.2 上执行同样代码,都会触发 Error。
本文依据 PHP 官方手册与 Readonly amendments RFC 核对版本行为:
# PHP 属性与 readonly 官方手册
https://www.php.net/manual/en/language.oop5.properties.php
# readonly 克隆重新初始化规则的官方 RFC
https://wiki.php.net/rfc/readonly_amendments
先把复制边界说清:外层对象复制,不等于对象图深复制
假设订单快照里有一个 readonly 的 DateTime。执行 clone $snapshot 后,快照本身已经是另一个实例,但两个快照的 capturedAt 起初仍指向同一个 DateTime 对象。readonly 阻止的是 $snapshot->capturedAt = ... 这种重新赋值,不会阻止 $snapshot->capturedAt->modify(...) 对嵌套对象内部状态的修改。
capturedAt->modify('+1 day');
// 两个属性仍指向同一对象,所以原对象也会看到日期变化。
var_dump($original->capturedAt === $copy->capturedAt); // true
这类问题最容易被“外层对象编号已经不同”掩盖。判断是否真正隔离,不能只比较 $original !== $copy,还要比较需要隔离的嵌套对象。

PHP 8.3 的门禁:只在 __clone() 执行期重新初始化一次
PHP 8.3 放开的不是“readonly 可以随便改”,而是一个非常窄的生命周期门禁:对象已经由引擎复制完成、正在执行该对象的 __clone(),并且目标 readonly 属性在这次克隆中还没有被重新初始化。满足这些条件时,可以为它换一个新值。
最小的深复制写法如下。它保留原对象不变,只让克隆对象的属性改为指向新的 DateTime 实例。
capturedAt = clone $this->capturedAt;
}
}
$original = new Snapshot(new DateTime('2026-10-10 10:00:00'));
$copy = clone $original;
// 克隆对象修改自己的嵌套对象,不再影响原对象。
$copy->capturedAt->modify('+1 day');
var_dump($original !== $copy); // true
var_dump($original->capturedAt !== $copy->capturedAt); // true
如果最低运行版本仍是 PHP 8.2,不能仅靠条件判断绕过运行时错误;应继续在工厂或 wither 方法里调用构造器创建新对象,或者把最低版本提升到 8.3 后再启用这条路径。

不是每个 readonly 属性都要深复制
标量、数组与对象要分开判断。字符串、整数等标量随对象复制后没有共享可变实例的问题。PHP 数组本身采用写时复制,通常也不需要在 __clone() 里为了隔离而重复赋值。真正需要重点检查的是对象和资源。
- 值类型或不可变对象:例如字符串、整数、
DateTimeImmutable,通常可以保持默认复制。 - 可变对象:例如
DateTime、可变集合、可写配置对象,需要判断克隆后是否必须独立。 - 有意共享的服务对象:记录器、连接池或无状态服务可能就应共享,盲目深复制反而破坏语义。
- 对象组成的数组:数组外壳会分离,但数组元素仍可能是共享对象,需要逐个按业务语义处理。
所以规则不是“看到 readonly 就 clone”,而是先画出对象所有权:这个子对象由谁拥有,克隆后是否允许共同变化,复制成本是否可接受。readonly 解决的是属性写入约束,不等于完整的不可变对象模型。
一次写入是硬边界,unset 也算重新初始化流程的一部分
在 __clone() 中,同一个 readonly 属性只能重新初始化一次。先赋回原值再赋新值并不是两阶段更新,而是第二次写入,后一次会报错。RFC 还允许在克隆执行期先 unset(),之后再初始化;这主要服务于惰性属性或魔术访问器,不应成为普通深复制的默认写法。
capturedAt = $this->capturedAt;
// 第二次写入会抛出 Error,不能把 readonly 当临时变量使用。
$this->capturedAt = clone $this->capturedAt;
}
PHP 8.4 还明确禁止在 __clone() 中通过引用间接修改 readonly 属性,例如获取 &$this->capturedAt 的引用。迁移旧代码时,应使用清晰的直接赋值,不要依赖引用技巧。
把验证做成双向测试,而不是只看一次输出
好的回归测试至少验证三件事:外层对象不同、需要隔离的嵌套对象不同、任意一侧改变都不会串到另一侧。仅验证初始值相等是不够的,因为浅复制与深复制在刚完成时看起来完全一样。
capturedAt !== $copy->capturedAt);
// 再修改克隆侧,确认原对象仍保持原值。
$copy->capturedAt->modify('+1 day');
assert($original->capturedAt->format('Y-m-d') === '2026-10-10');
assert($copy->capturedAt->format('Y-m-d') === '2026-10-11');
如果类里有多个可变子对象,逐个列入测试。新增 readonly 属性时,也应把“共享还是隔离”作为代码评审项,否则未来一次看似安全的属性扩展就可能重新引入共享状态。
兼容性落地清单
- 在
composer.json和部署镜像中确认最低 PHP 版本;使用重新初始化能力时最低必须是 PHP 8.3。 - 枚举 readonly 属性里的对象,标注“不可变、需要隔离、有意共享”三种所有权。
- 只在
__clone()中直接替换需要隔离的属性,每个属性只写一次。 - 避免通过引用修改 readonly 属性,尤其是目标环境包含 PHP 8.4 及更高版本时。
- 测试对象身份和双向变更,不用单次序列化输出代替引用隔离验证。
- 对 PHP 8.1/8.2 保持构造器重建路径,不要让同一套包在低版本运行时才暴露致命错误。
常见问题
readonly 属性里的对象完全不能改吗?
不是。readonly 阻止属性被重新赋值,但对象内部仍可能被修改,这就是“内部可变性”。要完全不可变,应优先保存不可变对象,或在 API 层同时约束其修改方法。
PHP 8.2 中的 __clone() 能重新赋值 readonly 属性吗?
不能。已初始化 readonly 属性在 PHP 8.2 中仍不能于克隆时重新初始化。需要用构造器创建新实例,或把最低运行版本升级到 PHP 8.3。
为什么 clone 后 readonly 的 DateTime 仍会互相影响?
因为默认 clone 是浅复制,两个 readonly 属性槽位都保存了同一个 DateTime 引用。readonly 没有自动深复制语义。
可以在 __clone() 里连续修改同一个 readonly 属性吗?
不可以。每个属性在一次克隆执行中只能重新初始化一次,第二次写入会抛出 Error。
DateTimeImmutable 也需要在 __clone() 中复制吗?
通常不需要。它的修改方法返回新实例,不会原地改变旧实例。除非业务明确要求不同对象身份,否则共享不可变实例更简单。
json/v2 解码 null 到指针字段的兼容处理
- 上一篇
- json/v2 解码 null 到指针字段的兼容处理
- 下一篇
- encoding/json/v2 用 StringifyNumbers 兼容旧数字字段
-
- 文章 · php教程 | 11小时前 | 序列化 · 工程实践 · php教程 · 兼容性 · 数据迁移 对象序列化 __unserialize PHP __serialize 兼容字段
- PHP 序列化对象时 __serialize 怎样控制兼容字段
- 398浏览 收藏
-
- 文章 · php教程 | 13小时前 |
- PHP FFI 调用本地库时如何管理指针生命周期
- 284浏览 收藏
-
- 文章 · php教程 | 15小时前 | PHP ·
- PHP OPcache JIT 调试信息如何定位未编译的函数
- 295浏览 收藏
-
- 文章 · php教程 | 19小时前 |
- PHP match 表达式怎样覆盖枚举分支并保持穷尽
- 377浏览 收藏
-
- 文章 · php教程 | 22小时前 | php教程 · PHP生成器 yield from Generator send getReturn
- PHP 生成器如何双向传值并接收最终返回值
- 208浏览 收藏
-
- 文章 · php教程 | 1天前 |
- PHP readonly 类继承时有哪些属性限制
- 223浏览 收藏
-
- 文章 · php教程 | 1天前 |
- PHP ReflectionReference 如何判断数组元素是否共享引用
- 376浏览 收藏
-
- 文章 · php教程 | 1天前 | php教程 · PHP 8.4 · php ReflectionClass newLazyGhost newLazyProxy lazy object 重量级服务
- PHP lazy object 如何延迟创建重量级服务
- 202浏览 收藏
-
- 文章 · php教程 | 1天前 | 面向对象 · PHP · PHP 8.4 · PHP非对称属性可见性 private(set) protected(set) PHP 8.4属性 PHP对象封装
- PHP 非对称属性可见性如何限制对象外部写入
- 216浏览 收藏
-
- 文章 · php教程 | 1天前 | 内存管理 · php教程 · 弱引用 PHP 8 SplObjectStorage PHP WeakMap 对象元数据
- PHP WeakMap 为什么适合保存对象附加元数据
- 227浏览 收藏
-
- 文章 · php教程 | 1天前 | PHP · 异步编程 · php教程 · 异步回调 事件循环 PHP Fiber Fiber suspend Fiber resume
- PHP Fiber 如何让同步接口适配事件循环
- 272浏览 收藏
-
- 文章 · php教程 | 1天前 |
- PHP readonly 对象适合配置值还是领域实体
- 178浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 402次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 478次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 487次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 433次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 260次使用
-
- PHP JSON_THROW_ON_ERROR 抛错后怎么保留原始字段位置
- 2026-09-09 501浏览
-
- PHP 8.5 array_last() 怎么处理空数组:从 null 结果到兼容旧版本的 Polyfill
- 2026-08-16 501浏览
-
- 宝塔配置Ruby环境:RVM+Nginx反代教程
- 2026-05-29 501浏览
-
- unset函数作用范围详解
- 2026-05-29 501浏览
-
- VS Code配置Xdebug教程:PHP调试技巧全解析
- 2026-05-13 501浏览

