PHP 8.2 的 readonly class 直击对象状态意外修改、DTO/配置被污染、并发数据不一致三类高频问题,通过引擎层强制禁止属性赋值实现运行时保障,但不递归冻结嵌套对象、不防反射或魔术方法。

PHP 8.2 的 readonly class 不是用来“锦上添花”的,它直击三类高频、难调试、易被忽视的运行时问题:对象状态意外被改、DTO/配置被中间层污染、并发场景下数据不一致。这些问题在中大型项目里不是“可能出”,而是“已经出过”,只是没被归因到对象可变性上。
防止构造后属性被意外赋值
传统 DTO 类靠约定或文档说明“别改”,但 PHP 运行时不拦着。一旦某个中间件、装饰器或日志逻辑写了 $dto->status = 'processed',下游拿到的就是脏数据,且错误堆栈里根本看不到源头。
-
readonly class在引擎层拦截所有写操作,抛出明确错误:Fatal error: Cannot modify readonly property User::$name - 不需要写
__set()魔术方法,也不依赖 IDE 提示或 Psalm/PHPStan 静态检查——这是运行时强制保障 - 特别适合 API 层返回的响应对象,比如
readonly class ApiResponse,确保控制器 return 之后没人能偷偷塞字段进去
避免嵌套对象状态被间接污染
只读类对嵌套引用不做深冻结,但它的设计天然阻断“浅层污染路径”。比如你有一个 readonly class Order,里面包含 public Address $address,虽然 $address 本身不是只读类,但只要你不暴露 setter 或允许外部修改其引用,风险就集中在入口点。
- 若
Address也声明为readonly class,则整个链路不可变;否则需额外注意:只读类不阻止你调用$order->address->updateCity(...)(如果Address是可变的) - 真正起作用的是“不可赋值”而非“不可调用方法”,所以只读类必须配合合理的方法设计(比如不提供修改自身状态的 public 方法)
- 常见误用:把只读类当 immutable struct 用,却忘了它不自动递归冻结子对象
消除多线程/协程环境下的竞态隐患
PHP 虽无真正多线程,但在 Swoole、RoadRunner 或异步 HTTP 客户端场景中,多个协程共享一个对象引用时,可变对象极易成为状态混乱的源头。
立即学习“PHP免费学习笔记(深入)”;
- 只读类实例一旦创建,其属性内存地址被 Zend 引擎标记为
IS_IMMUTABLE,Zval 层禁止写入,从底层规避了竞态条件 - 相比手动 clone 或序列化传递,只读类零成本、无性能损耗,且语义清晰
- 注意:只读类不能解决“对象被多个协程同时读+写不同属性”的问题(因为 PHP 没有真正并行),但它杜绝了“同一属性被反复覆盖”的典型脏写
最常被忽略的一点是:只读类不是万能锁。它不防反射(ReflectionProperty::setValue() 仍可绕过)、不防 __get()/__set() 自定义逻辑、也不影响你 new 出来的新对象。它的价值在于把“不该发生的赋值”变成“立刻报错”,而不是让代码看起来更‘安全’。



















