登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  php教程

readonly 属性怎么配置或排查

来源:17golang原创

时间:2026-09-13 09:31:01 104浏览 收藏

PHP 中的 readonly 属性不是“只读显示”开关,而是一次初始化后的写保护。排查“不能赋值”时,先确认运行时至少是 PHP 8.1,再检查属性是否有类型、是否在声明它的类作用域内直接完成首次赋值,以及后续代码有没有通过引用、数组偏移或递增操作间接修改它。

最小可用规则是:public readonly string $id; 必须带类型,不能写属性默认值;在允许的类作用域中直接赋值一次,之后只能读取。PHP 8.4 起它默认允许受保护的设置作用域,子类场景要按实际版本判断。
要点速览
  • readonly 属性从 PHP 8.1 起可用,必须是有类型属性,静态属性不支持 readonly。
  • 初始化必须是直接赋值;构造完成后再次赋值、递增、引用、数组元素修改和 unset 都属于修改。
  • readonly 锁住的是属性绑定,不等于递归冻结:属性里存放的对象仍可能改变内部字段。
  • PHP 8.4 默认把 readonly 属性的设置作用域扩展为 protected(set),旧版本升级时要留意继承代码。

一、先确认版本与有类型声明

如果项目在 PHP 8.0 或更早版本运行,解析器会把 readonly 当成不支持的语法。先看实际执行脚本所使用的 PHP,而不是只看开发机或容器镜像标签:

# 查看当前命令行实际使用的 PHP 版本
php -v

# 在应用入口临时输出版本,排除 CLI 与 FPM 版本不一致
php -r 'echo PHP_VERSION, PHP_EOL;'

声明时必须给属性一个类型;如果确实需要接受任意值,可以明确写 mixed。不要给 readonly 属性写常量默认值,也不要声明成 static

id = $id;
        $this->email = $email;
    }
}
PHP readonly 属性声明中 PHP 版本、类型、实例属性和默认值限制的静态关系框图
图1:readonly 声明边界示意图,查看版本支持、类型属性、实例槽位与默认值限制之间的静态关系。

二、在允许的作用域内只初始化一次

首次写入通常放在构造方法里。外部代码可以读取公开属性,但不能替换它;即使新值与原值相同,第二次赋值也会抛出 Error。如果属性没有初始化,直接读取则会遇到“must not be accessed before initialization”。

requestId = $requestId;
    }
}

$request = new ApiRequest('req-1001');
echo $request->requestId, PHP_EOL; // 允许读取

// 这是第二次写入,即使值相同也会触发 Error
$request->requestId = 'req-1001';

“Cannot initialize readonly property … from global scope”通常表示属性还没有值,但调用方试图从对象外初始化。把赋值移回构造方法或类内工厂方法,并保证所有构造分支都给它赋值即可。PHP 8.4 起,readonly 属性默认可由子类设置;PHP 8.1–8.3 的继承代码则不能按这个新行为假设。

三、按错误类型排查隐式修改

readonly 的陷阱在于,修改不只是一行普通赋值。下面的检查表可以把错误快速归类:

现象常见写法处理方向
Cannot modify readonly property$obj->count++把计算结果放到新变量,或重新创建值对象
Cannot indirectly modify$obj->items[] = $item先复制数组,修改副本后构造新对象
Cannot initialize from scope对象外直接给未初始化属性赋值移入声明类的构造方法/工厂方法
不能声明无类型、static 或属性默认值补类型,改为实例属性并在初始化阶段赋值

引用传递也会被拦截,像 $ref =& $obj->count 或把属性传给需要引用参数的函数,都不适合作为初始化手段。数组同样不能通过元素偏移“偷偷改一部分”;需要变化时,建议使用不可变风格的方法返回一个新对象:

items, $item];
        return new self($items);
    }
}

$cart = new Cart([]);
$next = $cart->withItem('book');
var_dump($cart->items, $next->items);
PHP readonly 属性重复赋值、数组间接修改、引用操作和新对象返回的静态诊断关系图
图2:修改诊断示意图,查看直接写入、间接修改、引用边界和新对象返回之间的静态关系。

四、检查对象内部可变与继承边界

把对象放进 readonly 属性,只能保证属性不会被替换,不能自动冻结对象本身。下面的内部字段变化是允许的,因此需要在领域对象内部再设计不可变接口,不能把 readonly 当成深度冻结:

data->name = 'Lin'; // 对象内部字段变化,属性本身没有被替换

// 下面是替换属性引用,仍然不允许
// $profile->data = new stdClass();

如果只想让子类在构造链中补值,先确认最低支持版本。PHP 8.4 的默认设置作用域为 protected(set),可以由子类写入;如果项目还要兼容 PHP 8.1–8.3,就应把初始化集中在父类,或者明确设计兼容分支。需要把整类实例属性都设为 readonly 时,可以考虑 PHP 8.2 的 readonly class,但它不能声明静态或无类型属性,也不能与普通可写父类互相继承。

五、用最小检查清单收尾

接入 DTO、值对象或配置快照时,按下面顺序排查,通常不需要先改业务逻辑:

  1. 运行时是否为 PHP 8.1 及以上,且 CLI、FPM、容器内版本一致。
  2. 属性是否是实例属性,并且带有具体类型或 mixed
  3. 是否把默认值写在属性声明上;readonly 应在构造阶段直接赋值。
  4. 首次赋值是否发生在允许的类作用域,继承场景是否考虑 PHP 8.4 的 protected(set) 变化。
  5. 后续是否存在重复赋值、++、引用、数组偏移或 unset
  6. 属性保存的是对象时,是否还需要约束对象内部的方法和字段变化。

官方规则可以从 PHP 属性手册和 PHP 8.1 发布说明继续核对:https://www.php.net/manual/en/language.oop5.properties.phphttps://www.php.net/releases/8.1/en.php。这两个入口足以覆盖声明语法、初始化限制、间接修改和版本背景。

相关问题

readonly 属性能不能写默认值?

不能写属性级默认值;把默认值作为构造参数默认值,再在构造方法中直接赋给属性。

readonly 数组能不能追加元素?

不能。数组偏移追加属于间接修改,应复制数组并创建新的对象。

readonly 对象是不是完全不可变?

不是。属性引用不能替换,但对象自身仍可能通过方法或公开字段改变内部状态。

什么时候应该用 readonly class?

当类的大多数实例属性都需要一次初始化后保持不变,并且项目运行在 PHP 8.2 及以上时,再考虑用类级声明统一约束。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>