这是php 8.2+的预期行为,非bug;只读类实例属性在构造完成后不可修改,运行时强制拦截所有写操作,与作用域无关,无法绕过。

函数返回 readonly class 实例后,外部赋值触发 “Cannot modify readonly property” 错误
这是 PHP 8.2+ 的预期行为,不是 bug。只要函数返回的是 readonly class 实例(无论是否显式声明 readonly 关键字),其所有属性在构造完成后就不可写——哪怕你用 $obj->prop = ... 在同一作用域、甚至同一函数内操作,也会立即抛出 Error: Cannot modify readonly property。
关键点在于:只读性由类定义决定,与调用位置无关;错误发生在运行时,PHP 引擎强制拦截,无法绕过(反射、__set、serialize/unserialize 等均无效)。
为什么不能在函数外“修复”只读对象的属性
因为这不是权限或作用域问题,而是语言级的不可变契约。PHP 运行时会在每次对属性执行写操作时检查该属性是否属于只读类实例,并直接终止执行。
-
readonly class的所有属性隐式具备readonly语义,无需额外标注 - 即使属性是
public,也不代表可写;可见性 ≠ 可写性 - 子类继承只读类时,同样无法在子类方法中修改父类的只读属性(包括
$this->prop) - 静态分析工具(如 PHPStan)会提前报
property.readOnlyAssignOutOfClass,但运行时错误仍会发生
真正可行的三种替代方案
必须放弃“修改已有实例”的思路,转而通过重建、替换或设计调整来满足业务逻辑需求:
-
重构为非只读类 + 显式封装:如果确实需要运行时变更状态,就不要用
readonly class。改用普通类,把属性设为private,提供受控的withXxx()方法返回新实例(类似不可变集合的 fluent 风格) -
用构造函数参数覆盖初始化值:测试或逻辑分支中,不复用旧实例,而是重新
new一个带不同参数的实例。例如:function makeUser(string $name, int $age): User { return new User($name, $age); },调用方传入新值即可 -
拆分只读数据与可变状态:把真正不变的部分(如 ID、创建时间)放进
readonly class,把可能变化的部分(如状态标记、缓存字段)抽到另一个普通对象或数组中,两者组合使用
容易被忽略的关键细节
只读类的不可变性是“全有或全无”的——哪怕只有一处需要动态更新,整个类就不适合声明为 readonly。很多人试图用 __unserialize() 或 ReflectionProperty::setValue() 强行突破,结果要么失败(PHP 8.3+ 更严格),要么破坏类型安全和静态分析信任链。真正的解法永远在设计层:明确哪些数据该 immutable,哪些该 mutable,然后用不同类职责隔离。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











