热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

依赖注入容器_如何构建自己的依赖注入容器

依赖注入容器Asearchfor“dependencyinjectioncontainer”onpackagistcurrentlyprovidesover95pagesofres

依赖注入容器

A search for “dependency injection container” on packagist currently provides over 95 pages of results. It is safe to say that this particular “wheel” has been invented.

在packagist上搜索“依赖注入容器”目前可提供95页以上的结果。 可以肯定地说,已经发明了这种特殊的“轮子”。

Square wheel?

However, no chef ever learned to cook using only ready meals. Likewise, no developer ever learned programming using only “ready code”.

然而,没有厨师学会只用即食做饭。 同样, 没有开发人员曾经只使用“就绪代码”来学习编程

In this article, we are going to learn how to make a simple dependency injection container package. All of the code written in this article, plus PHPDoc annotations and unit tests with 100% coverage is available at this GitHub repository. It is also listed on Packagist.

在本文中,我们将学习如何制作一个简单的依赖项注入容器包。 该GitHub存储库中提供了本文编写的所有代码,以及覆盖率100%PHPDoc注释和单元测试。 它也在Packagist上列出 。

规划我们的依赖注入容器 (Planning Our Dependency Injection Container)

Let us start by planning what it is that we want our container to do. A good start is to split “Dependency Injection Container” into two roles, “Dependency Injection” and “Container”.

让我们从计划容器要做什么开始。 一个好的开始是将“依赖注入容器”划分为两个角色,“依赖注入”和“容器”。

The two most common methods for accomplishing dependency injection is through constructor injection or setter injection. That is, passing class dependencies through constructor arguments or method calls. If our container is going to be able to instantiate and contain services, it needs to be able to do both of these.

完成依赖项注入的两种最常见方法是通过构造函数注入setter注入 。 也就是说,通过构造函数参数或方法调用传递类依赖关系。 如果我们的容器能够实例化并包含服务,则它必须能够同时执行这两项。

To be a container, it has to be able to store and retrieve instances of services. This is quite a trivial task compared to creating the services, but it is still worth some consideration. The container-interop package provides a set of interfaces that containers can implement. The primary interface is the ContainerInterface that defines two methods, one for retrieving a service and one for testing if a service has been defined.

要成为容器,它必须能够存储和检索服务实例。 与创建服务相比,这是一项微不足道的任务,但是仍然值得考虑。 container-interop软件包提供了一组容器可以实现的接口。 主要接口是ContainerInterface ,它定义了两种方法,一种用于检索服务,另一种用于测试是否已定义服务。

interface ContainerInterface
{
public function get($id);
public function has($id);
}

向其他依赖注入容器学习 (Learning From Other Dependency Injection Containers)

The Symfony Dependency Injection Container allows us to define services in a variety of different ways. In YAML, the configuration for a container might look like this:

Symfony依赖注入容器允许我们以各种不同的方式定义服务。 在YAML中,容器的配置可能如下所示:

parameters:
# ...
mailer.transport: sendmail
services:
mailer:
class: Mailer
arguments: ["%mailer.transport%"]
newsletter_manager:
class: NewsletterManager
calls:
- [setMailer, ["@mailer"]]

The way Symfony splits the container configuration into configuration of parameters and services is very useful. This allows for application secrets such as API keys, encryption keys and auth tokens to be stored in parameters files that are excluded from source code repositories.

Symfony将容器配置拆分为参数和服务配置的方式非常有用。 这允许将应用程序秘密(例如API密钥,加密密钥和身份验证令牌)存储在参数文件中,该文件不包含在源代码存储库中。

In PHP, the same configuration for the Symfony Dependency Injection component would look like this:

在PHP中,Symfony依赖注入组件的相同配置如下所示:

use Symfony\Component\DependencyInjection\Reference;
// ...
$container->setParameter('mailer.transport', 'sendmail');
$container
->register('mailer', 'Mailer')
->addArgument('%mailer.transport%');
$container
->register('newsletter_manager', 'NewsletterManager')
->addMethodCall('setMailer', array(new Reference('mailer')));

By using a Reference object in the method call to setMailer, the dependency injection logic can detect that this value should not be passed directly, but replaced with the service that it references in the container. This allows for both PHP values and other services to be easily injected into a service without confusion.

通过在对setMailer的方法调用中使用Reference对象,依赖项注入逻辑可以检测到该值不应直接传递,而应替换为其在容器中引用的服务。 这使得PHP值和其他服务都可以轻松地注入到服务中,而不会引起混淆。

入门 (Getting Started)

The first thing to do is create a new project directory and make a composer.json file that can be used by Composer to autoload our classes. All this file does at the moment is map the SitePoint\Container namespace to the src directory.

要做的第一件事是创建一个新的项目目录,并创建一个composer.json文件, Composer可以使用该文件自动加载我们的类。 此文件目前所做的所有操作都是将SitePoint\Container命名空间映射到src目录。

{
"autoload": {
"psr-4": {
"SitePoint\\Container\\": "src/"
}
},
}

Next, as we are going to make our container implement the container-interop interfaces, we need to make composer download them and add them to our composer.json file:

接下来,当我们要使容器实现container-interop接口时,我们需要使作曲家下载它们并将其添加到composer.json文件中:

composer require container-interop/container-interop

Along with the primary ContainerInterface, the container-interop package also defines two exception interfaces. The first for general exceptions encountered creating a service and another for when a service that has been requested could not be found. We will also add another exception to this list, for when a parameter that has been requested cannot be found.

与主要的ContainerInterface , container-interop包还定义了两个异常接口。 对于一般的例外,第一个遇到创建服务的情况,第二个则是在找不到请求的服务时遇到的情况。 我们还将在此列表中添加另一个例外,用于无法找到所请求的参数的情况。

As we do not need to add any functionality beyond what is offered by the core PHP Exception class, these classes are pretty simple. Whilst they might seem pointless, splitting them up like this allows us to easily catch and handle them independently.

由于除了核心PHP Exception类提供的功能之外,我们不需要添加任何功能,因此这些类非常简单。 尽管它们似乎毫无意义,但将它们拆分成这样可以使我们轻松地独立捕获和处理它们。

Make the src directory and create these three files at src/Exception/ContainerException.php, src/Exception/ServiceNotFoundException.php and src/Exception/ParameterNotFoundException.php respectively:

进入src目录,并分别在src/Exception/ContainerException.php , src/Exception/ServiceNotFoundException.phpsrc/Exception/ParameterNotFoundException.php创建这三个文件:

namespace SitePoint\Container\Exception;
use Interop\Container\Exception\ContainerException as InteropContainerException;
class ContainerException extends \Exception implements InteropContainerException {}

namespace SitePoint\Container\Exception;
use Interop\Container\Exception\NotFoundException as InteropNotFoundException;
class ServiceNotFoundException extends \Exception implements InteropNotFoundException {}

namespace SitePoint\Container\Exception;
class ParameterNotFoundException extends \Exception {}

容器参考 (Container References)

The Symfony Reference class discussed earlier allowed the library to distinguish between PHP values to be used directly and arguments that needed to be replaced by other services in the container.

前面讨论的Symfony Reference类允许库区分直接使用PHP值和需要由容器中的其他服务替换的参数。

Let us steal that idea, and create two classes for references to parameters and services. As both of these classes are going to be value objects storing just the name of the resource that they refer to, it makes sense to use an abstract class as a base. That way we do not have to write the same code twice.

让我们窃取这个想法,并创建两个类来引用参数和服务。 由于这两个类都是将仅存储它们所引用资源名称的值对象,因此使用抽象类作为基础是有意义的。 这样,我们不必编写相同的代码两次。

Create the following files at src/Reference/AbstractReference.php, src/Reference/ServiceReference.php and src/Reference/ParameterReference.php respectively:

src/Reference/AbstractReference.php , src/Reference/ServiceReference.phpsrc/Reference/ParameterReference.php分别创建以下文件:

namespace SitePoint\Container\Reference;
abstract class AbstractReference
{
private $name;
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}

namespace SitePoint\Container\Reference;
class ServiceReference extends AbstractReference {}

namespace SitePoint\Container\Reference;
class ParameterReference extends AbstractReference {}

集装箱类 (The Container Class)

It is now time to create our container. We are going to start with a basic sketch map of our container class, and we will add methods to this as we go along.

现在是时候创建我们的容器了。 我们将从容器类的基本草图开始,然后逐步添加方法。

The general idea will be to accept two arrays in the constructor of our container. The first array will contain the service definitions and the second will contain the parameter definitions.

一般的想法是在容器的构造函数中接受两个数组。 第一个数组将包含服务定义,第二个数组将包含参数定义。

At src/Container.php, place the following code:

src/Container.php ,放置以下代码:

namespace SitePoint\Container;
use Interop\Container\ContainerInterface as InteropContainerInterface;
class Container implements InteropContainerInterface
{
private $services;
private $parameters;
private $serviceStore;
public function __construct(array $services = [], array $parameters = [])
{
$this->services = $services;
$this->parameters = $parameters;
$this->serviceStore = [];
}
}

All we are doing here is implementing the ContainerInterface from container-interop and loading the definitions into properties that can be accessed later. We have also created a serviceStore property, and initialized it to be an empty array. When the container is asked to create services, we will save these in this array so that they can be retrieved later without having to recreate them.

我们在这里所做的就是从container-interop实现ContainerInterface ,并将定义加载到以后可以访问的属性中。 我们还创建了一个serviceStore属性,并将其初始化为一个空数组。 当要求容器创建服务时,我们会将它们保存在此数组中,以便以后可以检索它们而不必重新创建它们。

Now let us begin writing the methods defined by container-interop. Starting with get($name), add the following method to the class:

现在让我们开始编写container-interop定义的方法。 从get($name) ,将以下方法添加到类中:

use SitePoint\Container\Exception\ServiceNotFoundException;
// ...
public function get($name)
{
if (!$this->has($name)) {
throw new ServiceNotFoundException('Service not found: '.$name);
}
if (!isset($this->serviceStore[$name])) {
$this->serviceStore[$name] = $this->createService($name);
}
return $this->serviceStore[$name];
}
// ...

Be sure to add the use statement to the top of the file. Our get($name) method simply checks to see if the container has the definition for a service. If it does not, the ServiceNotFoundException that we created earlier is thrown. If it does, it returns the service, creating it and saving it to the store if it has not already done so.

确保将use语句添加到文件顶部。 我们的get($name)方法只是检查容器是否具有服务的定义。 如果不是,则会抛出我们之前创建的ServiceNotFoundException 。 如果是,它将返回服务,将其创建并保存到商店(如果尚未这样做)。

While we are at it, we should make a method for retrieving a parameter from the container. Assuming the parameters passed to the constructor form an N-dimensional associative array, we need some way of cleanly accessing any element within that array using a single string. An easy way of doing this is to use . as a delimiter, so that the string foo.bar refers to the bar key in the foo key of the root parameters array.

在此过程中,我们应该制定一种从容器中检索参数的方法。 假设传递给构造函数的参数形成一个N维关联数组,我们需要某种方式使用单个字符串来干净地访问该数组中的任何元素。 一种简单的方法是使用. 作为分隔符,因此字符串foo.bar指向根参数数组的foo键中的bar键。

use SitePoint\Container\Exception\ParameterNotFoundException;
// ...
public function getParameter($name)
{
$tokens = explode('.', $name);
$context = $this->parameters;
while (null !== ($token = array_shift($tokens))) {
if (!isset($context[$token])) {
throw new ParameterNotFoundException('Parameter not found: '.$name);
}
$context = $context[$token];
}
return $context;
}
// ...

Now, we have used a couple of methods that we have not yet written. The first of those is the has($name) method, that is also defined by container-interop. This is a pretty simple method, and it just needs to check if the definitions array provided to the constructor contains an entry for the $name service.

现在,我们使用了一些尚未编写的方法。 第一个是has($name)方法,该方法也由container-interop定义。 这是一个非常简单的方法,它只需要检查提供给构造函数的definitions数组是否包含$name服务的条目。

// ...
public function has($name)
{
return isset($this->services[$name]);
}
// ...

The other method we called that we are yet to write is the createService($name) method. This method will use the definitions provided to create the service. As we do not want this method to be called from outside the container, we shall make it private.

我们还需要编写的另一个方法是createService($name)方法。 此方法将使用提供的定义来创建服务。 由于我们不希望从容器外部调用此方法,因此应将其设为私有。

The first thing to do in this method is some sanity checks. For each service definition we require an array containing a class key and optional arguments and calls keys. These will be used for constructor injection and setter injection respectively. We can also add protection against circular references by checking to see if we have already attempted to create the service.

此方法要做的第一件事是进行完整性检查。 对于每个服务定义,我们都需要一个包含class键以及可选argumentscalls键的数组。 这些将分别用于构造函数注入和setter注入。 我们还可以通过检查是否已经尝试创建服务来添加针对循环引用的保护。

If the arguments key exists, we want to convert that array of argument definitions into an array of PHP values that can be passed to the constructor. To do this, we will need to convert the reference objects that we defined earlier to the values that they reference in from the container. For now, we will take this logic into the resolveArguments($name, array $argumentDefinitons) method. We use the ReflectionClass::newInstanceArgs() method to create the service using the arguments array. This is the constructor injection.

如果arguments键存在,我们希望将该参数定义数组转换为可以传递给构造函数PHP值数组。 为此,我们需要将之前定义的引用对象转换为它们从容器中引用的值。 现在,我们将把这个逻辑带入resolveArguments($name, array $argumentDefinitons)方法。 我们使用ReflectionClass::newInstanceArgs()方法使用arguments数组创建服务。 这是构造函数注入

If the calls key exists, we want to use the array of call definitions and apply them to the service that we have just created. Again, we will take this logic into a separate method defined as initializeService($service, $name, array $callDefinitions). This is the setter injection.

如果存在calls键,则我们要使用call definitions数组,并将它们应用于刚刚创建的服务。 同样,我们将此逻辑带入定义为initializeService($service, $name, array $callDefinitions)的单独方法中。 这是二传手注射

use SitePoint\Container\Exception\ContainerException;
// ...
private function createService($name)
{
$entry = &$this->services[$name];
if (!is_array($entry) || !isset($entry['class'])) {
throw new ContainerException($name.' service entry must be an array containing a \'class\' key');
} elseif (!class_exists($entry['class'])) {
throw new ContainerException($name.' service class does not exist: '.$entry['class']);
} elseif (isset($entry['lock'])) {
throw new ContainerException($name.' service contains a circular reference');
}
$entry['lock'] = true;
$arguments = isset($entry['arguments']) ? $this->resolveArguments($name, $entry['arguments']) : [];
$reflector = new \ReflectionClass($entry['class']);
$service = $reflector->newInstanceArgs($arguments);
if (isset($entry['calls'])) {
$this->initializeService($service, $name, $entry['calls']);
}
return $service;
}
// ...

That leaves us with two final methods to create. The first should convert an array of argument definitions into an array of PHP values. To do this it will need to replace ParameterReference and ServiceReference objects with the appropriate parameters and services from the container.

剩下两个最终的创建方法。 第一个应该将参数定义数组转换为PHP值数组。 为此,它将需要用容器中的适当参数和服务替换ParameterReferenceServiceReference对象。

use SitePoint\Container\Reference\ParameterReference;
use SitePoint\Container\Reference\ServiceReference;
// ...
private function resolveArguments($name, array $argumentDefinitions)
{
$arguments = [];
foreach ($argumentDefinitions as $argumentDefinition) {
if ($argumentDefinition instanceof ServiceReference) {
$argumentServiceName = $argumentDefinition->getName();
$arguments[] = $this->get($argumentServiceName);
} elseif ($argumentDefinition instanceof ParameterReference) {
$argumentParameterName = $argumentDefinition->getName();
$arguments[] = $this->getParameter($argumentParameterName);
} else {
$arguments[] = $argumentDefinition;
}
}
return $arguments;
}

The last method performs the setter injection on the instantiated service object. To do this it needs to loop through an array of method call definitions. The method key is used to specify the method, and an optional arguments key can be used to provide arguments to that method call. We can reuse the method we just wrote to translate those arguments into PHP values.

最后一种方法在实例化的服务对象上执行setter注入。 为此,它需要遍历方法调用定义的数组。 method键用于指定方法,可选的arguments键可用于为该方法调用提供参数。 我们可以重用刚刚编写的方法,将这些参数转换为PHP值。

private function initializeService($service, $name, array $callDefinitions)
{
foreach ($callDefinitions as $callDefinition) {
if (!is_array($callDefinition) || !isset($callDefinition['method'])) {
throw new ContainerException($name.' service calls must be arrays containing a \'method\' key');
} elseif (!is_callable([$service, $callDefinition['method']])) {
throw new ContainerException($name.' service asks for call to uncallable method: '.$callDefinition['method']);
}
$arguments = isset($callDefinition['arguments']) ? $this->resolveArguments($name, $callDefinition['arguments']) : [];
call_user_func_array([$service, $callDefinition['method']], $arguments);
}
}
}

And we now have a usable dependency injection container! To see usage examples, check out the repository on GitHub.

现在,我们有了一个可用的依赖项注入容器! 要查看用法示例,请查看GitHub上的存储库 。

整理思路 (Finishing Thoughts)

We have learned how to make a simple dependency injection container, but there are loads of containers out there with cool features that ours does not have yet!

我们已经学习了如何制作一个简单的依赖项注入容器,但是其中有许多容器具有我们还没有的很酷的功能!

Some dependency injection containers, such as PHP-DI and Aura.Di provide a feature called auto-wiring. This is where the container guesses which services from the container should be injected into others. To do this, they use the reflection API to find out information about the constructor parameters.

一些依赖项注入容器,例如PHP-DI和Aura.Di提供了一种称为自动装配的功能。 容器在此处猜测容器中的哪些服务应注入到其他服务中。 为此,他们使用反射API来查找有关构造函数参数的信息。

Feel free to fork the repository and add features such as auto-wiring or whatever else you can think of, it’s great practice! Furthermore, we keep a public list all known forks of this container so that others can see the work you have done. Just use the comments below to share your work with us, and we will make sure it gets added.

随意分叉存储库并添加诸如自动装配之类的功能,或者您可以想到的任何其他功能,这是个好习惯! 此外,我们会公开列出此容器的所有已知派生,以便其他人可以看到您所做的工作。 请使用以下评论与我们分享您的工作,我们将确保添加它。

You can also use the comments below to get in touch. Let us know about anything that you would like clarified or explained, or any bugs that you have spotted.

您也可以使用下面的评论进行联系。 让我们知道您想澄清或解释的任何内容,或发现的任何错误。

Keep your eyes open for more articles like this on SitePoint PHP. We will soon be explaining how to reinvent the wheel with a range of common PHP components!

请继续关注SitePoint PHP上的更多此类文章。 我们将很快解释如何使用一系列常见PHP组件重塑轮子!

翻译自: https://www.sitepoint.com/how-to-build-your-own-dependency-injection-container/

依赖注入容器



推荐阅读
  • ALTERTABLE通过更改、添加、除去列和约束,或者通过启用或禁用约束和触发器来更改表的定义。语法ALTERTABLEtable{[ALTERCOLUMNcolu ... [详细]
  • 深度学习中的Vision Transformer (ViT)详解
    本文详细介绍了深度学习中的Vision Transformer (ViT)方法。首先介绍了相关工作和ViT的基本原理,包括图像块嵌入、可学习的嵌入、位置嵌入和Transformer编码器等。接着讨论了ViT的张量维度变化、归纳偏置与混合架构、微调及更高分辨率等方面。最后给出了实验结果和相关代码的链接。本文的研究表明,对于CV任务,直接应用纯Transformer架构于图像块序列是可行的,无需依赖于卷积网络。 ... [详细]
  • 本文介绍了在处理不规则数据时如何使用Python自动提取文本中的时间日期,包括使用dateutil.parser模块统一日期字符串格式和使用datefinder模块提取日期。同时,还介绍了一段使用正则表达式的代码,可以支持中文日期和一些特殊的时间识别,例如'2012年12月12日'、'3小时前'、'在2012/12/13哈哈'等。 ... [详细]
  • 本文整理了315道Python基础题目及答案,帮助读者检验学习成果。文章介绍了学习Python的途径、Python与其他编程语言的对比、解释型和编译型编程语言的简述、Python解释器的种类和特点、位和字节的关系、以及至少5个PEP8规范。对于想要检验自己学习成果的读者,这些题目将是一个不错的选择。请注意,答案在视频中,本文不提供答案。 ... [详细]
  • Python的参数解析argparse模块的学习
    本文介绍了Python中参数解析的重要模块argparse的学习内容。包括位置参数和可选参数的定义和使用方式,以及add_argument()函数的详细参数关键字解释。同时还介绍了命令行参数的操作和可接受数量的设置,其中包括整数类型的参数。通过学习本文内容,可以更好地理解和使用argparse模块进行参数解析。 ... [详细]
  • OpenMap教程4 – 图层概述
    本文介绍了OpenMap教程4中关于地图图层的内容,包括将ShapeLayer添加到MapBean中的方法,OpenMap支持的图层类型以及使用BufferedLayer创建图像的MapBean。此外,还介绍了Layer背景标志的作用和OMGraphicHandlerLayer的基础层类。 ... [详细]
  • node.jsurlsearchparamsAPI哎哎哎 ... [详细]
  • HashMap的扩容知识详解
    本文详细介绍了HashMap的扩容知识,包括扩容的概述、扩容条件以及1.7版本中的扩容方法。通过学习本文,读者可以全面了解HashMap的扩容机制,提升对HashMap的理解和应用能力。 ... [详细]
  • flowable工作流 流程变量_信也科技工作流平台的技术实践
    1背景随着公司业务发展及内部业务流程诉求的增长,目前信息化系统不能够很好满足期望,主要体现如下:目前OA流程引擎无法满足企业特定业务流程需求,且移动端体 ... [详细]
  • 本文介绍了Windows操作系统的版本及其特点,包括Windows 7系统的6个版本:Starter、Home Basic、Home Premium、Professional、Enterprise、Ultimate。Windows操作系统是微软公司研发的一套操作系统,具有人机操作性优异、支持的应用软件较多、对硬件支持良好等优点。Windows 7 Starter是功能最少的版本,缺乏Aero特效功能,没有64位支持,最初设计不能同时运行三个以上应用程序。 ... [详细]
  • 本文介绍了OpenStack的逻辑概念以及其构成简介,包括了软件开源项目、基础设施资源管理平台、三大核心组件等内容。同时还介绍了Horizon(UI模块)等相关信息。 ... [详细]
  • 本文介绍了如何在Azure应用服务实例上获取.NetCore 3.0+的支持。作者分享了自己在将代码升级为使用.NET Core 3.0时遇到的问题,并提供了解决方法。文章还介绍了在部署过程中使用Kudu构建的方法,并指出了可能出现的错误。此外,还介绍了开发者应用服务计划和免费产品应用服务计划在不同地区的运行情况。最后,文章指出了当前的.NET SDK不支持目标为.NET Core 3.0的问题,并提供了解决方案。 ... [详细]
  • 云原生应用最佳开发实践之十二原则(12factor)
    目录简介一、基准代码二、依赖三、配置四、后端配置五、构建、发布、运行六、进程七、端口绑定八、并发九、易处理十、开发与线上环境等价十一、日志十二、进程管理当 ... [详细]
  • 从壹开始前后端分离【 .NET Core2.0 +Vue2.0 】框架之六 || API项目整体搭建 6.1 仓储模式
    代码已上传Github+Gitee,文末有地址  书接上文:前几回文章中,我们花了三天的时间简单了解了下接口文档Swagger框架,已经完全解放了我们的以前的Word说明文档,并且可以在线进行调 ... [详细]
  • 本文整理了Java中org.assertj.core.api.AbstractPathAssert.existsNoFollowLinks()方法的一些代码示例,展示了 ... [详细]
author-avatar
houxue
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有