stellarwp / container
适用于 WordPress 代码库的 PSR-11 依赖注入 (DI) 容器
Requires
- php: ^5.6 | ^7.0 | ^8.0
- psr/container: ~1.0.0
Requires (Dev)
- league/climate: ^3.8
- stellarwp/coding-standards: dev-develop
- szepeviktor/phpstan-wordpress: ^1.0
- yoast/phpunit-polyfills: ^1.0
Provides
- psr/container-implementation: ^1.0.0
This package is auto-updated.
Last update: 2024-09-21 21:14:34 UTC
README
此库包含一个 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
对象。我们不是给类提供完成其工作的工具,而是把整个应用程序扔给它,说“这里,你自己解决吧。”
安装
建议您通过 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()
方法中,我们将定义一个匿名函数,该函数将返回一个绑定到SandwichInterface
的PBandJ
实例。
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
参数:这是当前容器实例,让我们可以递归地定义我们的依赖关系。
例如,如果我们使用自制面包,我们可能有一个接受Flour
、Yeast
、Water
和Salt
作为依赖项的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()
,所以Fruit
和Apple
的定义将始终被缓存。在某些情况下,这可能是可取的,但通常最好在解析时使用$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 许可证 的条款进行许可。