readonly属性必须在构造函数或__clone中唯一赋值,禁止声明默认值、延迟初始化或重复写入,构造器提升是最简安全方式,且readonly仅锁定引用而非内容。

readonly属性必须在构造函数中首次赋值
PHP 8.1 的 readonly 属性不允许声明时带默认值,也不允许延迟初始化;它必须在对象实例化过程中、且仅在构造函数(或 __clone)内完成**唯一一次**赋值。任何其他时机的写入(包括构造函数外、或构造函数里重复赋值)都会触发 Fatal error: Readonly property X::$y cannot be reinitialized。
常见错误现象:
-
public readonly string $name = 'default';→ 直接语法错误 - 在构造函数体里写
$this->name = $name;,但同时又用了构造器属性提升(public function __construct(public readonly string $name) {})→ 实际赋值了两次,报错 - 试图在
__construct()之后、或在 getter / 其他方法里重新赋值 → 运行时报致命错误
构造器属性提升是初始化最简方式
用构造器属性提升配合 readonly,能省掉手动赋值语句,也天然规避重复赋值风险。PHP 会自动把参数值绑定到同名 readonly 属性上,且只做一次。
实操建议:
- 写法示例:
public function __construct(public readonly int $id, public readonly string $title) {} - 此时
$this->id和$this->title已完成初始化,无需再写$this->id = $id; - 如果需要做类型转换或条件处理,就**放弃提升写法**,改用传统构造函数 + 显式赋值(且确保只赋一次)
- 注意:提升的参数不能带默认值(如
public readonly string $slug = ''),否则 PHP 会拒绝编译
readonly保护的是引用,不是内容
readonly 锁定的是属性变量“指向哪个值”,而不是该值内部是否可变。这意味着数组、对象等复合类型的内容仍可修改,只是不能整个替换。
例如:
class Post {
public readonly array $tags;
public readonly DateTimeImmutable $createdAt;
public function __construct() {
$this->tags = ['php', 'readonly'];
$this->createdAt = new DateTimeImmutable();
}
}
$post = new Post();
$post->tags[] = '8.1'; // ✅ 合法:往数组追加元素
$post->tags = ['new']; // ❌ 致命错误:试图替换整个数组
$post->createdAt = new DateTimeImmutable(); // ❌ 致命错误:DateTimeImmutable 是不可变类,但这里错在替换属性本身
这个区别容易被忽略——尤其当团队误以为 readonly array 就等于“数组内容不可变”时,会埋下运行时逻辑漏洞。
绕过readonly的反射手段已被严格限制
PHP 8.1 起,即使调用 ReflectionProperty::setAccessible(true),也无法对已初始化的 readonly 属性重新赋值。引擎级拦截了所有写操作路径(包括反射、var_dump 后的序列化还原、甚至直接内存写入尝试)。
唯一合法的“绕过”仅发生在对象尚未初始化时:
- 用
ReflectionClass::newInstanceWithoutConstructor()创建空实例 - 再通过反射首次写入值(仅限一次)
- 但这属于测试/框架底层行为,**业务代码中不应依赖或模仿**
真正需要关注的,是初始化逻辑是否覆盖所有分支(比如条件判断漏赋值),因为未初始化的 readonly 属性在首次读取时就会报错,而不是等到写入时才暴露问题。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











