exteon/chaining-class-resolver

为PHP模块插件提供类链编织的类加载器框架

2.1.1 2022-04-17 14:38 UTC

This package is auto-updated.

Last update: 2024-09-16 02:15:56 UTC


README

mapping-class-loader一起,这个库提供了一个框架插件开发和加载的平台。通过使用“链式加载”的概念,它提供了一个位于混入和多重继承之间的实现平台。

摘要

一个提供插件功能的平台常见的问题是这样的:我们有多个针对相同代码库(相同的合约)开发的插件,但是它们互相之间并不知情(它们必须像黑盒一样对待其他插件)。

由于插件的本质,它们与它们扩展或定制的基代码紧密耦合。同时,加载的插件的效果需要是可叠加的。多个与代码库耦合的插件必须组合在一起才能一起工作。

我们发现一些常见的解决方案存在不足之处

  • 基于装饰器的方法提供的耦合性不足,插件结构是一个“洋葱层”,其中外层的输出无法馈送到内层的输入
  • 观察者模式插件系统(钩子、事件驱动、通用调用混入等)实现、维护和调试成本很高

大多数高效的插件模式依赖于混入,类似于多重继承。虽然PHP的Traits是一个了不起的成就,但它不足以实现插件所需的混入模式,因为它们缺乏静态身份(静态属性被复制到实现它们的类中)。

我们的解决方案提供了一种以链式(分层)动态结构加载插件的方法(类似于装饰器),但使用源编织来修改类层次结构,使得结果继承模型完全耦合。

以下插图将提供对链式过程的更直观解释。让我们从如何从代码库开始,插件开发者通过扩展现有类来添加特殊化,目的是使它们的特殊化替换初始实现

所以在上面的图像中,Plugin1Plugin2都扩展了基代码,而Plugin3基于Plugin2

问题是,我们如何重新组合这些结构,以便在应用程序中只使用一个A类和一个B类,就可以使用所有3个插件?

这时,chaining-class-resolver就出现了

通过chaining-class-loader,代码以模块的形式出现,所有基代码和插件都是将被线性化的模块。这是在类加载时间通过更改继承来完成的。在第二个图中,你会注意到\Plugin2\A现在继承自\Plugin1\A,而不是像原始源(第一个图)中那样从\Code\Base\A继承。

所有这些线性化的类随后被投影到一个目标命名空间中,以便应用程序可以使用,例如\Target\A,而对生成它的链相对无知。

这种类编织是在运行时完成的,因此这个过程对应用程序开发者来说是透明的,只需要配置模块。因此,插件可以从任何来源(如composer)引入,而且不需要对应用程序进行任何特定于应用的定制,chaining-class-resolver将完成所有魔术。

用法

您可以在上面的图中找到示例在示例目录中实现。

加载器设置部分在setup.inc.php中。

要运行示例,请运行 app.php

设置链式类加载的步骤如下

创建模块

我们将模块组织在文件夹中,具有 PSR-4 类结构

每个模块定义或扩展类 A 和/或 B,如上图所示

你可以使用任何目录结构,只要你有多个模块目录,每个目录都包含一个 PSR-4 命名空间。

链式解析器

我们获取一个解析器实例

use Exteon\Loader\ChainingClassResolver\ChainingClassResolver;
use Exteon\Loader\ChainingClassResolver\Module;
use Exteon\Loader\ChainingClassResolver\ClassFileResolver\PSR4ClassFileResolver;


$chainingClassResolver = new ChainingClassResolver(
    [
        new Module(
            'Code base',
            [new PSR4ClassFileResolver(__DIR__ . '/base', 'Code\\Base')]
        ),
        new Module(
            'Plugin 1',
            [new PSR4ClassFileResolver(__DIR__ . '/plugins/plugin1', 'Plugin1')]
        ),
        new Module(
            'Plugin 2',
            [new PSR4ClassFileResolver(__DIR__ . '/plugins/plugin2', 'Plugin2')]
        ),
        new Module(
            'Plugin 3',
            [new PSR4ClassFileResolver(__DIR__ . '/plugins/plugin3', 'Plugin3')]
        )
    ],
    'Target'
);

构造函数参数 $targetNs 定义了类链将被编织进的目标命名空间,在我们的例子中,类将在 \Target 下链式化。

模块将按照它们被发送给构造函数的顺序链式化;这意味着,在上面的例子中,Plugin 1 将扩展/覆盖 Code base 中的类,Plugin 3 将覆盖所有类等。

设置加载器

我们将使用 exteon/mapping-class-loader 来加载链式文件

use Exteon\Loader\MappingClassLoader\MappingClassLoader;
use Exteon\Loader\MappingClassLoader\StreamWrapLoader;

$loader = new MappingClassLoader(
    [],
    [
        $chainingClassResolver
    ],
    [],
    new StreamWrapLoader([
        'enableMapping' => true
    ])
);
$loader->register();

有关使用 MappingClassLoader 的更多详细信息,请参阅 exteon/mapping-class-loader 文档

使用链式类

我们现在可以使用链式类。所有类都定义或覆盖了 whoami() 方法,并添加了结果。

    use Target\A;
    use Target\B;

    $a = new A();
    $b = new B();

    var_dump($a->whoami());
    var_dump($b->whoami());

上面的代码将产生以下结果

array(3) {
  [0] =>
  string(11) "Code\Base\A"
  [1] =>
  string(9) "Plugin1\A"
  [2] =>
  string(9) "Plugin2\A"
}
array(2) {
  [0] =>
  string(9) "Plugin2\B"
  [1] =>
  string(9) "Plugin3\B"
}

所以现在类就像这张图中所示的那样链式化。

类提示文件

如果你在智能 IDE 中打开 app.php,则 Target 命名空间中的类将无法解析,并且你将无法为它们提供自动完成。这是因为 Target\ATarget\B 还未在代码的任何地方定义。

为了解决这个问题,我们需要创建包含 Target 类存根的类提示文件。我们可以通过在 example 目录中运行 create-hints.php 来实现这一点。

使用相同的 setup.inc.php,这个工具会运行

$loader->dumpHintClasses(__DIR__.'/dev/hints');

这个工具会在你指定的目录中生成类提示文件到 dev/hints。这些是具有 PSR-4 结构的 PHP 类文件,它们将在你指定的目录中生成。现在只需将提示目录添加到你的 IDE 的源目录中,现在,你的 Target\A 类将被定义为以下内容

namespace Target {
    /**
     * @extends \Code\Base\A
     * @extends \Plugin1\A
     */
    class A extends \Plugin2\A {}
}

注意

每次你向项目中添加类或更改类层次结构、重新配置模块等时,都应该运行 dumpHintClasses 工具来重新生成提示文件。

注意

提示类只包含一个扩展,即 Plugin2\A,它在您的源文件中扩展了 Code\Base\A。因此,如果您现在在 Plugin1\A 上添加一个 someMethod() 方法,在静态分析 Target\A 时,该方法将无法识别,除非您的IDE可以读取多个 @extends 注解。

在未来的版本中,我们可能会通过始终使用特性来扩展类来解决这个问题。(参见特性链

然而,插件的主要功能是修改(即覆盖)现有类的方法。因此,我们建议您在考虑扩展现有类的契约时,考虑使用单独的特性来实现,或者使用对象组合将添加的功能保持到不同的类中。

其他功能

调试

尽管 chaining-class-resolver 通过修改(交织)源代码来实现其功能,但可以通过使用 exteon/mapping-class-loader 的映射功能轻松地进行步骤调试。在调试代码时,您将像往常一样在原始类文件上执行步骤。您需要做的唯一事情,就像上面的代码示例一样,是将 'enableMapping' => true 设置到 StreamingWrapLoader 的配置中。

有关映射功能的更多信息,请参阅exteon/mapping-class-loader文档

缓存

类交织过程可能是一个很大的开销;exteon/mapping-class-loader 为交织的类文件提供了缓存。要启用缓存,您需要将 enableCachingcacheDir 配置选项设置到 MappingClassLoader 构造函数中。有关缓存的更多详细信息,请参阅exteon/mapping-class-loader文档

对于开发,每次更改源类时,都需要清除和重新生成缓存;有一个基于 inotify 的更改监视器的开发正在进行中,但直到那时,如果您在开发环境中使用缓存,您应该创建自己的工具。

特性链

在PHP中,实现相同方法的特性不能添加到同一个类中(《参见这里》)。虽然这是复制粘贴特性实现的产物,但它也是特性可重用性和表现力的一个缺点。由于 parent::static:: 都支持特性,而PHP不支持重载,因此多个特性可以覆盖相同的基方法。

chaining-class-resolver 通过线性化(链式)类 uses 子句中的特性列表来支持这一点,创建中间类,每个类使用一个特性。因此,特性的优先级是它们在 use 子句中列出的顺序。

例如,这是使用 chaining-class-resolver 的可能性(请注意,根据PSR-4标准,不同的类需要在不同的文件中实现)

class A {
    public function whoami(){
        return ['A'];
    }
}
trait T1 {
    public function whoami(){
        return array_merge(parent::whoami(),['T1']);
    }
}
trait T2 {
    public function whoami(){
        return array_merge(parent::whoami(),['T2']);
    }
}
class B extends A {
    use T1,T2;
    public function whoami(){
        return array_merge(parent::whoami(),['B']);
    }
    
}
var_dump((new B())->whoami());

这将列出 ['A','T1','T2','B'] 作为类继承。

请参阅多重继承注意事项,以了解更多关于影响的讨论。

多重继承注意事项

如果链式类或多个特性定义了相同名称的新方法,就会出现臭名昭著的菱形问题chaining-class-resolver 对此没有特殊处理,但 PHP 也没有(没有语法可以明确表示重写)。因此,只要按照 PHP 规则,第二个方法的签名与第一个方法兼容,就可以假设它是重写的。

将来我们可能会实现更严格的机制来检测和解决菱形问题。

链式反射

您可以使用 ChainedClassMeta 在运行时查找有关链式类的信息。

ChainedClassMeta::get('Some\Class')->isChained()

返回是否有名为指定类的链。

ChainedClassMeta::get('Some\Class')->getChainParent()

返回 Some\Class 的链父类作为一个 ChainedClassMeta 对象,如果没有则返回 null。使用 ->getClassName() 获取这个父类的类名。

ChainedClassMeta::get('Some\Class')->getChainedClass()

返回 Some\Class 的链目标类(即目标命名空间中的链的末端)。

ChainedClassMeta::get('Some\Class')->getModuleName()

返回定义 Some\Class 的模块的名称。

ChainedClassMeta::get('Some\Class')->getChainTraits()

返回在 Some\Class 的链中由所有链式类添加的特性。(但不包括继承的特性)。

ChainedClassMeta::get('Some\Class')->getChainInterfaces()

返回在 Some\Class 的链中由所有链式类添加的接口。(但不包括继承的接口)。