popsul/php-token-reflection

使用标记化源代码模拟 PHP 内部反射的库。

1.5.8 2016-07-12 09:59 UTC

This package is auto-updated.

Last update: 2024-09-11 14:53:36 UTC


README

Build Status

简而言之,这个库使用标记化的 PHP 源代码来模拟 PHP 反射模型。

基本概念是,任何反射都可以处理描述反射元素的特定部分的标记数组。它还能够找出是否存在任何子元素(例如,类反射可以在源中找到方法定义),创建它们的反射并将适当的标记数组部分传递给它们。

这个概念使我们能够保持解析器代码相对简单且易于维护。我们能够在单次遍历中创建所有反射。这对库的性能至关重要。

所有反射实例都保存在一个 TokenReflection\Broker 实例中,并且所有反射都知道创建了它们的代理。这非常重要,因为例如类反射持有所有其内部实例化的常量、方法和属性反射,但它对父类或实现的接口一无所知。它只知道它们的完全限定名。所以当你调用 $reflectionClass->getParentClass(); 时,类反射会向代理请求一个类反射并根据其名称返回它。

当定义了父类但它未被处理(换句话说,你请求代理一个它不知道的类)时,它仍然会返回一个反射!是的,我们确实有不存在类的反射!太酷了!

存在文件(*)、文件-命名空间(*)、命名空间、类、函数/方法、常量、属性和参数的反射。你通常不会接触到标记为*的反射,但它们在内部使用。

ReflectionFile 是我们的反射树中的顶级结构。它获取整个标记化源代码并尝试在其中找到命名空间。如果找到,它创建 ReflectionFileNamespace 实例并将适当的标记数组部分传递给它们。如果没有找到,它创建一个单独的伪命名空间(称为无命名空间)并将整个标记化源代码传递给它。

ReflectionFileNamespace 从文件中获取命名空间定义,找出其名称、其他别名命名空间并尝试找到任何定义的常量、函数和类。如果找到,它创建它们的反射并将适当的标记数组部分传递给它们。

ReflectionNamespace 与其名称相似但意义相当不同。它是每个命名空间的唯一结构,并在此命名空间内保存所有常量、函数和类。实际上,它是一个简单的容器。它也不是由任何父反射直接创建的,而是由代理创建的。

为什么我们需要两个单独的类?因为命名空间可以分散到许多文件中,并且每个文件可以有自己的命名空间别名。在解析父类/接口名称时必须考虑这些别名。这意味着为每个文件中的每个命名空间创建一个 ReflectionFileNamespace,并解析其内容,解析所有类的完全限定名及其父类和接口。之后,代理将所有相同的命名空间的 ReflectionFileNamespace 实例合并为单个 ReflectionNamespace 实例。

ReflectionClassReflectionFunctionReflectionMethodReflectionParameterReflectionProperty 与它们的内部反射同名功能工作方式相同。

ReflectionConstants 是我们对反射模型的补充。它所能做的并不多 - 它可以返回其名称、值(我们稍后会谈到值)以及它的定义方式。

(几乎)所有反射类都共享一个公共基类,该基类定义了一些公共功能和方法。这意味着我们的反射模型比内部模型更加统一。

除了上述的标记源反射外,还有内部反射的后代,它们实现了我们的附加功能(它们都使用相同的接口)。它们代表 PHP 的内部类、函数等。因此,当你向代理请求内部类时,它返回一个封装了内部反射功能并添加了我们特性的 TokenReflection\Php\ReflectionClass 实例。还有一个 TokenReflection\Php\ReflectionConstant 类,在内部反射模型中没有父类。

备注

从一开始,我们就尽量与内部反射保持兼容(包括返回接口列表的相同 - 很奇怪 - 顺序等)。然而,有些情况下这是不可能的(例如,我们更倾向于一致性而不是与内部反射的兼容性,并且不会将 此错误 引入库中)。

我们在处理常量值以及属性和参数默认值的方式上受到限制。当作为常量定义时,我们将尽最大努力解决其值(在解析和内部常量中)并使用它。这最终是通过组合 var_export()eval() 来实现的。是的,这很糟糕,但这是没有更好的方法。此外,引用的常量可能不存在。在这种情况下,它被替换为 ~~NOT RESOLVED~~ 字符串。

不支持运行时常量。

当库遇到重复的类、函数或常量名称时,它将之前创建的反射转换为“无效反射”实例。这意味着解析器无法区分此类类,也无法构建正确的类树,例如。它会抛出异常。当你捕获这个异常并继续与代理实例一起工作时,重复的类、函数或常量将只有一个反射,并且它将分别是 Invalid\ReflectionClassInvalid\ReflectionFunctionInvalid\ReflectionConstant 的实例。

用法

要能够使用反射,你必须让库先解析源代码。这就是 TokenReflection\Broker 所做的。它遍历给定的目录,标记 PHP 源代码并缓存反射对象。此外,你无法直接实例化一个反射类。你必须请求代理的反射。一旦你有了反射实例,一切都将按预期工作 :)

<?php
namespace TokenReflection;

$broker = new Broker(new Broker\Backend\Memory());
$broker->processDirectory('~/lib/Zend_Framework');

$class = $broker->getClass('Zend_Version'); // returns a TokenReflection\ReflectionClass instance
$class = $broker->getClass('Exception');    // returns a TokenReflection\Php\ReflectionClass instance
$class = $broker->getClass('Nonexistent');  // returns a TokenReflection\Dummy\ReflectionClass instance

$function = $broker->getFunction(...);
$constant = $broker->getConstant(...);

要求

该库需要 PHP 5.3,并且必须启用 tokenizer 扩展。如果你想要处理 PHAR 归档,还需要启用相应的 扩展

当前状态

当前版本应该支持 PHP 内部反射的大多数功能,并添加了许多其他功能。

每个版本都使用我们的测试包(几个 PHP 框架和其他库)进行测试,并且其在 5.3 和 5.4 分支的所有 PHP 版本以及实际主分支上的兼容性都经过测试。