
PHPStan 错误指南property.readOnlyByPhpDocAssignNotOnThis—— readonly 属性为何必须赋值在 $this 上【免费下载链接】phpstanPHP Static Analysis Tool - discover bugs in your code without running it!项目地址: https://gitcode.com/gh_mirrors/ph/phpstan本文以 PHPStan 官方错误标识error identifier文档 property.readOnlyByPhpDocAssignNotOnThis.md 为骨架结合仓库中 PHPDoc 基础文档 及 错误标识索引 中记录的规则实现深入讲解readonly属性在构造函数内赋值时为何必须作用于$this、该错误在什么场景触发以及三种可落地的修复方案。读完本文你将理解 PHPStan 只读语义的完整约束边界并能用ignoreErrors精确忽略这类误报。错误概览何时触发本标识错误标识property.readOnlyByPhpDocAssignNotOnThis英文短描述readonlyproperty is assigned on a different instance instead of$this.readonly属性被赋值到了另一个实例上而不是$this。所属规则类PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyAssignRule见 errorsIdentifiers.json 中 2.3.x 分支的规则定位可忽略性ignorable: true即该错误可通过ignoreErrors配置精确忽略说明中允许开发者针对特定代码位置豁免此检查。触发场景与完整代码示例以下代码会让 PHPStan 报出该错误?php declare(strict_types 1); class Foo { /** readonly */ public int $value; public function __construct(self $other) { $other-value 10; // ERROR: readonly property Foo::$value is not assigned on $this. } }关键点在于构造函数参数是同类self的另一个实例而赋值操作$other-value 10的目标对象是$other而不是当前正在构造的对象$this。与之形成对比的是合法写法——在构造函数里对$this赋值是允许的?php declare(strict_types 1); class Foo { /** readonly */ private int $value; public function __construct(int $value) { $this-value $value; // OK } }为什么会报告此错误readonly 的契约边界根据 property.readOnlyByPhpDocAssignNotOnThis.md 的说明该错误基于以下语义readonly属性只应被赋值一次且赋值时机限定在“初始化阶段”。PHPStan 将初始化阶段界定为声明类自身的构造函数内部。构造函数内的赋值必须作用于$this。赋值到同一类的其他实例并不算“初始化本对象”因为它修改的是另一个对象的状态——该对象可能早已完成初始化。这就违背了 readonly 契约。与相邻错误标识的边界区分readonly校验并非只有一个标识理解这一点有助于准确归类报错。从 errorsIdentifiers.json 可以看到ReadOnlyByPhpDocPropertyAssignRule这条规则同时产出多个标识各自对应不同的违约形式错误标识触发条件property.readOnlyByPhpDocAssignOutOfClass在声明类外部给readonly属性赋值例如其他类中$foo-value 42;对应 property.readOnlyByPhpDocAssignOutOfClass.mdproperty.readOnlyByPhpDocAssignNotInConstructor在声明类内部但构造函数之外的方法中对$this的readonly属性赋值对应 property.readOnlyByPhpDocAssignNotInConstructor.mdproperty.readOnlyByPhpDocAssignNotOnThis在构造函数内给readonly属性赋值但赋值目标是同类实例而非$this本文主题property.readOnlyByPhpDocAssignByRef以引用by-ref方式传递readonly属性见ReadOnlyByPhpDocPropertyAssignRefRuleproperty.readOnlyByPhpDocDefaultValuereadonly属性声明了默认值见ReadOnlyByPhpDocPropertyRule换言之ReadOnlyByPhpDocPropertyAssignRule从“赋值位置”类外/类内构造函数外与“赋值目标”非$this的实例两个维度分别判定违约类型本文讨论的是“位置合法构造函数内但目标不合法非$this”的中间情形。设计意图为什么不允许给同类的其他实例赋值即使$other与$this属于同一个类$other也是一个独立对象。它的readonly属性应当由它自己的构造函数初始化。在另一个对象哪怕同类的构造函数里改写它等于绕过了该对象自身的初始化流程破坏了“只读属性不可二次写入”的不变式。这正是文档中“assigning areadonlyproperty on a different object instance violates the readonly contract because it modifies state that may have already been initialized”的含义。修复方案一改为在 $this 上初始化推荐如果$other实例的值确实只是用来作为初始化数据那么应该把构造函数的入参从对象改为标量值并在$this上赋值?php declare(strict_types 1); class Foo { /** readonly */ public int $value; - public function __construct(self $other) public function __construct(int $value) { - $other-value 10; $this-value $value; } }这样既保留了readonly的不可变性约束又让初始化发生在当前对象的构造函数内符合契约。修复方案二去掉 readonly 注解允许跨实例写入如果业务上确实需要让同类的不同实例之间互相改写属性例如克隆后同步状态则应移除readonly注解放弃只读约束?php declare(strict_types 1); class Foo { - /** readonly */ public int $value; public function __construct(self $other) { $other-value 10; } }移除注解后该属性恢复为普通可变属性PHPStan 不再对此赋值行为做任何只读校验。修复方案三类级/属性级只读语义的配套工具除上述直接修复外PHPStan 还提供若干配套机制可结合场景选择phpstan-allow-private-mutation与readonly组合当希望“对外只读、类内部允许变更”时使用组合写法phpstan-readonly-allow-private-mutation等价于同时声明两者。参见 phpdocs-basics.mdclass Foo { /** * readonly * phpstan-allow-private-mutation */ public int $counter 0; /** phpstan-readonly-allow-private-mutation */ public string $name ; public function increment(): void { $this-counter; // OK - private mutation is allowed $this-name foo; // OK } } (new Foo())-counter 5; // Error: readonly property Foo::$counter is assigned outside of its declaring class.类级immutable/readonly把类标记为不可变后PHPStan 会将类的所有属性视为只读/** immutable */ class Foo { public string $bar; } (new Foo())-bar baz; // readonly property Foo::$bar is assigned outside of its declaring class.with*不可变更新模式与其在类内“原地修改”只读属性会触发property.readOnlyByPhpDocAssignNotInConstructor不如返回携带新值的新实例这也是 property.readOnlyByPhpDocAssignNotInConstructor.md 推荐的替代做法class Foo { /** readonly */ private int $value; public function __construct(int $value) { $this-value $value; } public function withValue(int $newValue): self { return new self($newValue); } }将本错误加入 ignoreErrors可选由于该标识声明了ignorable: true当团队经过评审确认某处跨实例赋值是刻意设计例如序列化/反序列化框架的回填逻辑可以在phpstan.neon中按标识精确豁免parameters: ignoreErrors: - identifier: property.readOnlyByPhpDocAssignNotOnThis path: src/Persistence/*.php不过需要注意精确忽略应只用于经过评审的例外。readonly的核心价值在于把“只读”约束交给静态分析在编码期强制执行规避此类行为是首选忽略只是兜底手段。小结property.readOnlyByPhpDocAssignNotOnThis是 PHPStan 对readonly属性在构造函数中“赋值目标错误”的静态检查结果。其背后规则ReadOnlyByPhpDocPropertyAssignRule与类外赋值...AssignOutOfClass、类内非构造函数赋值...AssignNotInConstructor等标识共同构成一套完整的只读契约校验体系。实践中优先把初始化收敛到$this需要跨实例写入时再权衡去掉注解或使用phpstan-allow-private-mutation即可在保持代码不可变性的同时让 PHPStan 检查结果清晰可控。【免费下载链接】phpstanPHP Static Analysis Tool - discover bugs in your code without running it!项目地址: https://gitcode.com/gh_mirrors/ph/phpstan创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考