PHPStan 错误指南:如何理解并修复 “Unsafe usage of new static()“
2026/9/24 6:20:08 网站建设 项目流程
  • 开发工具
  • 代码质量
  • 静态分析

【免费下载链接】phpstan

PHP Static Analysis Tool - discover bugs in your code without running it!

项目地址:https://gitcode.com/gh_mirrors/ph/phpstan
点击查看免费下载

本篇技术指南围绕 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.typeclass.notFound)采用相同的 frontmatter + 示例 + 修复的结构。

触发场景:一段最小复现代码

以下代码(摘自错误文档)即可稳定触发该错误:

<?php declare(strict_types = 1); class Foo { public function create(): static { return new static(); } }

关键要素有两个:

  1. Foo未声明为final,即保留被继承的可能;
  2. 方法返回类型为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

执行过程拆解:

  1. Foo::doFoo()内部调用new static(1)
  2. 当调用对象是Bar实例时,static解析为Bar,于是执行new Bar(1)
  3. 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):

  1. 传递性生效:一旦类被标记@phpstan-consistent-constructor,该约束会传递性地作用于所有子类
  2. 构造器可见性约束:被标记类的构造器不能是 private(除非该类本身是 final);
  3. 静态工厂方法场景:在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构造器子类必须实现
允许继承、强制统一签名且父类保留实现接口约束需要引入接口
受控继承体系、确定安全ignoreErrorsnew.static忽略放弃该处静态保护

该错误的价值在于:PHPStan 把"继承体系中的晚期静态绑定 + 构造器重写"这对组合可能引发的运行时崩溃,提前到静态分析阶段暴露。结合本文的源码示例与 官方博客、PHPDoc 注解文档 以及 错误参考库,你可以针对每种继承设计意图选择最合适的修复路径,让代码既保持扩展性又获得静态分析层面的安全保障。

  • 开发工具
  • 代码质量
  • 静态分析

【免费下载链接】phpstan

PHP Static Analysis Tool - discover bugs in your code without running it!

项目地址:https://gitcode.com/gh_mirrors/ph/phpstan
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询