PHP 8.5 为 clone 引入了带属性的克隆写法:clone($obj, ['name' => 'new'])。第二个参数是关联数组,键名对应要更新的属性,键值就是新值。该调用会返回一个克隆对象,同时将该数组中的属性覆盖写入新对象,而不是修改原对象。
在 8.5 之前,readonly 属性只能在构造期间赋值一次。要“修改”一个不可变对象,通常只能 new 一个对象并复制所有字段,或者通过 clone 加反射绕过限制,两种方式都繁琐。8.5 的 clone() 第二参数本质上是在构造路径上给属性赋初值,所以对 readonly 属性同样生效。
一个典型场景是金额值对象:
final readonly class Money
{
public function __construct(public int $amount, public string $currency) {}
public function withAmount(int $amount): self
{
return clone($this, ['amount' => $amount]);
}
}
调用时只需:
$m2 = $money->withAmount(500);
与 clone 再逐个赋值相比,这种写法只更新你列出的属性,其余字段原样拷贝,适合大多数不可变对象的局部更新。如果字段很多且变更频繁,为每个字段写一个 withXxx() 仍然是模板代码,这时可以考虑让 with() 方法直接接收关联数组,内部依然返回一个 clone。
需要注意两个边界:
- 关联数组中的键必须是对象的构造器或属性提升中已有的属性名,传入未知键会导致异常。所以调用时只传想改的字段,不要额外塞元数据。
- 如果对象同时有常规属性和提升属性,或者构造时存在一些初始化顺序,要确认
clone()的覆盖行为与预期一致。readonly属性在这种情况下被允许重新赋值,仅因为它在克隆路径中仍以“首次构造”的方式处理。
这个语法主要面向 readonly DTO、事件对象、配置快照这类不可变值。8.5 之前没有这个语法,现有项目不能不加修改地直接使用,升级时要注意检查代码里是否有自定义的 __clone() 或对 clone 关键字的特殊依赖,测试对像重构或反序列化是否受影响。