- 开发工具
- 代码质量
- 静态分析
【免费下载链接】phpstan
PHP Static Analysis Tool - discover bugs in your code without running it!
本篇技术指南围绕 PHPStan 错误标识符new.internalInterface展开,讲解静态分析器在"实例化(new)一个标记为@internal的内部接口"场景下如何报错、为何报错以及如何修复。读完本文,你将掌握 PHPStan 内部 API 约束体系的判定逻辑、new前缀标识符家族的组织方式,以及在实际项目中安全处理内部接口引用的完整实战方案。
一、错误标识符总览:什么是new.internalInterface
new.internalInterface是 PHPStan 在实例化上下文(instantiation context)中检测到内部接口引用时报告的错误标识符。其官方定义为:
Referencing an internal interface in an instantiation context.(在实例化上下文中引用了内部接口。)
该标识符的元数据在 website/errors/new.internalInterface.md 的 frontmatter 中声明了三个关键属性:
title: "new.internalInterface":错误标识符全名;ignorable: true:该错误可通过ignoreErrors配置忽略;unlikely: true:该标识符被标记为"不太可能单独出现"——因为接口根本无法实例化,此场景通常会被更基础的new.interface错误优先命中(详见下文第三节)。
从错误标识符的命名规则看(参见 CLAUDE.md 中的前缀参考表),前缀new明确对应new ClassName()实例化表达式。同样的前缀还派生出new.interface、new.internalTrait、new.noConstructor、new.nonObject、new.privateConstructor、new.protectedConstructor等一组与实例化相关的标识符(见 website/src/errorsIdentifiers.json)。
从源码结构看该标识符的来源
根据错误标识符与规则类的映射表 website/src/errorsIdentifiers.json,new.internalInterface由规则类PHPStan\Rules\InternalTag\RestrictedInternalClassNameUsageExtension产生。该类属于 phpstan-src 仓库中src/Rules/InternalTag/目录下的"内部标签限制"规则族——该规则族专门负责检查@internal标记类型在各种使用位置的越权访问,与new.internalInterface同族的标识符还包括assert.internalInterface、instanceof.internalInterface、catch.internalInterface、property.internalInterface、staticMethod.internalInterface等二十余个(这些文档均收录于 website/errors 目录)。
二、触发场景:完整的可复现示例
文档给出了最小可复现示例(见 website/errors/new.internalInterface.md):
<?php declare(strict_types = 1); namespace Vendor { /** @internal */ interface Handler { public function handle(): void; } } namespace App { $handler = new \Vendor\Handler(); // error: Instantiation of internal interface Vendor\Handler. }该示例包含两个关键要素:
- 接口被标记为
@internal:Vendor命名空间中的Handler接口通过 PHPDoc 标签@internal声明为包内部类型; - 在外部命名空间进行实例化:
App命名空间中通过new \Vendor\Handler()试图实例化该接口。
PHPStan 对第二个要素——new实例化表达式中的\Vendor\Handler类型引用——进行检查时发现它指向一个@internal接口,于是上报new.internalInterface错误,提示信息为Instantiation of internal interface Vendor\Handler.。
需要注意的是,示例中的$handler = new \Vendor\Handler();只是触发检测的最小代码形态。实际项目中,@internal接口的"越权使用"不局限于实例化,凡是类型名出现在任何使用位置都可能被同族标识符命中——例如instanceof检查对应instanceof.internalInterface(见 website/errors/instanceof.internalInterface.md),@phpstan-assert断言对应assert.internalInterface(见 website/errors/assert.internalInterface.md)。理解这一族错误,本质上就是理解 PHPStan 对@internal契约的强制约束。
三、为什么很少单独出现:与new.interface的优先级关系
原文档明确指出一个重要的实践经验(见 website/errors/new.internalInterface.md):
In practice, this is typically reported as
new.interfacebecause interfaces cannot be instantiated at all. Thenew.internalInterfaceidentifier is reported when the internal access violation is the primary concern.
翻译过来即:实际场景中该错误通常表现为new.interface,因为接口从根本上就无法实例化;只有当"内部 API 越权访问"成为首要问题时,PHPStan 才报告new.internalInterface。
对比new.interface的文档(见 website/errors/new.interface.md):
<?php declare(strict_types = 1); interface LoggerInterface { public function log(string $message): void; } $logger = new LoggerInterface();new.interface描述的问题是"接口不能被实例化"——这是 PHP 语言层面的硬性约束:接口只定义契约、不提供方法实现,new一个接口名会在运行时直接触发致命错误。而new.internalInterface描述的问题是"内部接口被外部越权引用"——这是 API 设计层面的约束:@internal类型不保证向后兼容。
两个标识符的关系可以归纳为:
| 维度 | new.interface | new.internalInterface |
|---|---|---|
| 核心问题 | 接口无法实例化(语言语义) | 内部类型被越权使用(API 契约) |
| 判定来源 | InstantiationRule(见 errorsIdentifiers.json 中new.nonObject等同类规则) | RestrictedInternalClassNameUsageExtension(内部标签规则族) |
| 报告时机 | 只要new目标是接口即报告 | 仅当内部访问违规是首要关注点时报告 |
| 严重程度 | 运行期必然致命错误 | 可能当前能运行,但未来版本随时会坏 |
因此,如果你的代码同时命中两者,PHPStan 通常优先报告new.interface(接口实例化本身就不合法);只有内部契约的破坏才是分析重点时,new.internalInterface才作为独立标识符浮出水面。这也解释了 frontmatter 中unlikely: true标记的缘由。
四、为什么会报告:@internal的 API 契约语义
原文档"Why is it reported?"一节给出的核心解释(见 website/errors/new.internalInterface.md):
- 内部接口正在被实例化,且调用方位于其根命名空间之外;
- 标记为
@internal的接口不打算在定义它的包或命名空间之外被使用; - 内部接口可能在未来的版本中无通知地变更或被移除。
这三点构成了 PHPStan 报告此类错误的设计动机:@internal是 PHP 生态中广泛认可的一种"实现细节"标记。第三方包用它在公开 API 表面之下隐藏实现细节;而外部代码一旦直接引用@internal类型,就形成了一种脆弱的、对实现细节的隐式依赖——当包的下一个版本重构内部结构时,这类引用会静默损坏。PHPStan 通过静态分析在编译期/分析期就暴露出这种隐患,而不是等到升级依赖后由运行时错误来惩罚。
从同类文档的表述可以更完整地理解这套契约(如 website/errors/assert.internalInterface.md 所述):依赖内部类型会在你的断言、实例化、instanceof等位置形成对"会无通知变更的实现细节"的脆弱依赖。这本质上是把 API 边界问题前置到了静态分析阶段。
五、如何修复:两种标准解法
原文档给出了两种修复路径(见 website/errors/new.internalInterface.md)。
解法一:改用包的公开 API
不要直接new内部接口,而是使用包公共 API 提供的工厂方法或具体类:
namespace App { - $handler = new \Vendor\Handler(); + $handler = \Vendor\HandlerFactory::create(); }这一方案的要点是:Vendor包既然把Handler标记为@internal,说明它期望调用方通过公开的创建入口(工厂、构造器、DI 容器、服务定位器等)获得实例,而不是自己直接实例化。HandlerFactory::create()返回的具体类型可能是Handler接口的公开实现类,也可能是满足公开契约的别的类型——总之,类型引用的书写位置从"越权使用内部类型"变成了"合法使用公开 API"。
解法二:从源头开放内部接口
如果你自己就是该内部接口的维护者,可以考虑两条路:
- 将接口设为公开:移除
@internal标记,将其纳入包的正式公共 API 并承诺向后兼容; - 提供公开替代品:保留内部接口不动,另设计一个公开接口(或抽象类),并让内部实现实现它。
如果两个方向都不可行(例如你只是第三方代码的使用者、无法影响上游包),文档建议与包维护者沟通,请求为你的用例提供公开 API。
修复时的通用原则
结合 CLAUDE.md 中的修复偏好顺序,此类错误的修复优先级应为:
- 修复真正的 bug——使用公开类型替换内部类型引用;
- 如果问题出在类型声明上,用原生 PHP 类型声明或 PHPDoc 类型(
@param、@return、@var)收紧类型; - 在函数体内做类型收窄;
- 只有规则可配置时才考虑调整 PHPStan 配置。
注意:不要通过ignoreErrors忽略该错误来"解决"问题——ignorable: true只说明技术上允许忽略,但忽略意味着把脆弱依赖继续留在代码里,与 PHPStan 报告它的初衷背道而驰。
六、进阶:new.internalInterface与new.internalTrait的同族关系
在 website/src/errorsIdentifiers.json 中可以看到,紧邻new.internalInterface的是new.internalTrait,二者映射到同一个规则类RestrictedInternalClassNameUsageExtension,且源码位置完全一致(src/Rules/InternalTag/RestrictedInternalClassNameUsageExtension.php#L65)。
这说明 PHPStan 内部标签规则族对"内部类名在实例化上下文中的使用"采用统一判定:只要new的目标是被@internal标记的类、接口或 trait,就由同一规则上报new.internal*系列标识符。同理,new.interface则聚焦"接口不可实例化"这一与@internal无关的通用问题。
new前缀下的标识符家族(映射自 errorsIdentifiers.json)还包括:
| 标识符 | 含义 |
|---|---|
new.noConstructor | 目标类没有构造函数(对应InstantiationRule) |
new.nonObject | new的目标不是可实例化对象 |
new.privateConstructor/new.protectedConstructor | 构造函数可见性不允许外部实例化 |
理解这条家族脉络,有助于你在排查实例化相关报错时快速定位:new.internal*看 API 契约,new.interface看语言语义,new.privateConstructor等看访问控制——三者是不同层面的约束,修复策略也各不相同。
七、实践要点总结
- 快速识别:看到错误信息
Instantiation of internal interface <FQCN>.,即对应new.internalInterface;标识符的 frontmatter 文档位于 website/errors/new.internalInterface.md。 - 判定根因:检查被
new的类型是否带有@internal标记,以及调用代码是否位于该类型定义命名空间之外。 - 优先修复顺序:换用工厂/公开实现 → 开放接口(若你拥有该类型)→ 与上游维护者沟通申请公开 API。
- 识别连带风险:即使代码当前能运行,
@internal引用也是"定时炸弹",升级依赖时可能随时失效,因此不应以忽略错误了事。 - 举一反三:同样的
@internal契约约束遍布instanceof、catch、@phpstan-assert、属性声明、静态方法调用等所有类型使用位置,分别对应instanceof.internalInterface、catch.internalInterface、assert.internalInterface、property.internalInterface、staticMethod.internalInterface等同族标识符,完整列表可查阅 website/src/errorsIdentifiers.json 与 website/errors 目录下的对应文档。
延伸阅读:想深入了解 PHPStan 错误标识符文档体系的生成规则与命名前缀约定,可阅读 website/errors/CLAUDE.md;想了解同族@internal约束在instanceof场景下的表现,可对照 website/errors/instanceof.internalInterface.md。
- 开发工具
- 代码质量
- 静态分析
【免费下载链接】phpstan
PHP Static Analysis Tool - discover bugs in your code without running it!
相关推荐
PHPStan 错误标识符 `instanceof.internalInterface` 详解:instanceof 检查引用了 `@internal` 内部接口
PHPStan 错误标识符 instanceof.internalInterface 详解:instanceof 检查引用了 @internal 内部接口 本文
开发工具代码质量静态分析WinUI 3 应用动效与打磨实战:主题过渡、连接动画与动画纪律
WinUI 3 应用动效与打磨实战:主题过渡、连接动画与动画纪律 导读 本文围绕 winui app 技能库中的动效参考文档 motion animations
开发工具代码质量静态分析PHPStan 错误标识符 interface.extendsInternalEnum 全解析:接口继承 @internal 枚举
PHPStan 错误标识符 interface.extendsInternalEnum 全解析:接口继承 @internal 枚举 导读 interface.e
开发工具代码质量静态分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考