只读类实例化必须走构造函数,PHP 8.2 强制所有属性仅在 __construct() 中一次性赋值,绕过(如反射、DI 容器无参创建、mock)均触发致命错误;继承需显式声明 readonly;不可通过 __set 或反射修改属性;推荐工厂模式统一校验并初始化。

只读类实例化必须走构造函数,不能跳过
PHP 8.2 的 readonly class 强制所有属性只能在 __construct() 中完成一次性赋值,任何绕过构造函数的初始化方式都会失败。比如用 new static()、反射创建空对象再赋值、或依赖 DI 容器自动注入未声明参数——这些在普通类里可能可行,但在只读类中直接触发 Fatal error: Uninitialized readonly property。
常见错误现象:
- 用 Laravel 的
app()->make(UserDTO::class)且未配置构造参数 → 报错 - 测试中 mock 只读类时用
Mockery::mock(readonly class)→ 不支持,会崩溃 - 数组解包传参漏字段:
new UserDto(...$data)但$data缺email→ 即使属性是?string也必须显式传null
正确做法:
- 所有实例化必须显式调用构造函数,并确保每个参数都提供(哪怕为
null) - 工厂函数是安全出口:封装校验逻辑,统一返回完整初始化的对象
- 避免在测试中直接 new 只读类;改用工厂或预设 fixture 数组
继承只读类必须显式加 readonly,否则编译失败
只读类可以被继承,但 PHP 8.2 要求子类也必须声明为 readonly,否则报 Compile Error: Non-readonly class cannot extend readonly class。这不是运行时错误,是解析阶段就拒绝的硬性限制。
立即学习“PHP免费学习笔记(深入)”;
使用场景有限,典型如分层 DTO:
- 基类
readonly class ApiResponse定义通用字段(status,timestamp) - 子类
readonly class UserResponse extends ApiResponse必须写成readonly class UserResponse extends ApiResponse
容易踩的坑:
修复 Core bailout 状态损坏安全问题,以及 BZ2、DOM、PDO、Opcache/JIT 内存与执行错误,改进 Unicode 字素处理、URL 解析、TLS 流状态、信号分发及会话错误处理。
- 误以为“父类只读,子类自动只读”→ 实际必须显式写
readonly - 子类构造函数忘记调用
parent::__construct()→ 触发Uninitialized readonly property - 试图让非只读类继承只读类 → 直接语法错误,IDE 通常提前标红
只读类不能用 __set()、反射或魔术方法修改属性
即使你重写了 __set() 或用 ReflectionProperty->setValue(),PHP 运行时仍会拦截并抛出 Error: Cannot modify readonly property。这不是权限问题,是语言层面的不可变性保障。
性能与兼容性影响:
- 反射写入失败是即时的,无额外开销,但别指望绕过它做“调试临时修改”
- 某些旧版 ORM 或序列化工具(如 Doctrine 的 lazy load)若尝试动态设置属性,会直接崩掉
-
json_decode($json, true, 512, JSON_OBJECT_AS_ARRAY)返回数组,不是只读类实例——别混淆数据结构和对象实例
替代方案:
- 需要“变更”时,用工厂生成新实例,而非修改旧对象
- 调试时 dump 构造参数,而不是试图 patch 对象
- 序列化/反序列化走
__serialize()/__unserialize()显式控制,不依赖默认行为
工厂模式是只读类最自然的搭档
只读类本身不处理缺失字段、类型转换或业务规则,这些全得由外部承担。工厂函数就是那个“守门人”,把混乱的原始数据变成合规的只读实例。
实操建议:
- 工厂函数名建议带
make前缀,如makeUserDto(),语义清晰 - 参数校验要早:用
filter_var()、throw new InvalidArgumentException(),别等进构造函数才爆 - 可空字段(
?string)必须显式传null,不能靠?? null在工厂里补 —— 因为构造函数参数是 required,PHP 不允许省略 - 不要在工厂里 new 多个只读类再挑一个,那违背了“一次构造即完整”的设计意图
复杂点在于:工厂本身要覆盖所有合法输入组合,且错误信息得足够开发者定位问题。漏掉一种边界情况(比如 status 是字符串数字 "1"),就可能让只读类在构造时因类型强制失败而中断。


















