stellarwp/container

适用于 WordPress 代码库的 PSR-11 依赖注入 (DI) 容器

v0.1.1 2022-03-28 19:13 UTC

This package is auto-updated.

Last update: 2024-09-21 21:14:34 UTC


README

CI Pipeline

此库包含一个 PSR-11 兼容的依赖注入 (DI) 容器,用于帮助解决各种应用中所需的依赖关系。

什么是依赖注入?

简单来说,依赖注入是向对象提供依赖项,而不是让对象尝试创建/检索它们。

例如,想象我们正在构建一个包含不同“模块”的插件,每个模块都可能接收一个全局的 Settings 对象。

使用依赖注入,我们的模块定义可能如下所示

namespace Acme\SomePlugin;

class SomeModule extends Module
{

  /**
   * @var Settings
   */
  protected $settings;

  /**
   * @param Settings $settings
   */
  public function __construct(Settings $settings)
  {
    $this->settings = $settings;
  }
}

通过注入 Settings 对象,我们能够创建对象的单个实例,并在测试中更容易地注入 测试替身

现在,比较一下同一类不使用依赖注入的版本

namespace Acme\SomePlugin;

class SomeModule extends Module
{

  /**
   * @var Settings
   */
  protected $settings;

  public function __construct()
  {
    $this->settings = new Settings();
  }
}

在这种模式下,每个模块的实例都将负责实例化自己的 Settings 对象实例,并且我们无法注入测试替身。

此外,如果 Settings 类更改其构造函数方法签名,我们必须更新应用程序中所有对 new Settings() 的调用。

这是 DI 容器的主要优势之一:我们可以在一个地方定义对象的构建方式,然后递归地解决依赖关系。

依赖注入与服务定位

值得一提的是,容器是为依赖注入而设计的,而不是作为服务定位器。

什么是服务定位器?想象一下,如果我们不是将 Settings 对象注入到我们的集成中,而是注入整个 Container 对象。我们不是给类提供完成其工作的工具,而是把整个应用程序扔给它,说“这里,你自己解决吧。”

PSR-11 元文档对这些模式有很好的分解.

安装

建议您通过 Composer 将 DI 容器作为项目依赖项安装

$ composer require stellarwp/container

接下来,在您的项目中创建一个新的类,该类扩展了 StellarWP\Container\Container

<?php

namespace Acme\SomePlugin;

use StellarWP\Container\Container as BaseContainer;

class Container extends BaseContainer
{
    /**
	 * Retrieve a mapping of identifiers to callables.
	 *
	 * When an identifier is requested through the container, the container will find the given
	 * dependency in this array, execute the callable, and return the result.
	 *
	 * @return Array<string,callable> A mapping of identifiers to callables.
	 */
	public function config()
    {
        return [
            // ...
        ];
    }
}

您可以自由地自定义任何内容,但有一个抽象方法需要填写:config()

定义 config() 方法

DI 容器的一个关键部分是将抽象依赖项(例如,接口和/或类名)与具体实例之间的映射;在 StellarWP 容器中,这是通过 StellarWP\Container\Container::config() 方法定义的。

config() 方法应该返回一个一维的关联数组,将抽象标识符映射到将产生具体实例的可调用项。

一个非常简单的例子可能如下所示:假设我们有一个 接口SandwichInterface,它描述了如何制作三明治。

现在,假设我们有一个该接口的实现,PBandJ,它定义了一个花生酱果酱 (PB&J) 三明治。正如你所猜到的,我们的 PBandJ 类有三个依赖项:面包、花生酱和果酱。这个类的定义可能如下所示

namespace Acme\SomePlugin;

class PBandJ implements SandwichInterface
{

    public function __construct(Bread $bread,PeanutButter $pb, Jelly $jelly)
    {
        // ...
    }
}

现在,让我们假设在任何时候我们想要在我们的应用程序中获取三明治,它应该是一个花生酱果酱三明治。在我们的容器config()方法中,我们将定义一个匿名函数,该函数将返回一个绑定到SandwichInterfacePBandJ实例。

use Acme\SomePlugin\Bread;
use Acme\SomePlugin\Jelly;
use Acme\SomePlugin\PeanutButter;
use Acme\SomePlugin\SandwichInterface;

public function config()
{
    return [
        Bread::class        => null,
        Jelly::class        => null,
        PeanutButter::class => null,

        // In order to construct a PBandJ, we need both PeanutButter and Jelly.
        SandwichInterface::class => function ($container) {
            return new PBandJ(
                $container->make(Bread::class),
                $container->make(PeanutButter::class),
                $container->make(Jelly::class)
            );
        },
    ];
}

当我们从DI容器请求三明治时,我们现在将得到上面定义的PB&J。

$sandwich = (new Container())->get(SandwichInterface::class);

var_dump($sandwich instanceof PBandJ);
# => bool(true)

如果我们想定义多种类型的三明治,我们也可以使用PBandJ作为抽象(数组键),然后通过$container->get(PBandJ::class)请求它。

⚠️ 关于抽象标识符的说明

虽然使用类或接口名称作为抽象标识符可能最有用,但这可以是任何字符串(例如,“peanut_butter”)。

递归定义

在上面的PB&J示例中,请注意,SandwichInterface的回调被赋予了一个$container参数:这是当前容器实例,让我们可以递归地定义我们的依赖关系。

例如,如果我们使用自制面包,我们可能有一个接受FlourYeastWaterSalt作为依赖项的Bread实现。我们会在配置中定义Bread及其依赖项,并在定义SandwichInterface时调用$container->make(Bread::class),容器会自动在注入之前解析Bread

请注意,如果在解析依赖关系时检测到递归循环(例如,DrinkingCoffee依赖于MakingCoffee,后者依赖于BeingFunctionalInTheMorning,后者又依赖于DrinkingCoffee),将抛出StellarWP\Container\Exceptions\RecursiveDependencyException异常。

别名

有时将一个容器定义指向另一个容器很有帮助,尤其是在构建可扩展的基础容器或引入容器到现有代码库时。

StellarWP容器支持别名定义,其中配置数组中的“具体”值指向另一个抽象。

[
    Hero::class   => Hoagie::class,
    Hoagie::class => Sub::class,
    Sub::class    => function () {
        return new ItalianSubSandwich();
    },
    // ...
]

$hero   = $container->get(Hero::class);
$hoagie = $container->get(Hoagie::class);
$sub    = $container->get(Sub::class);

var_dump(($hero === $hoagie) && ($hoagie === $sub));
# => bool(true)

⚡️ 性能建议

为了获得最佳性能,建议您尽量使用单个抽象而不是依赖别名,但它们在那里供您使用。

使用DI容器

一旦定义了容器的配置,就是时候开始在您的项目中使用了!

首先,您需要构建容器的一个实例。

use Acme\SomePlugin\Container;

$container = new Container();

现在我们有了容器实例,让我们尝试解决一些依赖关系。为了做到这一点,我们可以使用两种方法之一:get()make()

get()方法将解析依赖关系并缓存结果,因此对同一依赖关系的后续调用将返回相同的值。

$first  = $container->get(SomeAbstract::class);
$second = $container->get(SomeAbstract::class);

var_dump($first === $second);
# => bool(true)

另一方面,make()方法每次都会返回依赖项的新副本。

$first  = $container->make(SomeAbstract::class);
$second = $container->make(SomeAbstract::class);

var_dump($first === $second);
# => bool(false)

然而,需要注意的是,对依赖项调用get()始终缓存它及其递归依赖项,而make()只有在通过get()解析时才会缓存递归依赖项。想象我们的容器包含以下定义

[
    Lunch::class             => function ($container) {
        return new BoxedLunch(
            $container->make(Sandwich::class),
            $container->get(Fruit::class)
        );
    },
    SandwichInterface::class => function ($container) {
        return $container->make(PBandJ::class);
    },
    Fruit::class             => function ($container) {
        return $container->make(Apple::class);
    },

    // ...and more!
]

当通过容器解析Lunch时,基于在定义中是否使用make()get(),缓存行为会有所不同。

使用$container->get()

$container = new Container();
$container->get(Lunch::class);

var_dump($container->hasResolved(Lunch::class));
# => bool(true)

var_dump($container->hasResolved(SandwichInterface::class));
# => bool(true)

var_dump($container->hasResolved(PBandJ::class));
# => bool(true)

var_dump($container->hasResolved(Fruit::class));
# => bool(true)

var_dump($container->hasResolved(Apple::class));
# => bool(true)

使用$container->make()

$container = new Container();
$container->make(Lunch::class);

var_dump($container->hasResolved(Lunch::class));
# => bool(false)

var_dump($container->hasResolved(SandwichInterface::class));
# => bool(false)

var_dump($container->hasResolved(PBandJ::class));
# => bool(false)

var_dump($container->hasResolved(Fruit::class));
# => bool(true)

var_dump($container->hasResolved(Apple::class));
# => bool(true)

如你所见,由于在Lunch的定义中使用了get(),所以FruitApple的定义将始终被缓存。在某些情况下,这可能是可取的,但通常最好在解析时使用$container->make()

如果容器被请求一个它没有定义的依赖,它将抛出 StellarWP\Container\Exceptions\NotFoundException。为了避免这种情况,你可以通过 $container->has(SomeAbstract::class) 来检查是否存在定义。你也可以通过 $container->resolved(SomeAbstract::class) 来检查容器是否有一个缓存的解析。

清除缓存依赖

如果你需要清除特定依赖的缓存,你可以调用 $container->forget(SomeAbstract::class),随后的 $container->get() 调用将重新生成缓存的值。

需要注意的是,调用 $container->forget() 不会递归地移除其子依赖,例如:

$container = new Container();
$container->get(Lunch::class);
$container->forget(Lunch::class);

var_dump($container->hasResolved(Lunch::class));
# => bool(false)

var_dump($container->hasResolved(SandwichInterface::class));
# => bool(true)

如果你需要忘记多个依赖,你可以将它们作为单独的参数传递给 $container->forget()

$container->forget(Lunch::class, SandwichInterface::class);

将容器用作 Singleton

如果你需要能够在依赖中访问容器(在将 DI 容器引入现有代码库时并不罕见),你可以使用静态的 Container::getInstance() 来返回容器的 Singleton 版本(这意味着每次调用 Container::getInstance() 都将返回同一个实例)

use Acme\SomePlugin\Container;

Container::getInstance()->get(SomeAbstract::class);

然而,这可能导致两个独立的容器实例:Singleton 和通过 new Container() 创建的实例

use Acme\SomePlugin\Container;

$container = new Container();

// Elsewhere.
$singleton = Container::getInstance();

var_dump($singleton === $container);
# => bool(false)

为了减少这种重复,getInstance() 方法接受一个可选的 $instance 参数,该参数覆盖了容器内部的 $instance 属性

use Acme\SomePlugin\Container;

$container = new Container();

// Elsewhere.
$singleton = Container::getInstance($container);

var_dump($singleton === $container);
# => bool(true)

扩展定义

使用 DI 容器也使得测试更加容易,尤其是在我们利用 extend() 方法时。

此方法允许我们覆盖 DI 容器为给定抽象的定义,使我们能够注入测试双胞胎和/或已知值。

例如,假设我们有一个 ServiceSdk 依赖项,这是一个与某些服务交互的第三方 SDK。我们不一定希望我们的自动化测试实际调用服务(这会使我们的测试变得缓慢和脆弱),因此我们可以在测试中替换我们的服务定义

use Acme\SomePlugin\UserController;
use Vendor\Package\Response;
use Vendor\Package\Sdk as ServiceSdk;

/**
 * @test
 */
public function saveUser_should_update_the_account_email()
{
  $user_id = $this->factory()->user->create([
    'email' => '[email protected]',
  ]);

  /*
   * Expect that our code will end up calling ServiceSdk::patch() once with the given args and will
   * return a Response object with status code 200.
   *
   * @link https://phpunit.readthedocs.io/en/9.5/test-doubles.html
   */
  $service = $this->createMock(ServiceSdk::class);
  $service->expects($this->once())
    ->method('patch')
    ->withArgs('/users/' . $user_id, ['email' => '[email protected]'])
    ->willReturn(new Response(200));

  // Replace the default ServiceSdk instance with our mock.
  $this->container->extend(ServiceSdk::class, function () use ($service) {
    return $service;
  });

  $this->container->get(UserController::class)->update([
    'user'  => $user_id,
    'email' => '[email protected]',
  ]);
}

你还可以直接通过 extend() 将解析的实例传递到容器中

   // Replace the default ServiceSdk instance with our mock.
-  $this->container->extend(ServiceSdk::class, function () use ($service) {
-    return $service;
-  });
+  $this->container->extend(ServiceSdk::class, $service);

⏮ 恢复原始定义

如果你需要恢复一个抽象的原始定义,你可以使用 $container->restore() 移除其扩展。

贡献

如果你对为该项目做出贡献感兴趣,请 查看我们的贡献文档

许可

此库根据 MIT 许可证 的条款进行许可。