exteon / mapping-class-loader
PHP类加载器,具有动态修改和映射类文件、缓存和静态初始化的能力
README
摘要
在加载类时,有时在加载之前需要进行源代码操作,主要是在AOP上下文中编织类或为其他目的创建类代理。最常见的操作是在加载类之前重写类的继承关系。
这种方法的缺点是,在与Xdebug配合使用时难以维护调试功能,以便修改后的解释源文件可以与原始源文件进行逐步调试。
MappingClassLoader
是一个高级类加载器框架,提供以下功能
- 类解析器,能够为加载的类提供修改后的源代码
- 修改后的源文件的缓存
- 通过将修改后的源映射到原始源文件,使用流包装器启用调试
- 静态类初始化器,允许实现静态构造函数或静态依赖注入
要求
- PHP 7.2
用法
使用composer
安装
composer require exteon/mapping-class-loader
类解析器
MappingClassLoader
具有模块化设计,允许实现多个解析器;解析器是实现类加载的第一步。
解析器实现了ClassResolver
接口,以将请求的类名解析为一个或多个LoadAction
。一个LoadAction
是标识要加载的源代码,可以是以下三种类型之一
- 纯代码:仅提供包含要评估的代码的
source
属性 - 源文件:仅提供
file
属性 - 可映射的修改后的源文件:提供标识原始源文件的
file
属性和包含要评估的修改后源代码的source
属性
示例
A.php
<?php class A { public function doSomething(string $what): void { // ... } }
Resolver.php
<?php use Exteon\Loader\MappingClassLoader\ClassResolver; use Exteon\Loader\MappingClassLoader\Data\LoadAction; class Resolver implements ClassResolver { function resolveClass(string $class) : array{ $loadActions = []; $sourceFile = $class . '.php'; $sourceCode = file_get_contents($sourceFile); // Rename i.e. A to A_proxied $proxiedClass = $class.'_proxied'; $modifiedCode = preg_replace( '(/class\\s+)('.preg_quote($class,'/').')(\\s)/', '$1'.$proxiedClass.'$3', $sourceCode ); $loadActions[] = new LoadAction( $proxiedClass, $sourceFile, $modifiedCode ); $proxyCode = ' <?php class ' . $class . ' extends ' . $proxiedClass. ' { // ... specific generated proxy code ... } '; $loadActions[] = new LoadAction( $class, null, $proxyCode ); } }
main.php
<?php use Exteon\Loader\MappingClassLoader\MappingClassLoader; use Exteon\Loader\MappingClassLoader\StreamWrapLoader; $loader = new MappingClassLoader( [], [new Resolver()], null, new StreamWrapLoader([]) ); $loader->register(); (new A())->doSomething('anything');
如上例所示,Resolver
返回2个LoadAction
,一个用于修改后的代理类,另一个用于代理类。
注意由于这个原因,以及缓存(见下文),解析器返回的每个LoadAction
都必须指定LoadAction
应用的完全限定类名(第一个构造函数参数)。
LoadAction
LoadAction
是解析器返回的不可变对象,指定了搜索的类要加载的内容。构造函数的格式如下
public function __construct( string $class, ?string $file, ?string $source = null, ?string $hintCode = null );
字段具有以下含义
-
$class
必须始终提供,即使只是搜索类的回声(如果生成单个LoadAction
) -
$file
是要加载的类的源文件或基于修改后的源代码的文件,如果指定了$source
。当同时指定了$file
和$source
时,修改后的代码将映射到源文件以进行调试目的。$file
可以是null;在这种情况下,必须指定$source
,这种设置表示我们正在加载纯生成的代码。 -
$source
是要加载的生成或修改后的源代码。如果此为null,则必须指定$file
,其含义是$file
将被加载而无需进一步处理(换句话说,是传统的加载器行为)。 -
$hintCode
: 对于生成的代码,有时需要为开发工具(即开发者的GUI或静态分析器)生成一些提示类。该属性提供了这些代码,可以使用MappingClassLoader::dumpHintClasses()
将其输出到目录。
IClassScanner
解析器可以实现 IClassScanner
接口(不是必需的,但推荐),以启用缓存预生成和提示文件输出等功能。
该接口有一个方法,scanClasses()
,需要返回一个由解析器可解析的类名数组。
缓存
由于代码修改/生成可能很昂贵,MappingClassLoader
为源文件提供了缓存机制。要启用缓存机制,需要将 enableCaching
和 cacheDir
参数传递给 MappingClassLoader
构造函数,如下所示:
use Exteon\Loader\MappingClassLoader\MappingClassLoader; use Exteon\Loader\MappingClassLoader\StreamWrapLoader; $loader = new MappingClassLoader( [ 'enableCaching' => true, 'cacheDir' => '/tmp/caching' ], [new Resolver()], null, new StreamWrapLoader([]) ); $loader->register();
cacheDir
必须指向一个脚本可以创建或写入的目录。指定了 source
属性的 LoadAction
的源代码存储在此目录下的文件中,遵循 PSR-4 结构。
要清除缓存,可以使用 MappingClassLoader::clearCache()
或 MappingClassLoader::clearSpecificClasses()
中的任何一种方法。
预生成缓存(预加载)
使用 MappingClassLoader::primeCache()
,可以生成缓存。只有实现了 IClassScanner
接口的解析器和通过解析器的 scanClasses()
方法返回的类才会生成缓存。
调试映射
为了启用使用 XDebug 对修改后的文件进行逐步调试,我们使用流包装来包含修改后的源代码。流包装映射到原始脚本的路径。
注意 这只有在您对原始源文件所做的任何修改都保留行号且源代码在很大程度上相似的情况下才有意义。无法对修改后的文件进行完整的源文件映射,这是不可能的,只映射文件名。在逐步调试时,您将实际上看到原始源文件,任何修改都将被隐藏;因此,这仅适用于您在解析器中进行小修改的情况,例如修改类层次结构。
要启用此功能,必须将 enableMapping
配置参数传递给 StreamWrapLoader
构造函数,如下所示:
use Exteon\Loader\MappingClassLoader\MappingClassLoader; use Exteon\Loader\MappingClassLoader\StreamWrapLoader; $loader = new MappingClassLoader( [], [new Resolver()], null, new StreamWrapLoader([ 'enableMapping' => true ]) ); $loader->register();
静态初始化器
为了提供静态构造函数或静态依赖注入行为。
(在这里,“静态”是在静态类属性和方法的环境中使用的;类在加载时动态初始化,不是在不可变源代码的静态意义上)
MappingClassLoader
的构造函数有一个 $initializers
参数,其中可以加载类初始化器数组。类初始化器必须实现 IStaticInitializer
接口,实现 init($class)
方法。此方法将在类加载后调用,您可以在那里对类执行任何初始化操作。
包含了一个简单的静态初始化器,即 ClassInitMethodInitializer
。此初始化器在加载的任何类上调用 classInit()
静态方法,前提是它实现了 IClassMethodInitializable
接口。因此,静态 classInit
方法将像类的静态无参构造函数一样起作用。
示例
A.php
<?php use Exteon\Loader\MappingClassLoader\StaticInitializer\ClassInitMethodInitializable; class A implements ClassInitMethodInitializable { protected static $someClassStaticProperty; public static function classInit() : void{ self::$someClassStaticProperty = 'I am initialized now'; } function getStaticProperty(){ return self::$someClassStaticProperty; } }
main.php
<?php use Exteon\Loader\MappingClassLoader\MappingClassLoader; use Exteon\Loader\MappingClassLoader\StaticInitializer\ClassInitMethodInitializer;use Exteon\Loader\MappingClassLoader\StreamWrapLoader; $loader = new MappingClassLoader( [], [new Resolver()], new ClassInitMethodInitializer(), new StreamWrapLoader([]) ); $loader->register(); var_dump((new A())->getStaticProperty());
注意 将不会覆盖静态 classInit()
方法,因为这将导致语义上不一致。这意味着,如果您有:
class B extends A { protected static $someOtherStaticProperty; public static function classInit() : void{ self::$someClassStaticProperty = 'Other property is initialized'; } }
那么 A::classInit()
和 B::classInit()
都将调用(按此顺序,因为 A
总是必须在 B
之前加载)。因此,不要在 B::classInit()
中调用 parent::classInit()
。父方法已经调用过了。
这也意味着,例如,当加载 B
时,您不能避免调用 A::classInit()
。这是因为,一方面,多个类可能继承自 A
,这些类都将覆盖相同的静态行为;另一方面,继承自 A
的类可能永远不会被加载,但 A
仍然期望被初始化。因此,在这里覆盖行为在语义上没有意义。
通过实现自己的 IStaticInitializer
,您可以引入更高级的功能,例如静态依赖注入。
提示文件
可以为任何 LoadAction
设置 hintCode
属性,以定义非运行时代码,但为辅助工具服务。特别是对于生成的类,此代码可以用作提供有关类组成的类提示。
可以使用以下方式将类提示输出到目录
MappingClassLoader::dumpHintClasses($dir);
每个类的提示代码都将被输出到 $dir 中的单独文件,使用 PSR-4 结构。
为了使此功能正常工作,提供提示代码的解析器必须实现 ClassResolver
接口,以便加载器知道必须为哪些类生成提示文件。
更多示例
要了解如何使用此类加载器实现一个用于提供类链的 PHP 模块插件的织入类加载器框架,您可以查看 exteon/chaining-class-resolver。
您可以在那里看到一个实现高级自定义解析器的示例,该解析器使用了大多数 mapping-class-loader
的功能。