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

依赖注入容器

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.

与主要的ContainerInterfacecontainer-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.phpsrc/Exception/ServiceNotFoundException.phpsrc/Exception/ParameterNotFoundException.php创建这三个文件:

<?php

namespace SitePoint\Container\Exception;

use Interop\Container\Exception\ContainerException as InteropContainerException;

class ContainerException extends \Exception implements InteropContainerException {}
<?php

namespace SitePoint\Container\Exception;

use Interop\Container\Exception\NotFoundException as InteropNotFoundException;

class ServiceNotFoundException extends \Exception implements InteropNotFoundException {}
<?php

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.phpsrc/Reference/ServiceReference.phpsrc/Reference/ParameterReference.php分别创建以下文件:

<?php

namespace SitePoint\Container\Reference;

abstract class AbstractReference
{
    private $name;

    public function __construct($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }
}
<?php

namespace SitePoint\Container\Reference;

class ServiceReference extends AbstractReference {}
<?php

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 ,放置以下代码:

<?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-DIAura.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/

依赖注入容器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值