php 8.3 的 readonly 属性必须在构造函数中显式赋值,即使声明了默认值或联合类型;继承时不可重声明;推荐使用构造函数参数提升语法;其不可变性不防反射或序列化篡改。

只读属性必须在构造函数中显式赋值
PHP 8.3 的 readonly 属性不会自动采用声明时的默认值,哪怕你写了 public readonly string $role = 'guest';,只要没在构造函数里给它赋值,实例化后访问就会报 Fatal error: Uninitialized readonly property。
常见错误是以为默认值能兜底,结果运行就崩。正确做法是:所有 readonly 属性都得在 __construct() 中被明确写一次 —— 不管是直接赋值、从参数传入,还是用条件分支覆盖所有路径。
- ✅ 允许:
$this->role = $role ?? 'guest'; - ✅ 允许:
if ($admin) { $this->role = 'admin'; } else { $this->role = 'user'; } - ❌ 禁止:
public readonly string $role = 'guest';单独存在且构造函数里没碰它
继承只读属性时不能重声明
子类继承父类的 readonly 属性后,不能再用 public readonly 去“重新定义”同名属性,否则会触发 ParseError: Cannot redeclare readonly property。
这是 PHP 8.3 明确禁止的行为,和普通属性继承不同。子类只能通过调用 parent::__construct() 让父类完成初始化,自己不能插手赋值逻辑。
- ✅ 正确:
parent::__construct($name);—— 把初始化责任交给父类 - ❌ 错误:
public readonly string $name;在子类中重复声明 - ❌ 错误:
$this->name = 'new';在子类构造函数中直接赋值(父类已初始化过)
联合类型只读属性必须构造函数赋值
哪怕你给 readonly 属性加了联合类型和默认值,比如 public readonly string|int $id = 0;,PHP 8.3 依然要求你在构造函数里显式赋值。默认值在这种情况下完全不生效。
原因在于联合类型的初始化需要运行时确定具体类型,而只读属性的初始化检查发生在构造函数执行期间,静态默认值无法满足类型推导要求。
- ✅ 安全写法:
$this->id = $id ?? 0;(确保类型匹配) - ❌ 无效写法:
public readonly string|int $id = 0;+ 空构造函数 - ⚠️ 注意:
??和三元运算符必须保证返回值落在联合类型范围内,否则会触发类型错误
私有只读属性配合构造函数参数更简洁
比起在类体里声明再在构造函数里赋值,PHP 8.3 支持直接把 readonly 属性“提升”进构造函数参数列表,语法更紧凑,也天然规避漏赋值风险。
这种写法强制你为每个只读属性提供初始化入口,IDE 和静态分析工具也能更好识别意图。
class User
{
public function __construct(
private readonly string $name,
private readonly int $id,
) {}
}
这个 User 类不需要额外的属性声明块,也不用担心忘记在构造函数体里写 $this->name = $name; —— 提升语法自动完成绑定。但要注意:这种写法下属性默认是 private,如果需要外部访问,得配 public getter 方法。
最易被忽略的一点是:只读属性的“不可变”仅作用于对象生命周期内,不防反射或序列化后的修改;如果你真需要强不可变语义(比如 DTO 防篡改),还得配合 __serialize() 和自定义反序列化逻辑来加固。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











