- 开发工具
- 代码质量
- 静态分析
【免费下载链接】phpstan
PHP Static Analysis Tool - discover bugs in your code without running it!
导读
new.internalEnum是 PHPStan 静态分析工具用于标识"从包外部实例化被标记为@internal的枚举"这一违规行为的错误标识符。本文以 new.internalEnum.md 为核心骨架,结合仓库内 errorsIdentifiers.json 中的标识符注册信息与 restricted-usage-extensions.md 中的扩展机制文档,完整讲解该标识符的触发场景、报告原理、与new.enum的边界关系,以及从实操到源码级的修复与规避策略。读完本文,你将能准确识别这类内部 API 误用,理解 PHPStan 为何及如何将其与"普通枚举实例化"区分开,并掌握面向@internal符号的规范开发与配置实践。
一、什么是 new.internalEnum:标识符语义与触发场景
new.internalEnum的 shortDescription 为"Referencing an internal enum in an instantiation context",即"在实例化上下文中引用内部枚举"。它属于 PHPStan 错误标识符体系中的一员,与new.internalClass、new.internalInterface、new.internalTrait构成new.*系列中针对@internal符号的同类检测族。
从 errorsIdentifiers.json 的注册信息可以看出,该标识符由规则类PHPStan\Rules\InternalTag\RestrictedInternalClassNameUsageExtension产生,其源码位置对应 phpstan-src 2.3.x 分支的src/Rules/InternalTag/RestrictedInternalClassNameUsageExtension.php。也就是说,new.internalEnum并非独立手写的规则,而是"受限内部类名使用"扩展在"实例化"这一具体位置上的产物——同一个扩展还会在其它使用位置上生成propertyTag.internalClass、class.implementsInternalClass等不同的标识符。
触发代码示例
原文档给出了最小复现示例:
<?php declare(strict_types = 1); namespace Vendor { /** @internal */ enum CacheDriver { case Redis; case Memcached; } } namespace App { $driver = new \Vendor\CacheDriver(); // error: Instantiation of internal enum Vendor\CacheDriver. }复现要点:
- 定义方:
Vendor命名空间内声明了一个带/** @internal */注解的枚举CacheDriver,包含Redis、Memcached两个 case; - 使用方:
App命名空间中用new关键字试图实例化该枚举,这一行即触发new.internalEnum报告; - 跨包/跨命名空间:使用位置处于枚举根命名空间之外,这是"内部 API 越界访问"判断成立的前提。
运行 PHPStan 后,对应错误消息为:Instantiation of internal enum Vendor\CacheDriver.。
Frontmatter 元信息解读
原文档 frontmatter 中ignorable: true与unlikely: true两个字段值得注意:
ignorable: true表示该标识符默认允许被忽略,可在phpstan.neon的ignoreErrors中通过identifier定向忽略(可结合 conf/bleedingEdge.neon 与配置参考文档了解忽略机制的运用);unlikely: true则提示该错误在实际分析中"不常见"——正如后文将分析的,因为枚举本身不可实例化,通常更先命中new.enum。
二、与 new.enum 的边界:为什么实际更常见到 new.enum
原文档明确指出:实践中,该错误通常以new.enum(Cannot instantiate enum)的形式被报告,因为枚举本身完全无法实例化。只有当"内部访问违规"是首要关注点时,new.internalEnum才会被报告。
对照 new.enum.md 文档:
enum Suit { case Hearts; case Diamonds; case Clubs; case Spades; } $suit = new Suit(); // error: Cannot instantiate enum Suit.new.enum描述的是 PHP 语言层面的硬性事实:PHP 中的枚举不能通过new关键字实例化,枚举 case 是预定义的单例,必须通过 case 名直接访问(如Suit::Hearts),运行时对枚举使用new会导致致命错误。
两者的分工可以这样理解:
| 标识符 | 报告侧重点 | 前提条件 |
|---|---|---|
new.enum | 语言层面:枚举不可实例化 | 任何枚举被new |
new.internalEnum | API 边界层面:@internal枚举被包外实例化 | 枚举被new且该枚举标记了@internal |
同样的关系也存在于new.internalTrait与new.trait之间——new.internalTrait.md 明确说明"触发该标识符需要实例化 trait,而 PHP 不允许这样做,因此 PHPStan 总是同时报告new.trait,实践中new.internalTrait不会伴随出现"。枚举场景的逻辑与之类似:由于new.enum总会先命中,new.internalEnum是"次要且罕见"的报告分支。这一点也解释了为什么其在 errorsIdentifiers.json 中仅对应一处源码位置、且 frontmatter 标记unlikely: true。
三、为什么会被报告:@internal 契约与公共 API 边界
原文档"Why is it reported?"部分的语义要点如下:
- 内部枚举正被用于实例化上下文:使用方从枚举的根命名空间之外引用它;
@internal意味着非公共 API:被标记的枚举不属于定义该枚举的包(package)的公开 API,只应在包内部使用;- 稳定性风险:内部实现细节可能在未来的任何版本中不经通知地变更或移除,不遵循语义化版本控制(semver)。
从 PHPStan 的规则设计看,/** @internal */是一种"显式声明 API 边界"的 PHPDoc 约定:开发者通过它向工具与协作者表明"这个符号不在契约之内"。PHPStan 的RestrictedInternalClassNameUsageExtension族规则正是把这种约定变成可执行的静态检查——任何从包外部(即 root namespace 之外)对@internal符号的使用都会被标记,而new实例化只是其中的一种使用位置。
这类报告的价值在于把"编译期可见但契约上不可用"的符号使用前置到 CI 阶段:如果不小心直接使用了内部枚举,升级依赖时可能因内部实现变更而静默出错,而 PHPStan 能在合并代码前就把这种脆弱依赖暴露出来。
四、如何修复:从替换调用到请求公共 API
原文档给出的标准修复路径是使用包提供的公共 API 替代直接引用内部枚举:
namespace App { - $driver = new \Vendor\CacheDriver(); + $driver = \Vendor\CacheFactory::create(); }修复要点:
- 寻找定义该枚举的包对外暴露的工厂方法、服务入口或公开类(本例为
\Vendor\CacheFactory::create()); - 如果包内不存在公共替代方案,则应联系包维护者,请求为所需功能提供公开 API——这正是
@internal契约的意图:迫使使用者走受支持的路径。
需要特别说明的是:由于枚举本就无法实例化,本例中的"正确用法"从纯语言层面讲应当是直接使用枚举 case(如\Vendor\CacheDriver::Redis)——但CacheDriver是@internal的,即使直接引用其 case 也属于内部 API 越界。因此这里正确的长期策略仍然是:改用包提供的公共 API(工厂方法返回的公开类型),而不是绕过@internal标记直接引用内部 case。这与new.internalClass文档(new.internalClass.md)中"使用包内公共 API 而非直接实例化内部类,若无公共替代则向维护者提交功能请求"的修复逻辑完全一致。
在实践中如何处理这类错误
由于该错误ignorable: true,若你的代码库确有合理原因临时引用内部符号(例如作为第三方集成方必须使用未公开接口),可以在phpstan.neon中按标识符定向忽略:
parameters: ignoreErrors: - identifier: new.internalEnum message: '#Instantiation of internal enum Vendor\\CacheDriver#' path: src/LegacyIntegration.php但需要注意:忽略只应作为受控例外,长期仍应推动依赖方向公共 API 迁移,否则升级依赖时面临静默破坏的风险。
五、源码级视角:new.internalEnum 从何而来
通过 errorsIdentifiers.json 可以精确回溯该标识符的生产链路:
- 规则类:
PHPStan\Rules\InternalTag\RestrictedInternalClassNameUsageExtension; - 源码锚点:phpstan-src 2.3.x 分支
src/Rules/InternalTag/RestrictedInternalClassNameUsageExtension.php(对应第 65 行附近的报告逻辑)。
结合 restricted-usage-extensions.md 对RestrictedClassNameUsageExtension机制的说明可以推断出完整链路:该扩展针对"独立类名引用"场景被调用,覆盖类继承、接口实现、参数与返回类型、PHPDoc 引用、静态方法调用、静态属性与类常量访问等25 个以上的使用位置。扩展实现通过ClassNameUsageLocation对象提供的createMessage()与createIdentifier()方法,把统一的"内部符号被外部引用"信息按位置差异化:
Instantiation of internal class Foo.→new.internalClass- 同理,
Instantiation of internal enum Vendor\CacheDriver.→new.internalEnum Class Bar implements internal class Foo.→class.implementsInternalClass
也就是说,new.internalEnum是RestrictedInternalClassNameUsageExtension在"实例化"这一ClassNameUsageLocation上针对枚举类型(getClassTypeDescription()返回enum)自动生成的标识符,与new.internalClass、new.internalInterface、new.internalTrait共享同一套底层报告机制,只是按符号类型与使用位置区分为不同 identifier。这正是"同一规则、多标识符"设计在 PHPStan 错误标识符体系中的体现。
六、同类标识符一览与扩展机制延伸
@internal检测并不只覆盖"实例化"位置。从 errorsIdentifiers.json 可以检索到同一 InternalTag 机制派生出的、针对枚举的兄弟标识符,例如:
assert.internalEnum:在断言上下文中引用内部枚举;attribute.internalEnum:在属性(Attribute)上下文中引用内部枚举;catch.internalEnum:在 catch 子句中引用内部枚举(配合new.internalEnum构成同一符号多位置覆盖);classConstant.internalEnum、instanceof.internalEnum、method.internalEnum、methodTag.internalEnum、mixin.internalEnum等。
这些标识符共同说明:PHPStan 对@internal的检查是按使用位置全覆盖的,而不是只在new时检查一次。如果你在代码库中看到形如xxx.internalEnum的其它标识符,其含义与修复思路同本文完全一致——从包外部访问了内部枚举,应改用公共 API。
对于想要自定义"内部使用限制"的扩展开发者,restricted-usage-extensions.md(标识为 Available in PHPStan 2.1.13)还提供了通用扩展接口:RestrictedMethodUsageExtension、RestrictedPropertyUsageExtension、RestrictedClassConstantUsageExtension、RestrictedFunctionUsageExtension与RestrictedClassNameUsageExtension,可通过注册phpstan.restrictedClassNameUsageExtension等 tag 挂载到配置中,实现完全自定义的受限使用规则——这也是@internal检查机制的"可编程化"延伸。
七、总结:识别、修复与防范
| 维度 | 结论 |
|---|---|
| 触发条件 | 在枚举根命名空间之外,用new实例化带/** @internal */的枚举 |
| 错误消息 | Instantiation of internal enum Vendor\CacheDriver. |
与new.enum关系 | 枚举本就不可实例化,实际通常先报new.enum;当内部访问违规是首要关注点时报告new.internalEnum |
| 根因 | @internal符号不是包的公共 API,可能随时变更或移除,包外直接使用形成脆弱依赖 |
| 标准修复 | 改用包提供的公共 API(如工厂方法);无公共替代时向维护者请求公开 API |
| 受控例外 | ignorable: true,可在phpstan.neon中按 identifier 定向忽略 |
| 底层实现 | PHPStan\Rules\InternalTag\RestrictedInternalClassNameUsageExtension在"实例化"位置上按符号类型自动生成 |
new.internalEnum虽然"罕见"(unlikely: true),却精确地揭示了 PHPStan 错误标识符体系的设计精髓:同一套内部 API 边界检查,按符号类型与使用位置拆分为细粒度、可忽略、可检索的标识符。理解它,就等于理解了@internal契约在现代 PHP 静态分析中的落地方式,也能让你在依赖第三方包时养成"只走公共 API"的健壮习惯。
- 开发工具
- 代码质量
- 静态分析
【免费下载链接】phpstan
PHP Static Analysis Tool - discover bugs in your code without running it!
相关推荐
PHPStan 错误标识符深度解析:enum.implementsInternalEnum —— 枚举实现内部枚举(@internal)的检测与修复
PHPStan 错误标识符深度解析:enum.implementsInternalEnum —— 枚举实现内部枚举(@internal)的检测与修复 导读 en
开发工具代码质量静态分析PHPStan 错误标识符 `assert.internalEnum` 详解:`@phpstan-assert` 引用 `@internal` 枚举的检测与修复
PHPStan 错误标识符 assert.internalEnum 详解: @phpstan assert 引用 @internal 枚举的检测与修复 asse
开发工具代码质量静态分析PHPStan 错误标识符 enum.implementsDeprecatedEnum:枚举实现已弃用枚举的检测原理与修复方案
PHPStan 错误标识符 enum.implementsDeprecatedEnum:枚举实现已弃用枚举的检测原理与修复方案 导读 enum.implemen
开发工具代码质量静态分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考