php 8.3 的只读类不支持原生反序列化,因 unserialize() 跳过构造函数且禁止写入只读属性;唯一合法入口是 __serialize() + __unserialize();深层拷贝禁用反射赋值,推荐 dto 转换、双阶段反序列化或只读包装器模式。

PHP 8.3 的只读类(readonly class)和只读属性(public readonly string $name)在序列化与深层拷贝场景中存在明确的行为边界——它们本身**不支持原生反序列化重建**,除非显式干预。这不是 Bug,而是语言为保障不可变语义所做的强制设计。
只读类的序列化在 PHP 8.3 中默认失败
只读类对象调用 serialize() 能成功,但 unserialize() 会直接报错:
Uncaught Error: Cannot modify readonly property …
原因在于:反序列化跳过构造函数,尝试直接写入只读字段,而 PHP 运行时严格禁止该操作。
- 只读类无法通过
__wakeup()恢复(该方法仍不允许赋值只读属性) -
__sleep()返回空数组可绕过序列化,但失去数据持久性价值 - 真正可用的入口只有
__serialize()+__unserialize()
深层拷贝必须避开反射赋值陷阱
使用 ReflectionProperty::setValue() 对只读属性强行写入,在 PHP 8.3 中已被完全禁用。即使属性未初始化,反射也不再提供“后门”:
-
$prop->setAccessible(true)对只读属性无效 -
$prop->isReadOnly()返回true后,setValue()必然抛出 Cannot assign to readonly property - 试图 clone 只读类实例会触发 Clone of readonly class is not allowed
安全兼容的三种落地方案
无需降级或回避只读语义,以下方式已在生产环境验证:
-
DTO 转换法:用
json_encode($obj)输出数组,再用构造函数重建新实例(推荐用于 API 响应、缓存) -
双阶段反序列化:在
__unserialize()中先临时关闭只读检查(PHP 内部机制允许),再调用私有初始化方法(需配合#[\AllowDynamicProperties]或非只读基类) - 只读包装器模式:将只读字段封装进普通类,对外暴露只读接口,内部用可序列化载体承载数据(适合 ORM 映射)
版本兼容性关键结论
PHP 8.2 及更早版本根本不识别 readonly class 语法,解析即报错;PHP 8.3 是首个完整支持该语法并严格执行运行时保护的版本。跨版本部署时:
- 所有节点必须统一为 PHP 8.3+,否则反序列化会因语法或行为差异彻底中断
- 避免混合使用
readonly class和__sleep/__wakeup——后者在只读类中已失效 - 测试重点不是“能否序列化”,而是“反序列化后
var_dump()是否还原全部只读字段值”
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











