php 8.1起支持只读属性,其核心是将赋值严格限定在初始化阶段(构造函数或声明时),之后任何写入均抛出“cannot modify readonly property”错误;声明必须满足三个条件:有明确类型声明、无默认值、仅通过构造函数参数提升或体内显式赋值初始化。

PHP 8.1 起支持只读属性(readonly),它是保护对象数据不被意外修改的最直接语言级机制。核心不是“禁止所有写操作”,而是把赋值严格限定在初始化阶段——构造函数中或属性声明时,之后任何写入都会在运行时抛出 Error: Cannot modify readonly property。
只读属性怎么声明才合法
必须满足三个硬性条件,缺一不可:
- 属性必须有明确的类型声明(如
string、int、?array),不能是无类型属性; - 不能设默认值(
public readonly string $name = 'default';❌ 不允许); - 只能通过构造函数参数提升(promoted properties)或在构造函数体内显式赋值(
$this->xxx = ...)来初始化。
推荐写法是参数提升,简洁且语义清晰:
class User {
public function __construct(
public readonly string $id,
public readonly string $email,
private readonly int $createdAt
) {}
}
只读类 vs 单个只读属性
PHP 8.2 引入了 readonly class,它不是 8.3 的新特性。它等价于给类里所有属性自动加上 readonly,但约束更严:
- 不允许声明
static属性; - 不允许使用
#[\AllowDynamicProperties]; - 子类若要继承,也必须声明为
readonly class,否则直接 Fatal error。
适合用作纯值对象(Value Object),比如 readonly class Money { public int $amount; },无需逐个加 readonly,代码更干净。
反射与序列化要注意什么
PHP 8.3 起,ReflectionProperty::isReadOnly() 才真正稳定可用,可用于运行时校验:
$prop = (new ReflectionClass(User::class))->getProperty('id');
var_dump($prop->isReadOnly()); // true
但反射不能绕过只读限制:一旦属性已初始化,$prop->setValue($obj, 'new') 会报错。序列化本身不破坏只读性,但如果你实现了 __serialize() 或 __unserialize(),必须手动确保不给只读属性重新赋值,否则会触发错误。
哪些场景最适合用只读属性
它不是万能锁,而是精准防护工具:
- DTO/数据传输对象:接收 API 请求或数据库结果后,立刻转成只读对象,防止后续流程误改原始数据;
- 配置类:加载完配置后封为只读,避免运行时被中间件或插件悄悄覆盖;
-
领域模型中的值对象:如
PhoneNumber、EmailAddress,本身无身份,只关心值是否合法且不变。
别用它替代权限控制或业务规则校验,它的作用域仅限单个对象实例的生命周期内。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











