- 开发工具
- 代码质量
- 静态分析
【免费下载链接】phpstan
PHP Static Analysis Tool - discover bugs in your code without running it!
本篇技术指南围绕 PHPStan 错误标识符new.static展开,讲解它在非 final 类中因晚期静态绑定(late static binding)与子类构造器重写组合而引发的安全隐患,并给出五种可在当前项目中直接落地的修复方案(final 类、final 构造器、abstract 构造器、接口约束、@phpstan-consistent-constructor注解)。读完本文,你将能够准确复现该错误、理解其底层触发机制,并根据类的继承设计意图选择最合适的修复方式。
错误速览
| 项目 | 内容 |
|---|---|
| 错误标识符(identifier) | new.static |
| 错误信息 | Unsafe usage of new static() |
| 报告位置 | 非 final 类中出现new static()的调用处 |
| 是否可忽略 | 错误文档 frontmatter 标记ignorable: true,可通过ignoreErrors按标识符忽略 |
| 参考文档 | new.static.md |
该错误文档位于 website/errors/new.static.md,属于 PHPStan 网站的错误参考库(website/errors/目录下每个错误一个 Markdown 文件),与仓库中的其他错误文档(如argument.type、class.notFound)采用相同的 frontmatter + 示例 + 修复的结构。
触发场景:一段最小复现代码
以下代码(摘自错误文档)即可稳定触发该错误:
<?php declare(strict_types = 1); class Foo { public function create(): static { return new static(); } }关键要素有两个:
- 类
Foo未声明为final,即保留被继承的可能; - 方法返回类型为
static(晚期静态绑定)并执行new static(),即实例化"调用时刻的实际类"而非写死Foo。
两者同时存在时,PHPStan 就会报出Unsafe usage of new static()。
为什么会被报告:子类重写构造器导致的运行时崩溃
错误文档给出的核心原因是:在非 final 类中使用new static()是不安全的,因为子类可能以不同的参数或要求重写构造函数。new static()会创建"实际运行时类"(可能是某个子类)的实例,但实例化时执行的是父类代码里的构造调用。一旦子类改变了构造器签名,这里就会在运行时崩溃。
配套的官方博客 solving-phpstan-error-unsafe-usage-of-new-static.md 给出了一个非常直观的崩溃演示:
<?php declare(strict_types = 1); class Foo { public function __construct(int $i) { } public function doFoo(): void { new static(1); // PHPStan reports: Unsafe usage of new static() } } class Bar extends Foo { public function __construct(string $s) { } } (new Foo(1))->doFoo(); // works, returns Foo (new Bar('s'))->doFoo(); // crashes with: Argument #1 ($s) must be of type string, int given执行过程拆解:
Foo::doFoo()内部调用new static(1);- 当调用对象是
Bar实例时,static解析为Bar,于是执行new Bar(1); - 但
Bar::__construct(string $s)要求字符串,传入整数1触发参数类型错误,程序崩溃。
也就是说,"父类代码写得没问题"并不等于"在继承体系下永远没问题"。PHPStan 的这一规则是在静态分析阶段提前拦截这类"一旦被继承就会爆炸"的隐患,而不是等运行时才暴露。
五种修复方案(按继承设计意图选择)
方案一:把类声明为 final(最简单)
如果这个类本就不打算被继承,直接封死继承即可。错误文档给出的 diff:
-class Foo +final class Foo { public function create(): static { return new static(); } }配套博客补充了一个细节:类变成 final 后,new static()与new self()在功能上完全等价,此时可以顺手把new static()改写为new self(),语义更清晰。
方案二:把构造函数声明为 final
如果希望类保持开放继承,但不允许子类重写构造器,则把构造器final化:
class Foo { + final public function __construct() + { + } + public function create(): static { return new static(); } }配套博客中的等价写法(带参数的构造器):
final public function __construct(int $i) { }这样子类无法改变构造器签名,new static(...)的调用参数在任何继承层级下都是一致的,安全性得到保证。
方案三:把构造函数声明为 abstract
如果允许子类定义自己的构造器,但又想强制所有子类的构造器签名一致,可以将构造器抽象化:
abstract public function __construct(int $i);这种方式由 PHP 语言本身强制子类遵循签名。配套博客指出其缺点:子类必须自行定义构造器,不能再继承父类的实现。
方案四:通过接口强制构造器签名
在"保留父类构造器实现、同时强制子类签名一致"的场景下,可以用接口替代 abstract 构造器:
interface FooInterface { public function __construct(int $i); } class Foo implements FooInterface { public function __construct(int $i) { ... } ... }子类可以继承父类的构造器实现;但如果子类自行定义构造器,其签名将由 PHP(以及 PHPStan)依据接口强制校验。
方案五:使用@phpstan-consistent-constructor注解(推荐用于可扩展类)
错误文档给出的第三种 diff 修复方式,是为类添加 PHPDoc 注解,声明"所有子类必须拥有兼容的构造器":
+/** @phpstan-consistent-constructor */ class Foo { public function create(): static { return new static(); } }官方 PHPDoc 文档 phpdocs-basics.md 对该注解有更完整的定义与示例:
/** @phpstan-consistent-constructor */ class Foo { public function __construct(string $name) { } public static function create(string $name): static { return new static($name); // OK - constructor signature is guaranteed } } class Bar extends Foo { public function __construct(int $id) // Error: not compatible with Foo::__construct() { } }关于该注解的三个关键规则(依据 phpdocs-basics.md):
- 传递性生效:一旦类被标记
@phpstan-consistent-constructor,该约束会传递性地作用于所有子类; - 构造器可见性约束:被标记类的构造器不能是 private(除非该类本身是 final);
- 静态工厂方法场景:在
Foo::create(): static这类返回static的静态工厂方法中,只要类被注解标记,new static($name)即为安全调用,因为所有子类的构造器签名已被保证兼容。
相关错误:抽象类静态方法中的 new static()
与new.static同族的还有一个错误new.staticInAbstractClassStaticMethod,文档见 new.staticInAbstractClassStaticMethod.md。它在抽象类的静态方法中使用new static()时触发:
<?php declare(strict_types = 1); abstract class Creator { public static function create(): static { return new static(); } }其触发原因与new.static不同:静态方法可以不经过实例化直接以Creator::create()形式调用,而抽象类本身无法被实例化,于是new static()在运行时直接抛出 fatal error。注意与实例方法的区别——实例方法调用前必须已存在一个对象,而静态方法没有这层保障。
该错误的修复方式同样有两种(对应不同设计意图):
- 将静态方法声明为 abstract,强制每个具体子类提供自己的实现:
<?php declare(strict_types = 1); abstract class Creator { - public static function create(): static - { - return new static(); - } + abstract public static function create(): static; }- 将类改为 final,使其可以被安全实例化:
<?php declare(strict_types = 1); -abstract class Creator +final class Creator { public static function create(): static { return new static(); } }何时需要忽略该错误
new.static错误文档的 frontmatter 中标有ignorable: true,意味着该错误可以通过 PHPStan 的ignoreErrors机制按标识符忽略。典型场景是:团队评估后确认当前类的继承体系完全受控、构造器签名永远不会变化,此时可以在phpstan.neon中定向放行:
parameters: ignoreErrors: - identifier: new.static path: src/SomeDirectory/*从仓库的website/errors/目录看,该目录下所有错误文档(如 consistentConstructor.private.md)均以identifier作为文件名,说明每个错误都可以通过标识符在ignoreErrors中精确匹配,无需使用脆弱的正则匹配错误文案。需要说明的是:忽略应作为例外手段,多数情况下优先采用上述五种修复方案之一,让代码在分析层面即保持安全。
小结
| 场景 | 推荐方案 | 代价 |
|---|---|---|
| 类不打算被继承 | 声明final | 失去扩展性 |
| 允许继承、不允许重写构造器 | final构造器 | 子类无法定制构造 |
| 允许继承、强制统一签名 | @phpstan-consistent-constructor | 子类构造器签名受限 |
| 允许继承、强制统一签名且子类必定义构造器 | abstract构造器 | 子类必须实现 |
| 允许继承、强制统一签名且父类保留实现 | 接口约束 | 需要引入接口 |
| 受控继承体系、确定安全 | ignoreErrors按new.static忽略 | 放弃该处静态保护 |
该错误的价值在于:PHPStan 把"继承体系中的晚期静态绑定 + 构造器重写"这对组合可能引发的运行时崩溃,提前到静态分析阶段暴露。结合本文的源码示例与 官方博客、PHPDoc 注解文档 以及 错误参考库,你可以针对每种继承设计意图选择最合适的修复路径,让代码既保持扩展性又获得静态分析层面的安全保障。
- 开发工具
- 代码质量
- 静态分析
【免费下载链接】phpstan
PHP Static Analysis Tool - discover bugs in your code without running it!
相关推荐
PHPStan 纯度分析实战:理解并修复 possiblyImpure.functionCall 错误
PHPStan 纯度分析实战:理解并修复 possiblyImpure.functionCall 错误 possiblyImpure.functionCall
开发工具代码质量静态分析PHPStan 错误标识 `new.trait` 全解析:为什么不能 `new` 一个 Trait,以及如何修复
PHPStan 错误标识 new.trait 全解析:为什么不能 new 一个 Trait,以及如何修复 new.trait 是 PHPStan 静态分析器(
开发工具代码质量静态分析PHPStan arrayFilter.empty 错误详解:如何识别并修复"空数组上的 array_filter 调用"
PHPStan arrayFilter.empty 错误详解:如何识别并修复"空数组上的 array_filter 调用" arrayFilter.empty
开发工具代码质量静态分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考