给 readonly 属性加了默认值,构造函数里再赋值就抛 Cannot modify readonly property
PHP 8.6 允许 readonly 属性在声明处直接写默认值(RFC: readonly_property_defaults,投票 24-0 通过;RC3 版本日期 2026-10-08,GA 计划 2026-11-19)。语法上看起来只是给属性加了个初始值,但语义上有个容易踩的点:默认值本身已经占用了 readonly 属性的那一次初始化赋值。
报错现场
final readonly class Rule
{
public string $name = 'default';
public function __construct(string $name)
{
$this->name = $name; // Error: Cannot modify readonly property Rule::$name
}
}
错误原文是:
Cannot modify readonly property Rule::$name
不是引擎没识别出构造函数,而是属性在构造函数体执行之前就已经被设置好了。$this->name = $name 在 readonly 语义下属于第二次写入,因此会直接失败。
规则只有一句
默认值算作那一次初始化赋值。readonly 属性只允许初始化一次,声明处的默认值占掉了这个名额,后续任何赋值——包括构造函数体里的——都算修改 readonly 属性。
也因此,带默认值的 readonly 属性是一个「声明即定型」的常量式属性,而不是「给个初始值、构造里再覆盖」的普通属性。
它和 8.4 的接口属性是配套的
这个 RFC 的动机来自 PHP 8.4 的接口属性(interface property hooks)。接口可以声明:
public string $name { get; }
实现类必须为这个属性提供不可变值。PHP 8.5 之前只有两种写法:在构造函数体里逐个常量赋值,或者写四个返回常量的 getter。构造函数版本长这样:
// PHP 8.5
final readonly class CsvReport implements ReportBlueprint
{
public string $name;
public string $extension;
public string $template;
public array $columns;
public function __construct()
{
$this->name = 'CSV';
$this->extension = 'csv';
$this->template = 'reports/%s.csv';
$this->columns = ['id', 'name', 'total'];
}
}
到 8.6 可以直接写默认值:
// PHP 8.6
final readonly class CsvReport implements ReportBlueprint
{
public string $name = 'CSV';
public string $extension = 'csv';
public string $template = 'reports/%s.csv';
public array $columns = ['id', 'name', 'total'];
}
两者语义等价,后者省掉了整个构造函数。也正因为它们是同一个「唯一初始化」的两种写法,不能混用——一旦属性上有默认值,构造函数体里的赋值就变成非法的修改。原始 readonly RFC 曾认为默认值「没什么用」,接口属性的出现改变了这个判断。
unset 也是错误
对这类带默认值的 readonly 属性调用 unset() 是错误。readonly 属性初始化后不能被取消设置,默认值不会让它成为例外。
迁移时的两种改法
值本来就是固定的:把构造函数体里的赋值全部删掉,只保留声明处的默认值,参照上面
CsvReport的 8.6 版本。仍然需要「构造时可传可不传」:不能在 readonly 属性上同时用默认值 + 构造赋值。把默认值移到构造参数上,属性本身不声明默认值:
final readonly class Rule
{
public string $name;
public function __construct(string $name = 'default')
{
$this->name = $name;
}
}
另一种做法是拆成两个类或工厂,由调用方选择走哪条路径。
这里的关键判断是:属性上的默认值表达的是「这个值只有一个」,构造参数上的默认值表达的是「调用方可以不传」。前者是 8.6 新增的能力,后者在 readonly 属性上一直可用。迁移时先确认要表达的是哪一种,再决定默认值写在属性上还是参数上。
版本信息
- 特性:readonly 属性声明默认值
- RFC:
readonly_property_defaults,24-0 通过 - 可用版本:PHP 8.6(RC3 2026-10-08,GA 计划 2026-11-19)
- 相关背景:PHP 8.4 接口属性(interface property hooks)