php 8.3 的 readonly 属性在 clone 时保持只读,深拷贝必须通过静态工厂方法手动重建对象并递归处理嵌套只读实例,反射强制写入属未定义行为且不可靠。

PHP 8.3 只读属性不能被 clone 修改,深拷贝必须绕过只读限制
PHP 8.3 的 readonly 属性在对象克隆时仍保持只读语义——clone 不会复制出可写副本,而是直接复用原属性引用(尤其对嵌套对象/数组)。这意味着标准 clone 关键字无法实现真正“可修改的深拷贝”,你拿到的仍是带只读约束的对象实例。
手动重建对象:唯一安全的深拷贝方式
必须显式构造新实例,并逐个赋值属性。PHP 不允许在构造后写入 readonly 属性,所以所有数据必须在 __construct() 中注入。常见错误是试图在 __clone() 里赋值,这会直接报 Fatal error: Cannot modify readonly property。
- 只读类应设计为“不可变即构造完成”,因此深拷贝逻辑应封装在静态工厂方法中,如
from()或withChanged(...) - 对嵌套的
readonly对象,需递归调用其对应工厂方法,不能用unserialize(serialize($obj))—— 序列化会保留只读状态,反序列化后仍是只读 - 若属性是数组且含只读对象,需用
array_map()逐个转换,而非array_replace_recursive()(后者不触发构造逻辑)
// 示例:从现有只读对象创建可修改副本
class Config
{
public function __construct(
public readonly string $host,
public readonly int $port,
public readonly array $features,
) {}
public static function deepClone(Config $original): self
{
return new self(
$original->host,
$original->port,
array_map(fn($f) => $f instanceof Feature ? Feature::deepClone($f) : $f, $original->features),
);
}
}
用 ReflectionProperty 强制写入?别试
虽然 PHP 8.3 允许通过反射临时解除只读($prop->setAccessible(true)),但这是危险且不可靠的操作:
- 仅对当前运行时有效,无法改变类型系统对属性的只读判定
- 后续任何对该属性的写操作(包括
json_encode()、var_dump()内部调用)都可能触发只读检查并崩溃 - PHP 官方明确将此列为未定义行为(UB),8.4 可能彻底禁止
- 即使成功,也无法解决嵌套只读对象的递归问题——你得对每一层都做反射,代码脆弱且难维护
第三方库如 spatie/data-transfer-object 在 PHP 8.3 下不适用
这类库依赖运行时属性写入或魔术方法,与 readonly 语义冲突。它们在 PHP 8.3+ 中要么抛出异常,要么静默失败(如跳过只读属性导致数据丢失)。真正可用的方案只有两类:
- 纯手工工厂方法(推荐,可控、类型安全、无额外依赖)
- 基于 AST 的代码生成工具(如
phpstan/phpstan插件或自定义CodeGenerator),在编译期生成deepClone()方法,但开发成本高
最常被忽略的一点:只读属性的“深拷贝”本质上是个语义矛盾——只读意味着你不该改它;所谓拷贝,其实是创建一个新不可变对象。别试图绕过只读,而是接受它,并把变化逻辑移到构造阶段。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











