PHP 8.5.7 的只读类在 Symfony Serializer 中默认不支持,需通过 __serialize()/__unserialize()、自定义 Normalizer 或禁用 PropertyAccessor 三种方案适配,否则抛出 SerializationException 或静默丢弃字段。

PHP 8.5.7 的只读类(readonly class)和只读属性在 Symfony Serializer 中默认不被支持,必须显式配置或绕过序列化限制,否则会抛出 SerializationException 或静默丢弃字段。
Serializer 默认拒绝只读类,因为无法写入属性
Symfony Serializer 使用反射直接设置属性值,而 PHP 8.5.7 对只读类/属性施加了运行时写入拦截。即使你用 NormalizerInterface 手动处理,底层 PropertyAccessor 在调用 setValue() 时仍会触发 Error: Cannot modify readonly property。
- 默认
ObjectNormalizer不检查isReadOnly(),直接尝试写入 → 报错 -
serialize()原生函数在 PHP 8.5.7 中已废弃,__sleep()被移除 → 必须用__serialize() - 如果你的 DTO 是
readonly class User,且含public readonly string $id,Serializer 会卡在反序列化阶段
三种可行方案:选哪个取决于你的控制粒度
不是所有方案都适合生产环境,关键看你是想全局适配、按类定制,还是彻底绕过 Serializer。
-
方案一(推荐):为只读类实现
__serialize()+__unserialize()
确保类自身掌控序列化逻辑,避免 Serializer 干预属性写入:readonly class User { public function __construct( public readonly string $id, public readonly string $name ) {} <pre class="brush:php;toolbar:false;">public function __serialize(): array { return ['id' => $this->id, 'name' => $this->name]; } public function __unserialize(array $data): void { $this->__construct($data['id'], $data['name']); }}
此方式兼容 Symfony Serializer 的ObjectNormalizer,只要不启用setChangeListener即可安全使用 -
方案二:自定义 Normalizer 替换 ObjectNormalizer
注册一个仅读取属性、不尝试写入的 normalizer:class ReadOnlyClassNormalizer implements NormalizerInterface { public function normalize(mixed $object, string|null $format = null, array $context = []): array { $ref = new \ReflectionClass($object); $props = []; foreach ($ref->getProperties() as $prop) { if ($prop->isReadOnly()) { $prop->setAccessible(true); $props[$prop->getName()] = $prop->getValue($object); } } return $props; } <pre class="brush:php;toolbar:false;">public function supportsNormalization(mixed $data, string|null $format = null): bool { return $data instanceof \BackedEnum || \is_object($data) && $data::class::class === 'readonly'; }}
注意:需手动判断readonly class(PHP 8.5.7 中可通过ReflectionClass::isReadOnly()检测) -
方案三:禁用 PropertyAccessor 写入,改用构造器注入
在 Serializer 配置中禁用自动 setter,强制走构造器:# config/packages/framework.yaml framework: serializer: default_context: enable_max_depth: true # 关键:禁用属性写入,依赖构造器或 __unserialize object_to_populate: false同时确保消息类或 DTO 实现__unserialize(),否则反序列化失败
容易踩的坑:只读类 + 序列化器 + 缓存组合场景
当你把只读类实例存进 Redis 缓存再反序列化回来,问题会叠加:
- Redis 存的是
serialize()结果 → PHP 8.5.7 已移除该函数 → 必须用__serialize()+igbinary或json替代 - Symfony Cache 使用
PhpFilesAdapter时,若缓存对象含只读属性,unserialize()会失败 → 改用RedisAdapter+ JSON 编码 - Doctrine ORM 映射只读类时,
EntityRepository::find()返回的对象若含只读属性,Serializer 可能因 lazy-loading 触发写入 → 需配合VirtualProperty或 DTO 投影
最麻烦的点不在序列化本身,而在「谁在什么时候试图写入」——可能是 Serializer、Cache、ORM 还是你的自定义 event listener。建议先用 ReflectionProperty::isReadOnly() 扫描所有 DTO 类,再逐个决定走 __serialize() 还是降级为普通类。只读性是契约,不是装饰,一旦引入就得全程守约。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











