good-php / reflection
具有类型系统、泛型支持和缓存的反射API
Requires
- php: >=8.2
- nikic/php-parser: ^5.0
- phpstan/phpdoc-parser: ^1.26
- psr/simple-cache: ^3.0
- symfony/cache: ^7.0
- symfony/var-exporter: ^7.0
- tenantcloud/php-standard: ^2.0
- webmozart/assert: ^1.11
Requires (Dev)
- dms/phpunit-arraysubset-asserts: ^0.5.0
- jiripudil/phpstan-sealed-classes: ^1.1
- pestphp/pest: ^2.8
- phake/phake: ^4.2
- php-cs-fixer/shim: ~3.21.3
- phpbench/phpbench: ^1.2
- phpstan/phpstan: ~1.10.21
- phpstan/phpstan-mockery: ^1.1
- phpstan/phpstan-phpunit: ^1.3
- phpstan/phpstan-webmozart-assert: ^1.2
- roave/better-reflection: ^6.36
- tenantcloud/php-cs-fixer-rule-sets: ~3.1.0
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
实例)之间有一个清晰的分离。