PHPStan 错误标识符 new.internalEnum 全面解析:实例化 @internal 枚举的检测原理与修复方案
2026/9/24 10:09:18 网站建设 项目流程
  • 开发工具
  • 代码质量
  • 静态分析

【免费下载链接】phpstan

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

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

导读

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.internalClassnew.internalInterfacenew.internalTrait构成new.*系列中针对@internal符号的同类检测族。

从 errorsIdentifiers.json 的注册信息可以看出,该标识符由规则类PHPStan\Rules\InternalTag\RestrictedInternalClassNameUsageExtension产生,其源码位置对应 phpstan-src 2.3.x 分支的src/Rules/InternalTag/RestrictedInternalClassNameUsageExtension.php。也就是说,new.internalEnum并非独立手写的规则,而是"受限内部类名使用"扩展在"实例化"这一具体位置上的产物——同一个扩展还会在其它使用位置上生成propertyTag.internalClassclass.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,包含RedisMemcached两个 case;
  • 使用方App命名空间中用new关键字试图实例化该枚举,这一行即触发new.internalEnum报告;
  • 跨包/跨命名空间:使用位置处于枚举根命名空间之外,这是"内部 API 越界访问"判断成立的前提。

运行 PHPStan 后,对应错误消息为:Instantiation of internal enum Vendor\CacheDriver.

Frontmatter 元信息解读

原文档 frontmatter 中ignorable: trueunlikely: true两个字段值得注意:

  • ignorable: true表示该标识符默认允许被忽略,可在phpstan.neonignoreErrors中通过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.internalEnumAPI 边界层面:@internal枚举被包外实例化枚举被new且该枚举标记了@internal

同样的关系也存在于new.internalTraitnew.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?"部分的语义要点如下:

  1. 内部枚举正被用于实例化上下文:使用方从枚举的根命名空间之外引用它;
  2. @internal意味着非公共 API:被标记的枚举不属于定义该枚举的包(package)的公开 API,只应在包内部使用;
  3. 稳定性风险:内部实现细节可能在未来的任何版本中不经通知地变更或移除,不遵循语义化版本控制(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.internalEnumRestrictedInternalClassNameUsageExtension在"实例化"这一ClassNameUsageLocation上针对枚举类型(getClassTypeDescription()返回enum)自动生成的标识符,与new.internalClassnew.internalInterfacenew.internalTrait共享同一套底层报告机制,只是按符号类型与使用位置区分为不同 identifier。这正是"同一规则、多标识符"设计在 PHPStan 错误标识符体系中的体现。

六、同类标识符一览与扩展机制延伸

@internal检测并不只覆盖"实例化"位置。从 errorsIdentifiers.json 可以检索到同一 InternalTag 机制派生出的、针对枚举的兄弟标识符,例如:

  • assert.internalEnum:在断言上下文中引用内部枚举;
  • attribute.internalEnum:在属性(Attribute)上下文中引用内部枚举;
  • catch.internalEnum:在 catch 子句中引用内部枚举(配合new.internalEnum构成同一符号多位置覆盖);
  • classConstant.internalEnuminstanceof.internalEnummethod.internalEnummethodTag.internalEnummixin.internalEnum等。

这些标识符共同说明:PHPStan 对@internal的检查是按使用位置全覆盖的,而不是只在new时检查一次。如果你在代码库中看到形如xxx.internalEnum的其它标识符,其含义与修复思路同本文完全一致——从包外部访问了内部枚举,应改用公共 API。

对于想要自定义"内部使用限制"的扩展开发者,restricted-usage-extensions.md(标识为 Available in PHPStan 2.1.13)还提供了通用扩展接口:RestrictedMethodUsageExtensionRestrictedPropertyUsageExtensionRestrictedClassConstantUsageExtensionRestrictedFunctionUsageExtensionRestrictedClassNameUsageExtension,可通过注册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!

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

相关推荐

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

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

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

立即咨询