earc/di

eArc - 显式架构框架 - 依赖注入组件

3.1 2021-04-08 20:41 UTC

This package is auto-updated.

Last update: 2024-09-09 04:22:52 UTC


README

eArc 库的独立轻量级依赖注入组件。

如果您需要解耦组件、限制注入访问或希望使应用程序组件显式化,请使用基于 earc/di 构建的 earc/component-di 库。

目录

优缺点

优点

  • 无容器 - 实例实时生成
  • 无配置开销 - 依赖信息全部包含在类中,不包含在依赖解析过程中。直到类自动加载时才重要。
  • 无加载开销 - 依赖在使用时解析。
  • 无测试限制 - 模拟不仅限于构造函数参数
  • → 您可以自由地将依赖注入到它们演变的任何地方
  • → 因此,不需要使用重负载预构建类扩展 proxies,例如 symfony 使用的代理来在使用时实现延迟加载。
  • 使用全局函数 - 初始化后,无需在任何地方注入注入器
  • 无处不在使用 - 注入即使在 vanilla 函数和闭包中也有效
  • 架构优化代码 - 无需预构建或预编译
  • 支持所有标准依赖增强技术 - 装饰、模拟、标记
  • 易于序列化 - 醒来时无需重新连接依赖
  • 支持装饰纯函数调用
  • 支持显式编程/架构 - 类掌握所有其实施细节(除了装饰、模拟和参数,这些本质上是外部驱动的外部上下文)
  • 可扩展性 - 与 其他依赖系统 集成。

缺点

  • 依赖依赖注入库 - 虽然这是一种非常软的耦合(您可以通过任何您喜欢的逻辑调整和替换 di_* 函数背后的逻辑。)
  • 轻微开销 - 装饰、模拟和标记需要一些编程逻辑

安装

通过 composer 安装 earc 依赖注入库。

$ composer require earc/di

您甚至可以与 symfony 一起使用。

基本用法

可以无参数初始化一个新的依赖解析器。在您的 index.php、引导或配置脚本中使用它。

use eArc\DI\DI;

DI::init();

之后,可以通过 di_* 函数访问类和参数。

di_get(SomeClass::class); // returns an instance of SomeClass::class

di_param('some.parameter'); // returns the value of the parameter `some.parameter`

di_get 在连续调用中返回相同的实例。如果您需要一个 对象,请使用 di_make。它在每次调用时都返回一个新的实例。在这两个函数中,必须使用完整的类名作为参数。

di_getdi_make 注入的类不能有构造函数参数。令人惊讶的是,*咳嗽* 实际上不需要构造函数参数。

class alphaCentauri {
    //...
    public function construct()
    {
        $this->dependency1 = di_get(DependencyOne::class);
        $this->dependency2 = di_get(DependencyTwo::class);
        $this->parameterAlpha = di_param('alpha');
        $this->parameterBeta = di_param('beta');
        $this->parameterGamma = di_param('gamma');
    }
    //...
}

支持方法和函数注入。

class Example
{
    public function getResult($param)
    {
        $math = di_get(Math::class);
        return $math->calculate($param, di_param('pi'));
    }
}

function depending_on_injection($param)
{
        $math = di_get(Math::class);
        return $math->calculate($param, di_param('pi'));
}

$result = depending_on_injection(42);

不需要任何更多的依赖配置!

当然,你需要导入参数。

参数

参数是键值对。键是 string 类型,值可以是任何类型 - 甚至闭包。但可能会有其他限制因素。如果你选择在 YAML 文件中组织参数,你可能只能使用 stringintfloatbool 和数组。

用法

di_param('key_name') 返回属于 key_name 键的参数值。

你可以通过 di_has_param('key_name') 检查其是否存在。

如果参数是动态生成的,你可以使用 di_set_param('key_name', $value) 使其全局可用。不可变请求对象在大多数用例中都是一个宝贵的动态生成参数。

提示:可变的全局参数根本不是参数。否则它将引入真正的巨大副作用。

如果参数未设置,di_param 会抛出 NotFoundException。你可以通过提供非 null 的默认参数作为第二个参数来抑制这种行为。而不是抛出异常,默认参数将被返回。

点语法

在大型应用程序中,参数键名可能导致命名冲突。因此,像 PHP 中的命名空间一样组织参数键名树层次结构是一个好主意。数组为你免费提供这种树层次结构,但很难看到背后的树。

di_param('base_key')['level1']['level2']['parameter_name'];

如果你需要在未知代码中搜索 parameter_name,这会更难,因为它可能被几个不同的参数使用。因此,earc/di 支持点符号语法用于 di_paramdi_has_paramdi_set_param

点符号语法中,上述操作将是

di_param('base_key.level1.level2.parameter_name');

导入

di_import_param 接收一个(可能是多维的)数组作为参数。这使得它在所有实现细节和框架中都保持灵活性。你可以硬编码参数

# config.php

di_import_param([
    'data' => [
        'server' => [
            'mysql' => [
                'user' => 'foo',
                'db' => 'bar',
                'password' => 'x23W!_bxTff',
                //...
            ],
            //...
        ],
        //...
    ],
    //...
]);

或者使用流行的 YAML 格式

# config.yml
data:
    server:
        mysql:
            user: 'foo'
            db: 'bar'
            password: 'x23W!_bxTff'
...
# bootstrap.php

di_import_param(
    Yaml::parse(
        file_get_contents('/path/to/config.yml')
    )
);

di_import_paramdi_set_param 不同,因为参数可能被覆盖。因此,库能够提供默认参数,这些参数可能(或可能不)被使用该库的软件更改。

最佳实践

在项目中为每个模块定义一个 ParameterInterface。通过在 ParameterInterface 中定义所有参数键(点符号)来定义常量。让所有使用参数的类实现模块的 ParameterInterface

例如,earc/event-treeParameterInterface 可以让你知道所有可能对你有意义的参数。你不需要知道所有文档,只需一个文件。这种明确的编程可以在文档较少的项目中起到救命的作用。

interface ParameterInterface
{
    const VENDOR_DIR = 'earc.vendor_directory';
    const ROOT_DIRECTORIES = 'earc.event_tree.directories';
    const BLACKLIST = 'earc.event_tree.blacklist';
}

工厂

如果你想使用不使用 earc/di 的第三方库的对象,它们有时不容易构建。想想 doctrine 的 EntityManager。它在使用 doctrine 的应用程序中是一个如此常见的对象,使用工厂实例而不是实际对象是一个严重的障碍。

使用 di_register_factory 来注册一个 callable 到完全限定的类名。

di_register_factory(SomeInterface::class, [Factory::class, 'build']);

然后,使用这个 callable 来构建对象。

如果你已经通过 get 获取了实例,你必须手动调用 clear_cache($fCQN) 来从工厂获取实例。

如果你向同一个完全限定的类名注册第二个工厂,第二个工厂将被使用。通过传递 null 作为工厂参数,你可以注销一个工厂。

装饰

将接口作为di_getdi_make的参数是一个好主意。earc/di会将这个参数用作类型提示。将类作为参数表示所有可能注入的类都必须继承自类型提示的类。将接口作为参数会导致可以注入的类范围更广,因为它们只需要实现该接口。但这种自由也伴随着代价:earc/di不再知道要构建和注入的类。因此,每个用作di_getdi_make参数的接口都必须先进行装饰。

di_decorate(SomeInterface::class, ThisImplementsTheInterface::class);

但装饰远不止于此。它是做出最后决定的地方,即需要注入什么,并且可以多次重复。

这就是控制反转发生的地方,是服务定位器的一部分。

假设你编写了一个库。每个月你都会添加一些酷炫的功能和错误修复。你有工作,妻子,几个孩子,所以你对问题和合并请求的反应速度并不快。尽管如此,你的库在生产中仍然被用作第三方库。在升级后,在生产中检测到方法中的错误。编程工程师需要快速修复你的库。他们不想分叉你的库,因为维护分叉的库可能是一项巨大的任务。如果他们可以只是交换方法,那岂不是很好?

di_decorate可以完成这项工作。扩展类,覆盖错误的方法,并装饰原始类就是所需的一切。

di_decorate(ServiceContainingAnError::class, ServiceDecorator::class);

get_class(di_get(ServiceContainingAnError::class)); // equals ServiceDecorator::class
get_class(di_make(ServiceContainingAnError::class)); // equals ServiceDecorator::class

现在,任何有问题的服务注入的地方,都需要放置已修复的类。

earc/di允许你装饰抽象类。

$staticService = di_static(AbstractService::class);
$staticService::staticMethod(); // calls AbstractService::staticMethod()

di_decorate(StaticService::class, AbstractServiceDecorator::class);

$staticService = di_static(StaticService::class);
$staticService::staticMethod(); // calls AbstractServiceDecorator::staticMethod()

装饰是在你软件的配置部分进行的。为了不显得过于繁琐,有一种方法可以在配置文件中完成此操作。在调用DI::importParameter()时,将导入参数earc.di.class_decoration

# config.yml

earc:
    di:
        class_decoration:
            namespace\\A\\SomeServiceClass: 'namespace\\B\\SomeServiceClass'
            namespace\\A\\SomeOtherServiceClass: 'namespace\\B\\SomeOtherServiceClass'
...
use eArc\DI\DI;

DI::init();

di_import_param(
    Yaml::parse(
        file_get_contents('/path/to/config.yml')
    )
);

DI::importParameter();

请注意调用顺序很重要。

为了调试目的,di_is_decorateddi_get_decorator是很有用的函数。但请注意,它调试的是当前的装饰,而不是装饰链的结果。

di_decorate(Service::class, ServiceDecorator::class);
di_decorate(ServiceDecorator::class, MegaDecorator::class);

di_get_decorator(Service::class); // equals ServiceDecorator::class
get_class(di_get(Service::class)); // equals MegaDecorator::class

要清除装饰,请通过自身装饰类。

di_decorate(Service::class, Service::class);

di_is_decorated(Service::class); // equals false
get_class(di_get(Service::class)); // equals Service::class

命名空间装饰

有时你会在另一个项目中映射来自一个项目的文件结构。然后,可能希望通过(重新)在映射的文件结构中替换它来自动装饰类。这就是命名空间装饰的目的。你可以在调用DI::importParameter()之前设置earc.di.namespace_decoration参数来激活此功能。

# config.yml

earc:
    di:
        namespace_decoration:
            ['namespace\\of\\dir\\project\\A', 'namespace\\of\\mirrored\\dir\\project\\B']
            # you can decorate as many namespaces you want.
            # but you can even chain them. You have to name 
            # every namespace decoration explicitly:
            ['namespace\\of\\mirrored\\dir\\project\\B', 'namespace\\of\\mirrored\\dir\\project\\C'] 
            ['namespace\\of\\dir\\project\\A', 'namespace\\of\\mirrored\\dir\\project\\C']
use eArc\DI\DI;

DI::init();

di_import_param(
    Yaml::parse(
        file_get_contents('/path/to/config.yml')
    )
);

DI::importParameter();

模拟

在大多数测试库中,mocks是对象。因此,你不能使用装饰来替换原始类。di_mock就是为了这种用途而设计的。

$getObj = di_get(Service::class);
$makeObj = di_get(Service::class);

$mockedService = TestCase::createMock(Service::class);
di_mock(Service::class, $mockedService);

Assert::assertSame($mockedService, di_get(Service::class)); // passes 
Assert::assertSame($mockedService, di_make(Service::class)); // passes
Assert::assertSame($getObj, di_get(Service::class)); // fails 
Assert::assertSame($makeObj, di_make(Service::class)); // fails

请记住:静态访问方法(通过di_static)只能通过装饰作为di_static返回一个stringdi_mock接收一个object来mock。

di_static(StaticService::class)::staticMethod(); // calls StaticService::staticMethod()

di_decorate(StaticService::class, StaticMock::class);

di_static(StaticService::class)::staticMethod(); // calls StaticMock::staticMethod()

你可以通过di_is_mocked检查一个服务是否被mock。

Assert::assertSame(true, di_is_mocked(ServiceClass)); // passes 
Assert::assertSame(true, di_is_mocked(AnotherService::class)); // fails

如果你需要再次使用真实的服务,请使用di_clear_mock

di_clear_mock(Service::class);

Assert::assertSame($mockedService, di_get(Service::class)); // fails 
Assert::assertSame($mockedService, di_make(Service::class)); // fails
Assert::assertSame($getObj, di_get(Service::class)); // passes
Assert::assertSame($makeObj, di_make(Service::class)); // fails

你可以清除所有现有的mock。

di_clear_mock();

mock是在装饰之后应用的。但di_*mock函数不考虑任何装饰。这给你提供了更多的mock过程控制,但也需要更多的关注。请记住,你必须mock装饰器,而不是被装饰的类。

di_mock(Service::class, (object) ['iAmMock' => 1]);
di_mock(ServiceDecorator::class, (object) ['iAmMock' => 2]);
di_decorate(Service::class, ServiceDecorator::class);

Assert::assertSame(true, di_is_mocked(Service::class)); // passes
Assert::assertSame(true, di_is_mocked(ServiceDecorator::class)); // passes

Assert::assertSame(1, di_get(Service::class)->iAmMock); // fails
Assert::assertSame(2, di_get(Service::class)->iAmMock); // passes
Assert::assertSame(2, di_get(ServiceDecorator::class)->iAmMock); // passes

正如你在示例代码中看到的,mocks不强制遵循类型提示。这意味着你可以传递任何你想要的mock。只要你的代码没有将服务作为类型提示的参数传递。)

标记

也许你通过实现责任链设计模式来解决问题。只有第三方软件知道哪些服务添加到这个实现中。这留下了四个问题:

  1. 如何在不实例化的情况下注册到基服务?
  2. 如何在实例化时告诉基服务而不实例化所有处理器?
  3. 在哪里编写信息最好?
  4. 在哪里存储信息最好?

通过标记,earc/di为你解决了这三个问题。

第三方可以通过执行来存储相关信息。

di_tag('tag.name', Service002::class);
di_tag('tag.name', Service007::class);
di_tag('tag.name', Service014::class);

您的基类可以通过遍历 di_get_tagged('tag.name') 来检索服务类。

foreach (di_get_tagged('tag.name') as $handlerName => $argument) {
    $handler = di_get($handlerName);
    if ($handler->canHandleTask($task)) {
        $result = $handler->handleTask($task);
        
        return $result;
    }
}

throw new TaskNotHandledException($task);

如果您向 di_tag 传递了第三个参数,则 $argument 将持有此参数而不是 null

当然,服务应该实现一个接口,而基服务应该检查它以避免在名称冲突或忘记方法时失败。是的,没有实现接口的日志处理程序是一个好主意。但您最了解您的软件需要的(并且可以负担的)架构。

提示:装饰不是应用于标签,而是当然应用于 di_get

故障排除

earc/di 已经放弃了循环依赖检测,以换取性能。如果您在 earc/di 代码中遇到以下错误,其原因是循环依赖的可能性最大。请在您的代码中搜索循环类依赖。

$ PHP Fatal error:  Uncaught Error: Maximum function nesting level of '256' reached, aborting! 

异常

  • 所有抛出的异常都继承自 BaseException

  • 如果

    1. init() 被调用,并且通过参数标识的类没有实现 ResolverInterfaceParameterBagInterface,则会抛出 InvalidArgumentException
    2. di_make()di_get() 使用不尊重参数类型提示的装饰器。(请注意,此检查不会在调用 di_decorate 时进行,以避免提前加载类文件。)
    3. di_set_param 将覆盖现有参数。
  • 如果在调用类构造函数时抛出某些异常,则会抛出 MakeClassException

  • 如果应该检索从未设置/导入的参数,则会抛出 NotFoundDIException

高级用法

某些用法可能不明显但很理想,例如函数装饰。如果您用 di_static 调用您自己的函数,它们可以使用 di_decorate 进行装饰。

function something_cool($times) {
    return $times.' x icecream';
}

di_static('something_cool')(42); // returns '42 x icecream'

function something_cool_but_its_winter($times) {
    return $times.' x hot tea';
}

di_decorate('something_cool', 'something_cool_but_its_winter');

di_static('something_cool')(42); // returns '42 x hot tea'

性能考虑

处理 earc/di 行为的类总计约 250 行代码,主要是快速数组计算。但尽管如此,某些大型应用或服务器限制可能迫使您再次考虑性能。

通过 di_get 获取的对象在有人调用 di_clear_cache 之前不能进行垃圾回收。如果您只使用一次对象或只使用很短的时间,则使用 di_make 可能会节省一些内存。

调用 di_has 对类和接口进行存在性检查。这些检查可以触发自动加载。要明智地使用它们。同样,如果您使用命名空间装饰,则对于 di_is_decorated 也是如此。

命名空间装饰使用字符串替换,这比在数组上执行键查找要慢得多。尽管如此,如果您必须装饰数百个类,它比显式配置表现更好,因为仅在请求活动的类上使用它,而配置必须为所有装饰的类在每次请求上处理。

架构考虑

不要依赖于 di_get 的单例行为。它的主要目的是性能考虑。如果您的架构需要始终为类获取相同的实例,请明确这样做并使用真正的单例。

In earc/di 中,每个类型提示都设置为全局。这意味着每个类型提示恰好强制一个类。如果您被迫在应用程序的配置部分之外使用 di_decorate 来更改该行为,您将体验到一种不良的架构气味。这表明您的某些类需要比类型提示更多的内容。也许您的接口没有遵循接口隔离原则,也许您只需要另一个接口,或者也许您的某些类没有遵循单一责任原则。

与其他依赖注入系统的集成

您能够完全重写背后的行为逻辑。这使得 earc/di 能够与几乎所有依赖注入系统集成。根据您的需求扩展 Resolver::class 或 ParameterBag::class 或实现相应的接口。通过 DI::init 方法注册您的类。现在 di_* 函数遵循您已实现的逻辑。

如果第三方 di 系统使用容器,则集成是一个初学者的任务。您可以在 bridge 文件夹中找到一个 symfony 的现成示例。不要忘记将您的 symfony 服务定义切换到 public

如果您使用symfony,请注册SymfonyDICompilerPass,然后即可开始(或逐步迁移)。

没有限制。创建自己的以统治它们所有,使您的生活再次变得简单。

版本

版本 3.1

  • 循环依赖检测

版本 3.0

  • PHP 8.0 支持
  • 将 di_tag 参数交换,以便您能够更好地在代码中搜索相关片段

版本 2.4

  • 对 PHPStorm 的 IDE 支持
    • di_getdi_makedi_static 的返回类型支持

版本 2.3

  • 工厂支持

版本 2.2

  • 默认参数
  • 批量装饰
  • 命名空间装饰

版本 2.1

  • 可以为标签传递一个参数

版本 2.0

  • 基于对依赖注入的新看法的完整重写

  • 使用全局函数进行注入

  • 对容器合并的支持已删除 - 以定制(通过扩展或接口)为主

  • 对标志的支持已删除 - 所有操作现在都可以显式完成

  • 对树形依赖的支持已删除 - 扩展您的类以使用不同的构造函数或使用工厂来支持不同的注入类型(顺便说一下,这更明确)

  • 对循环依赖检测的支持已删除 - 以 php 执行任务为主

  • 删除所有类型的依赖配置 - 以纯注入为主(是的,这是真的!不再有依赖配置,只有参数导入、装饰、标签和模拟。依赖配置简化为基于服务定位器模式的模式。每个类型提示都设置为全局,这显著减少了您必须处理的信息。)

  • 删除对其他库的依赖 - 以轻量级架构为主

版本 1.0

  • 支持标志。

  • 支持容器合并和在构造时仅合并容器。

  • 已宣布的新功能子集生成已删除,以支持 earc/components-di

  • 删除 NotFoundExceptionOverwriteException,改为支持来自 earc/payload-container 包ItemNotFoundExceptionItemOverwriteException

  • DependencyContainer::loadFile() 已不再支持。您可以通过 DependencyContainer::load(include ...) 来模拟它。

版本 0.1

第一个官方版本