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覆盖的属性名与声明一致、可读性最好
© 版权声明
文章版权归作者所有,未经允许请勿转载。