PHP 8.1以下不支持readonly因是该版本引入的语法特性,低版本解析直接报错;需用private属性+__construct赋值+__get/__set模拟,配合@property-read注解和严格测试保障只读语义。

PHP 8.1以下为什么不能直接用readonly关键字
因为readonly是PHP 8.1引入的原生语法特性,低版本解析器会直接报Parse error: syntax error, unexpected token "readonly"。这不是运行时限制,而是词法分析阶段就失败——你连php -l都过不了。所以任何“兼容写法”必须绕过语法校验,靠约定+运行时防护实现模拟。
用__get + 私有属性 + 构造赋值是最稳妥的模拟方式
核心思路:属性声明为private,只在__construct中赋值一次,后续通过__get提供只读访问,同时拦截__set抛异常。这种方式不依赖扩展,兼容PHP 7.4+,且IDE能识别属性存在(配合PHPDoc)。
实操要点:
- 必须把属性设为
private,不能是protected——否则子类可绕过__set直接写入 - 在
__construct里完成所有只读属性赋值,避免后续任何方法意外修改 -
__get中用isset($this->prop)判断是否已初始化,未初始化就抛LogicException而非静默返回null - 在
__set里严格检查属性名,匹配到只读属性就抛RuntimeException,消息里明确写“Cannot modify readonly property”
示例片段:
立即学习“PHP免费学习笔记(深入)”;
class User
{
private string $id;
private string $name;
public function __construct(string $id, string $name)
{
$this->id = $id;
$this->name = $name;
}
public function __get(string $name): mixed
{
if (!isset($this->$name)) {
throw new \LogicException("Property {$name} is not initialized");
}
return $this->$name;
}
public function __set(string $name, mixed $value): void
{
if (in_array($name, ['id', 'name'], true)) {
throw new \RuntimeException("Cannot modify readonly property {$name}");
}
$this->$name = $value;
}
}
PHPDoc标注@property-read让IDE和静态分析工具识别
没有类型系统支持,光靠运行时防护不够。开发者写代码时可能误以为能赋值,或IDE无法提示只读性。必须补全PHPDoc,否则phpstan、psalm和VS Code/PhpStorm都会当作普通可写属性处理。
正确做法:
- 在类文档块中为每个只读属性加
@property-read string $id - 不要漏掉
@property-read的-read后缀,写成@property会被当成可读可写 - 如果属性是对象类型,需写完整命名空间,如
@property-read \DateTimeImmutable $createdAt - 配合
phpstan-phpdoc-plugin等插件,能让静态分析真正捕获对只读属性的赋值尝试
注意魔术方法性能开销和继承场景的陷阱
__get/__set每次访问都触发函数调用,比直接访问属性慢3–5倍。高频读取场景(如循环内)要权衡。更重要的是,子类若重写__set但没调用parent::__set(),只读保护就彻底失效。
规避建议:
- 对性能敏感字段,改用公共
getXXX()方法封装,属性保持private,既避免魔术方法开销,又保留控制权 - 父类
__set里用final修饰(PHP 8.0+),或文档强制要求子类调用parent::__set() - 测试用例必须覆盖“子类尝试赋值只读属性”的场景,验证是否真抛异常
- 别试图用
ReflectionProperty::setAccessible(false)——它只影响反射,不影响正常赋值
真正的难点不在实现,而在团队对“只读”语义的一致理解:构造之后谁都不能碰,包括子类、序列化回调、反序列化逻辑。一旦有地方绕过__set(比如unserialize()直接填充属性),整个模拟就形同虚设。



















