PHP8.5 clone with 用法与 readonly 类克隆详解

PHP 8.5 clone with 用法与 readonly 类克隆详解

学完能做什么

  • 在克隆对象时用一行语法直接覆盖属性,省去「克隆后逐个赋值」的样板代码
  • 克隆 readonly 类实例并修改其只读属性,突破传统克隆无法写只读属性的限制
  • 判断哪些场景该用 clone with、哪些场景仍需传统克隆或构造函数

前置条件

  • PHP 版本为 8.5 或更高(clone with 为 8.5 新增语法,低版本直接语法错误)
  • 目标类已定义,且需要覆盖的属性在类中可访问(public,或从可访问作用域调用)
  • 若目标属性为 readonly,需明确其初始化状态与克隆上下文

分步操作

第 1 步:掌握基础语法

在 clone 关键字后追加 with 块,块内以「属性名: 值」形式声明要覆盖的属性。

class Point
{
    public function __construct(
        public int $x = 0,
        public int $y = 0,
    ) {}
}

$p1 = new Point(1, 2);
$p2 = clone $p1 with { x: 10 };

var_dump($p2->x, $p2->y); // int(10) int(2)

做到以下程度即可继续:

  • 克隆结果为新对象,原对象 $p1 不受影响
  • 未在 with 块中列出的属性保持原值
  • with 块中的属性名必须真实存在于类中,拼写错误会触发错误

第 2 步:确认可覆盖属性的边界

按属性可见性与修饰符逐项核对:

属性类型 是否可在 clone with 中覆盖 说明
public 普通属性 是 最常见场景
protected / private 属性 视调用作用域而定 需在可访问该属性的作用域内使用
readonly 属性 是(8.5 关键能力) 克隆过程视为新对象初始化,允许写入
静态属性 否 静态属性不属于实例,with 块不处理
不存在或拼写错误的属性 否 会报错,需排查属性名

第 3 步:克隆 readonly 类并更新属性

定义 readonly 类,先用传统方式验证限制,再改用 clone with。

readonly class User
{
    public function __construct(
        public string $name,
        public string $email,
    ) {}
}

$user = new User('Alice', 'alice@example.com');

// 传统方式:直接改只读属性会失败
// $user->name = 'Bob'; // Error: Cannot modify readonly property

// 传统克隆后赋值同样失败
// $copy = clone $user;
// $copy->name = 'Bob'; // Error

// 8.5 方式:克隆时覆盖
$updated = clone $user with { name: 'Bob' };

var_dump($updated->name, $updated->email); // string(3) "Bob" string(17) "alice@example.com"
var_dump($user->name);                     // string(5) "Alice",原对象不变

做到以下程度即完成核心目标:

  • $updated 是独立新对象,name 已更新,email 继承原值
  • 原对象 $user 完全不变
  • 不再需要为「修改不可变对象」手写 withName() 之类的辅助方法

第 4 步:在不可变数据流中串联使用

对需要多次变更的对象,链式克隆保持每步都是新实例:

$step1 = clone $user with { email: 'bob@example.com' };
$step2 = clone $step1 with { name: 'Bob' };

// $user、$step1、$step2 为三个互不影响的对象

第 5 步:验证预期结果

运行以下检查清单:

  • 克隆对象 !== 原对象(引用不同)
  • with 块内属性为新值,块外属性与原对象相等
  • 原对象所有属性未被改动
  • 嵌套对象属性为浅拷贝:若属性本身是对象,克隆后仍指向同一实例,需要深拷贝时自行处理

常见问题与排查

  • 语法报错 unexpected token "with":PHP 版本低于 8.5,升级运行时或改用传统克隆加构造函数重建。
  • 报错属性不存在:核对 with 块中的属性名与类定义,注意大小写;readonly 类中通过构造函数提升的属性名即属性名。
  • private/protected 属性覆盖失败:clone with 的写入遵循可见性规则,需在类内部方法中使用,或改为提供公开的构造/工厂方法。
  • 误以为会深拷贝:clone with 与 clone 一样是浅拷贝,对象类型属性仍共享引用;需要深拷贝时在 __clone() 中显式处理。
  • 在 __clone() 中重复赋值:with 块覆盖发生在克隆流程中,避免在 __clone() 里再对同一属性做冲突赋值,保持单一来源。
  • 动态属性场景:依赖 #[AllowDynamicProperties] 或运行时动态添加的属性不属于类声明属性,不应指望 with 块处理,改用数组或值对象建模。

适用边界

  • 适合:DTO、值对象、配置对象、事件对象等以不可变为设计目标的类
  • 不适合:需要复杂校验或副作用的重建逻辑,此类场景仍建议走构造函数或工厂方法,把校验集中在一处
  • 与 readonly 配合时,优先用构造函数提升属性,保证 clone with 覆盖的属性名与声明一致、可读性最好
© 版权声明

相关文章