readonly属性是PHP引擎强制保障的底层只读机制,赋值失败直接抛Fatal error;不同于private+getter的封装式只读,它在字节码层标记、拦截反射/反序列化等所有写入操作,并支持IDE和静态分析提前报错。

因为 readonly 属性把“不可变”从靠人自觉、靠文档约定、靠 getter 封装的高成本实践,变成了 PHP 引擎强制保障的底层能力。它不是语法糖,而是运行时拦截——赋值失败会直接抛出 Error,不是警告,不是静默忽略,也不是靠 IDE 提示。这对配置类、DTO、领域模型等场景是质变。
readonly 属性和 private + getter 的实际差异在哪
表面看都是“不能改”,但底层逻辑完全不同:
-
private属性 +public function getFoo():只是封装了写入入口,但无法阻止反射(ReflectionProperty::setValue())、序列化后反序列化篡改、或通过__set()动态设置(如果类没禁用) -
public readonly string $foo;:PHP 引擎在字节码层标记该属性为只读,任何后续赋值(包括反射、__set、甚至unserialize()中的属性还原)都会触发Fatal error: Cannot modify readonly property - IDE 和静态分析工具(如 PHPStan)能识别
readonly,提前标红错误;而私有属性加 getter 需要额外规则才能推断“只读意图”
构造函数里怎么安全初始化 readonly 属性
必须在声明时或 __construct() 中完成首次赋值,且仅一次。常见陷阱:
- 不能在
__construct()之外的任何方法里赋值,哪怕只赋一次也不行 - 不能在构造函数中“先声明空值再赋值”,比如
$this->id = null; $this->id = 'abc';—— 第一次赋null算初始化,第二次就报错 - 推荐用构造函数属性提升(PHP 8.0+)配合
readonly,最简且无歧义:class User { public function __construct( public readonly string $id, public readonly string $email ) {} } - 如果属性类型含可变结构(如
array),注意传入的数组本身仍可被外部修改,需在构造内做浅拷贝:$this->data = [...$input];
readonly 属性和 const / static readonly 的混淆点
三者完全不是一回事:
立即学习“PHP免费学习笔记(深入)”;
-
const FOO = 'bar';:类常量,编译期确定,所有实例共享,不可变且不可覆盖 -
public static readonly string $FOO = 'bar';:PHP 8.2+ 支持,属于类级别只读变量,每个类一份,运行时初始化一次,不可重赋值 -
public readonly string $foo;:实例级只读属性,每个对象独立持有自己的值,只能在该实例构造时初始化 - 误用
const替代readonly会导致:无法根据构造参数动态设值、无法用于对象状态建模(比如用户 ID 每个实例都不同)
真正容易被忽略的是 readonly 的“一次性初始化语义”:它不关心值是否真的不可变,只管这个属性引用是否被改写。如果你给 readonly array $items; 赋了一个数组,数组内部元素仍可 push 或 unset——readonly 约束的是属性本身,不是它指向的数据结构。需要深层不可变,得靠 ArrayObject 封装或使用 readonly class(PHP 8.2)配合只读数组属性。



















