只读属性必须在构造函数中完成初始化,支持参数绑定或体内赋值,不可默认初始化,未赋值则首次访问报错;私有只读+public getter是推荐封装方式,反射无法修改,只读类自动使所有属性不可变。

只读属性必须在构造函数中完成初始化
PHP 的 readonly 属性不是声明即“锁死”,而是“赋值即锁死”——它允许且**必须**在构造函数里被首次赋值,之后任何写操作都会触发 Fatal error: Cannot modify readonly property。这意味着封装的关键不在于隐藏字段,而在于切断后续修改入口。
- 支持在构造函数参数列表中直接声明并绑定:
public function __construct(private readonly string $name) { } - 也支持在构造函数体内显式赋值:
$this->name = $name;,但不能省略这一步 - 不允许在声明时用默认值初始化(如
private readonly string $name = 'default';)——PHP 8.3+ 仍不支持该语法 - 若构造函数没赋值,实例化时不会报错;但首次访问该属性会触发
Uninitialized readonly property错误
私有只读属性 + public getter 是最简封装组合
要对外暴露只读语义,又不让外部直接访问属性,标准做法就是搭配 private readonly 和 public 方法。这不是权宜之计,而是语言设计推荐的封装路径——PHP 不提供自动 getter,也不鼓励反射绕过。
- getter 方法体只需一行:
return $this->name;,无需额外校验或缓存逻辑 - 不要试图在 getter 里做类型转换或计算:只读属性应代表原始、确定的状态,加工逻辑应放在更高层
- 避免为只读属性写 setter:哪怕空方法也会误导调用方,PHP 也不会阻止你定义,但语义冲突
- 若需批量读取多个只读字段,可考虑添加
toArray(),但注意它返回的是副本,不影响只读性
别用反射强行修改只读属性
有人尝试用 ReflectionProperty 调用 setAccessible(true) 再 setValue(),但在 PHP 8.3+ 中,这会直接失败,并抛出 Cannot modify readonly property ——Zend 引擎在底层 Zval 级别设置了 IS_IMMUTABLE 标志,反射也无法绕过。
-
ReflectionProperty::isReadOnly()返回true,但setAccessible(true)不改变其可写性 - 即使拿到
ReflectionProperty::getValue(),也只能读;setValue()无论是否setAccessible都会报错 - 序列化(
serialize())和反序列化(unserialize())能正常工作,但反序列化后属性仍是只读的,不能二次赋值 - JSON 编码(
json_encode())对只读属性无特殊处理,行为与普通属性一致
只读类(readonly class)比单个只读属性更彻底
如果你的整个类都该是不可变的(比如 DTO、配置对象),直接用 readonly class 更省事。它自动让所有属性变成只读,无需逐个加 readonly,且禁止子类添加可变属性。
立即学习“PHP免费学习笔记(深入)”;
- 只读类的属性可在构造函数参数中用
public/protected/private修饰符直接声明并初始化 - 子类继承只读类时,自身属性仍受只读约束;但普通类继承只读类是非法的,PHP 会编译时报错
- 只读类不支持动态属性(
__set无效),也不允许在构造函数外通过$this->newProp = ...添加新属性 - 注意:只读类不能有
__clone方法,因为克隆会破坏不可变契约(PHP 8.2+ 明确禁止)



















