PHP 8.3 允许在 __clone() 中对本类声明的 readonly 属性赋值一次,需显式直接赋值;嵌套 readonly 对象须逐层实现 __clone(),且不可对继承的 readonly 属性操作。
PHP 8.3 中
能改
属性,但仅限一次
PHP 8.3 之前,
属性一旦在构造函数中赋值,就彻底不可变;哪怕在
里尝试重设,也会直接报
。8.3 放开了这个限制——只允许在
方法体内对每个
属性**赋值一次**,且必须是该方法的直接执行路径(不能套在条件分支或函数调用里隐式触发)。
这是唯一被允许绕过
冻结的场景,其他任何地方(包括
、反射
、序列化后手动修改)仍会失败
赋值必须显式写出,例如
,不能通过引用传递或数组解构间接修改
如果属性本身是对象(如
),你得自己
它,否则只是浅拷贝引用
深拷贝只读对象时,
必须手动处理嵌套只读结构
PHP 不会自动递归克隆只读属性里的只读子对象。比如一个
包含
,而
本身也是
类,那么你在
里只写
是不够的——因为
同样需要存在并正确处理它自己的只读属性。
缺少子类的
方法会导致
失败,抛出
若子类未定义
,
操作会跳过只读属性初始化,导致新对象的只读属性保持未初始化状态(
或默认值),后续访问可能触发
推荐做法:所有参与深拷贝链路的只读类,都显式声明
,哪怕只是空实现(用于占位),再逐层委托
用
检查是否真能被克隆修改
不是所有标了
的属性都能在
里改——只有**类自身声明的属性**才适用该规则。如果属性来自父类(哪怕父类也是
类),子类的
无法触碰它,反射会返回
,但运行时仍报错。
PHP 8.5.5
PHP 8.5.5 是 PHP 8.5 分支的维护更新版本。该版本延续了“小步快跑”的迭代逻辑,通过深度错误修复、底层性能微调以及安全加固,旨在为开发者提供一个更健壮、更高效的运行环境。该版本严格遵守语义化版本规范,不包含破坏性变更。
下载
用
可以提前判断属性是否具备“可被
初始化”的资格,但它不区分“本类声明”和“继承而来”
要确认能否安全赋值,还需结合
框架或通用克隆工具里,建议先检查声明类,再决定是否尝试赋值,避免无谓的致命错误
和
一起用时容易忽略的兼容性问题
如果你给
加上
属性,PHP 会强制检查父类是否存在同名方法。但
是魔术方法,父类通常不会显式定义它——这意味着加了
反而会让代码直接崩溃,报
。
立即学习
“
PHP免费学习笔记(深入)
”;
本质是“拦截型”魔术方法,不是为覆盖设计的,加
没有意义,且破坏兼容性
只有当你明确继承了一个定义了
的父类(极少见),才考虑加该属性;否则一律不要加
混淆点:有人误以为加了
就能“启用”克隆时修改只读属性,其实完全无关——该能力由 PHP 8.3 运行时硬编码支持,不依赖任何属性
实际写
时,最易被忽略的是嵌套只读对象的初始化顺序和所有权转移。你以为 clone 了一次就万事大吉,但漏掉一层,整个深拷贝链就断在中间。
__clone()readonlyreadonly__clone()Fatal error: Cannot modify readonly property__clone()readonlyreadonly__wakeup()setValue()$this->createdAt = new DateTime();public readonly PHP $php;clone__clone()readonly class Postpublic readonly Author $authorAuthorreadonlyPost::__clone()$this->author = clone $this->author;Author::__clone()__clone()clone $this->authorFatal error: Cannot modify readonly property__clone()cloneNULLTypeError__clone()ReflectionProperty::isReadOnly()readonly__clone()readonly__clone()true$prop->isReadOnly()__clone()$prop->getDeclaringClass() === $reflectedClass#[\Override]__clone()__clone()#[\Override]__clone()#[\Override]Fatal error: ... has #[\Override] attribute, but no matching parent method exists__clone()#[\Override]__clone()#[\Override]__clone()