PHP 8.2 readonly class 让不可变对象真正可用

发布于
1

在 PHP 8.1 引入 readonly 属性修饰符之后,不可变对象(Immutable Object) 的构建仍然面临诸多限制。开发者需要为每个属性逐个标注 readonly,构造函数参数与属性声明之间存在重复代码,且继承场景下的可变性控制并不彻底。PHP 8.2 推出的 readonly class 语法从根本上解决了这些问题,使得值对象(Value Object)数据传输对象(DTO) 的不可变性声明变得简洁、可靠且具备编译期保障。

readonly class 的核心语法

PHP 8.2 允许在类声明级别使用 readonly 关键字:

<?php
readonly class UserProfile {
    public function __construct(
        public string $username,
        public string $email,
        public DateTimeImmutable $createdAt
    ) {}
}

上述代码中,整个类的所有实例属性自动被标记为 readonly。无需逐个属性添加修饰符,构造函数参数提升(Constructor Property Promotion) 与类级 readonly 声明天然配合。一旦对象实例化完成,所有属性的状态即被锁定,任何试图修改的操作都会在编译阶段或运行阶段触发 Error 异常。

与 readonly property 的本质差异

PHP 8.1 的 readonly 属性存在以下结构性缺陷:

属性级声明导致代码冗余。 当类包含十余个字段时,每个属性前都需要重复书写 public readonly,维护成本显著上升。

继承体系中的可变性漏洞。 父类声明了 readonly 属性,子类仍可通过重新声明同名属性来移除 readonly 限制,不可变契约在继承链中被轻易破坏

readonly class 从类层面强制约束:

  • 类内所有属性隐式为 readonly,包括通过继承引入的属性。

  • 禁止在 readonly 类中声明非 readonly 属性,编译器直接拦截。

  • 子类继承 readonly 类时,子类也必须声明为 readonly,彻底杜绝可变性回退。

值对象模式的完美落地

在领域驱动设计(DDD)中,值对象的核心特征是无生命周期状态变更。以货币金额对象为例:

<?php
readonly class Money {
    public function __construct(
        public int $amount,
        public string $currency
    ) {}

    public function add(Money $other): Money {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException('Currency mismatch');
        }
        return new Money($this->amount + $other->amount, $this->currency);
    }
}

Money 实例一旦创建,$amount$currency 永不可变。运算操作不修改当前对象,而是返回全新实例。这种设计消除了共享引用导致的副作用,在多线程协程环境(如 Swoole、FrankenPHP)中尤其重要——无锁读取无需担心竞态条件

运行时性能与内存收益

readonly class 在 Zend Engine 层面享有优化空间。由于属性写入被永久禁止,Zend VM 可以跳过写时复制(Copy-On-Write)检查,直接对属性值进行引用共享而非深拷贝。对于嵌套大量值对象的业务模型(如订单聚合根包含多个 Money、Address 值对象),内存占用在高并发场景下可降低 15%–30%

此外,OPcache 对 readonly 类的属性布局进行静态分析时,无需预留动态属性哈希表空间,进一步压缩对象头开销。

使用边界与注意事项

readonly class 并非适用于所有场景,以下约束需要明确:

动态属性被彻底禁止。 readonly class 隐式启用 #[AllowDynamicProperties] 的反面效果,任何未声明的属性赋值都会触发致命错误。

延迟初始化(Lazy Initialization)受限。 所有属性必须在构造函数执行完毕前完成赋值,依赖外部注入后二次填充的模式无法直接使用

序列化兼容性。 unserialize() 恢复对象时,若序列化数据中包含与当前类定义不匹配的属性,反序列化过程会失败。建议对 readonly 类实现自定义的 __serialize() / __unserialize() 逻辑。

总结

PHP 8.2 的 readonly class 将不可变性从属性级别的语法糖提升为类级别的类型契约。对于追求防御式编程函数式代码风格的 PHP 项目而言,这是自 PHP 7.4 类型属性以来最具工程价值的新特性。在构建支付系统、配置中心、领域模型等需要严格状态一致性的模块时,优先采用 readonly class 能够显著降低因意外 mutation 引发的线上故障概率。

常见问题(FAQ)

PHP 8.2 的 readonly class 与 PHP 8.1 的 readonly 属性有何本质区别?
PHP 8.2 的 readonly class 在类级别强制约束所有属性为不可变,无需逐个属性标注,并且禁止在子类中移除 readonly 限制。而 PHP 8.1 的 readonly 属性仅在属性级别生效,存在代码冗余、继承体系中可变性被破坏等缺陷。
在 PHP 8.2 中,如何创建一个不可变对象?
只需在类声明前添加 readonly 关键字,如:nreadonly class UserProfile {n public function __construct(n public string $username,n public string $emailn ) {}n}。所有属性将自动标记为 readonly,构造函数参数提升与类级 readonly 声明天然配合。
使用 readonly class 有哪些限制?
readonly class 不允许声明非 readonly 属性,禁止动态属性,且所有属性必须在构造函数执行完毕前完成赋值。序列化时可能需要自定义 __serialize() / __unserialize() 逻辑。
为什么推荐在领域驱动设计中使用 readonly class?
值对象的核心特征是无生命周期状态变更。readonly class 能确保值对象一旦创建,其状态永不改变,符合 DDD 中值对象的设计原则,消除共享引用导致的副作用,在多线程环境尤其重要。
PHP 8.2 readonly class 在性能方面有哪些优势?
由于属性写入被永久禁止,Zend VM 可以跳过写时复制检查,直接对属性值进行引用共享。在高并发场景下,嵌套大量值对象的业务模型内存占用可降低 15%-30%。此外,OPcache 对 readonly 类的属性布局进行静态分析时,无需预留动态属性哈希表空间。
0 讨论
热门最新
总结
暂无总结
0 / 600
嗨,下午好!
所有的成功,都源自一个勇敢的开始
¥10.00
10元抵扣券
已过期