php 8.3 只读属性提供运行时不可变保障,但不等于线程安全或真正不可变对象;它仅阻止直接赋值,仍可能被反射、反序列化或引用传递绕过,需配合防御性复制、自定义序列化及静态分析才能构建可靠值对象。

PHP 8.1 引入的 readonly 属性在 PHP 8.3 中已具备真正运行时不可变保障,但**它不等于线程安全,也不自动构成不可变数据结构**——关键取决于你是否控制了所有引用路径和序列化行为。
只读属性 ≠ 不可变对象:构造后仍可能被绕过
只读属性仅阻止直接赋值($obj->prop = ...),但不阻止通过反射、反序列化或引用传递间接修改其底层状态:
-
ReflectionProperty::setValue()在默认配置下仍可写入只读属性(PHP 8.3 默认禁用,但若启用了zend.assertions=1或某些调试扩展,可能恢复可写) - 使用
unserialize()时,若未启用__unserialize()钩子或未禁用unserialize_callback_func,恶意 payload 可绕过构造函数直接填充属性 - 若只读属性是数组或对象引用(如
privatereadonlyarray $data;),其内部元素仍可被修改(只读保护的是引用本身,不是所指内容)
如何真正构建不可变值对象(Value Object)
只读属性只是起点。要达成实际不可变性,必须组合以下措施:
- 所有只读属性类型应为标量、
string、int等值类型,或封装了深拷贝/防御性复制的自定义类(如DateTimeImmutable) - 禁止公开可变容器:避免
privatereadonlyarray $items;,改用privatereadonlyCollection $items;并确保Collection类自身无公开修改方法 - 显式实现
__serialize()和__unserialize(),拒绝非构造路径的属性填充 - 若需 JSON 序列化,优先用
JsonSerializable接口,而非依赖默认json_encode()的属性遍历
只读属性对并发场景的实际影响有限
PHP 的 FPM/Swoole/CLI 模型中,**单个请求生命周期内不存在多线程竞争**,所以“线程安全”在此语境下常被误用。真正受益点在于:
- 防止同一请求中多个逻辑分支意外覆盖关键状态(例如中间件、事件监听器、DTO 透传)
- 配合 OPcache JIT,引擎可对只读属性访问做更激进的内联与缓存优化(PHP 8.3 已实测提升 getter 调用约 12%)
- 在 Swoole 协程中,若对象跨协程共享(不推荐),只读属性能降低竞态调试成本——但前提是对象本身不包含可变引用
容易被忽略的继承陷阱
子类无法重写父类只读属性,但可以声明同名私有属性造成“遮蔽”,这不会报错,却会破坏预期不可变性:
class Base {
public readonly string $id;
public function __construct(string $id) {
$this->id = $id;
}
}
class Child extends Base {
private string $id = 'hijacked'; // ✅ 合法但危险:$id 不再是只读,且与父类 $id 无关
}
这种写法不会触发任何警告或错误,但 $child->id 实际访问的是子类私有属性,完全绕过只读约束。检查此类问题必须依赖静态分析工具(如 PHPStan level 8+)或人工审计构造链。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











