good-php/reflection

具有类型系统、泛型支持和缓存的反射API

v1.0.2 2024-05-15 14:21 UTC

This package is auto-updated.

Last update: 2024-09-15 15:05:54 UTC


README

考虑到静态分析器中存在但语言尚未实现的功能的反射。

为什么?

PHP 处于一种状态,其中一些非常重要的功能仅存在于用户空间(例如 PHPStan),如泛型、元组类型、条件类型、类型别名等。当你需要反射时,通常还需要它与所有这些用户空间功能一起工作。显然,PHP 内置的反射无法做到这一点。

尽管存在自定义的反射库(如 roave/better-reflection),但没有一个能够解析或理解现代PHP中基于PHPDoc的类型和功能。此外,它们中的一些性能不足以在运行时使用(例如在工具之外)。

这个库旨在既适用于运行时(使用一些高效的缓存和延迟解析)又覆盖内置反射和PHPStan功能的全套。

理想的情况是PHPStan将其自己的反射提取到包中,但这已经被项目作者 @ondrejmirtes 拒绝,这是可以理解的。我尝试通过重新定位PHPStan代码的一部分来提取它,但它已被证明是复杂、不可靠的,最重要的是,执行速度慢。

因此,该项目试图填补原生反射的空白,并在过程中使API现代化。大多数API都是通过接口定义的,您可以扩展它来实现需要的功能。

它做了什么

以下是一些支持(或欢迎)的功能:

  • 类、特性、接口和枚举的反射
  • 泛型(反射和替换)
  • 元组类型
  • 匿名类
  • 极快的缓存
  • 支持 strict_types 配置
  • 条件类型
  • 类型别名
  • 扩展反射(spl、zip、ds、dom等)
  • 函数的模板类型推理

它有多可靠?

它处于alpha版本,因此实际上并不稳定。然而,我们采取了一些措施来最大限度地减少问题。例如,整个代码库都使用 max 级别的PHPStan,几乎没有任何被忽略的错误;它覆盖了90%的覆盖率,包括简单的集成和单元测试。

话虽如此,由于PHPDoc的动态性和它提供的复杂类型系统,预计会遇到错误和问题。

它有多快?

尽可能多的反射信息被缓存到磁盘上的 .php 文件中。当你请求类型的反射时,它只会执行 require cache_file_for_something.php,将它们包装在反射类中,并(可选地)替换模板类型。对于以前已经反射过的类型,不会进行任何重解析。

因此,它在初始缓存之后非常快(纳秒级)。原生PHP反射和Roave/BetterReflection通常更快,但请注意,这也必须解析AST和DocBlocks以提取泛型和类型。然而,我相信如果启用缓存,它足够快以至于可以在生产中使用。

以下是一个参考基准,在一个配备OpCache的M1 MacBook Pro上执行

\Tests\Benchmark\ThisReflectionBench

    benchWarmWithMemoryCache # only name....I49 - Mo0.011ms (±15.06%) [3.856mb / 4.779mb]
    benchWarmWithMemoryCache # everything...I49 - Mo0.137ms (±5.25%) [9.970mb / 9.988mb]
    benchWarmWithFileCache # only name......I49 - Mo0.047ms (±11.98%) [6.917mb / 6.958mb]
    benchWarmWithFileCache # everything.....I49 - Mo0.172ms (±4.64%) [13.097mb / 13.114mb]
    benchCold # only name...................I199 - Mo2.384ms (±12.80%) [2.143mb / 4.779mb]
    benchCold # everything..................I199 - Mo2.506ms (±18.67%) [2.276mb / 4.779mb]
    benchColdIncludingInitializationAndAuto.I199 - Mo74.279ms (±18.78%) [2.092mb / 4.779mb]
    benchColdIncludingInitializationAndAuto.I199 - Mo72.901ms (±4.37%) [2.188mb / 4.779mb]

\Tests\Benchmark\BetterReflectionBench

    benchWarmWithMemoryCache # only name....I49 - Mo0.005ms (±8.26%) [3.085mb / 4.779mb]
    benchWarmWithMemoryCache # everything...I49 - Mo0.016ms (±5.70%) [3.093mb / 4.779mb]
    benchCold # only name...................I199 - Mo1.693ms (±6.13%) [3.104mb / 4.779mb]
    benchCold # everything..................I199 - Mo2.299ms (±14.67%) [3.116mb / 4.779mb]
    benchColdIncludingInitializationAndAuto.I199 - Mo59.184ms (±5.79%) [3.084mb / 4.779mb]
    benchColdIncludingInitializationAndAuto.I199 - Mo63.590ms (±18.52%) [3.092mb / 4.779mb]

\Tests\Benchmark\NativeReflectionBench

    benchWarm # only name...................I49 - Mo0.001ms (±9.11%) [517.504kb / 4.778mb]
    benchWarm # everything..................I49 - Mo0.004ms (±6.08%) [517.568kb / 4.778mb]
    benchCold # only name...................I199 - Mo0.009ms (±55.16%) [518.488kb / 4.779mb]
    benchCold # everything..................I199 - Mo0.022ms (±23.29%) [518.488kb / 4.779mb]

它是如何工作的

不幸的是,这不仅仅只是使用原生的反射和解析一些PHPDoc那么简单。尽管PHP的反射功能非常强大,但它并不能提供所有必要的工具来高效地解析PHPDoc。具体来说,存在一些限制

  • 无法访问use语句(导入),这在映射PHPDoc中的“导入”类时是必需的
  • 无法可靠地访问“立即”(即在结构内声明的)接口、特性使用、常量、属性和方法——所有这些对于嵌套泛型类型都是必需的
  • 无法访问特性使用的docblocks、别名或优先级——所有这些都是泛型所需的

由于这些限制,我们不得不依赖原生反射和AST解析的组合。一般原则是这样的:尽可能多地从原生反射中收集信息(因为它最快、最可靠),以及一些从使用nikic/php-parser解析PHP文件中获得的信息,然后将它们结合起来,生成一个“定义”。

“定义”是我们用来表示只包含特定结构(例如泛型类型参数、属性、方法等)反射信息的数据类,但仅限于该特定结构(不包括继承的)。这样做有几个原因,但主要原因是泛型:这样就可以通过递归地将它们映射到每个超类型,在整个继承树中轻松替换它们。

还有定义提供者,它们确实做了它们听起来应该做的事情。您可以按需链接任意数量的它们,并通过任何来源提供/覆盖反射数据。默认情况下,它使用原生反射和phpstan/phpdoc-parser来收集所有信息,但您可以按照需要适配任何其他反射库。

另一方面,反射是面向用户的API。它不是收集反射数据,而是简单地使用一组API“呈现”由定义提供的定义

  • 面向最终用户
  • 功能齐全的API
  • 由于依赖(例如Reflector)而难以序列化/缓存

这种方法允许在可缓存的数据结构和反射本身(它依赖于Reflector实例)之间有一个清晰的分离。