包含已注入依赖项的对象的 DI 容器

发布于 2024-11-10 09:40:55 字数 1930 浏览 3 评论 0原文

使用 pimple 作为我的 DI 容器,我一直勇敢地重构小类以依赖 DI 注入,消除了我可以看到的硬编码依赖项很容易被删除。

我执行此任务的方法非常简单,但我不知道它是否正确,因为除了上个月在这里学到的知识之外,我在 DI 和单元测试方面几乎没有经验。

我创建了一个 ContainerFactory 类,它是 pimple 的子类,并且在该子类中创建了仅返回特定对象容器的方法。

构造函数根据类型调用正确的创建者方法:

function __construct($type=null, $mode = null){

 if(isset($type)){  
    switch ($type) {
      case 'DataFactory':
         $this->buildDataFactoryContainer($mode);     
        break;
      case 'DbConnect':
         $this->buildDbConnectContainer($mode);  
        break;
     default:
        return false;
    }
  }
}

容器对象创建的方法签名如下:

public function buildDataFactoryContainer($mode=null)

这个想法是我可以在调用此容器时将 $mode 设置为 test,并让它加载测试值而不是实际的运行时设置。我想避免编写单独的容器类来进行测试,这是我认为不需要到处都有测试相关代码的简单方法。

我可以对 ContainerFactory 进行子类化,即:ContainerFactoryTesting extends ContainerFactory 并进行重写,而不是将测试代码与应用程序代码混合在一起,并使用 $mode=null 来混淆方法签名,但这不是本文的重点。继续,要为特定对象创建容器,我只需这样做:

 // returns container with DataFactory dependencies, holds $db and $logger objects.
 $dataFactoryContainer = new ContainerFactory('DataFactory');

// returns container with test settings.
$dataFactoryTestContainer = new ContainerFactory('DataFactory','test');

// returns container with DbConnect dependencies, holds dbconfig and $logger objects.
$dbConnectContainer = new ContainerFactory('DbConnect');

我刚刚遇到一个问题,让我怀疑我正在构建的策略有缺陷。

从上面的内容来看,DataFactory 包含保存数据库连接的 $db 对象。 我现在正在重构这个 dbclass 以删除它对 $registry 对象的依赖关系, 但是当我添加需要 $dbConnectContainer 的 $db 对象时,我将如何创建 $dataFactoryContainer?

例如,在 datafactory 容器中,我添加了一个 dbconnect 实例,但 IT 现在需要一个传递给它的容器......

我意识到我的英语不是很好,希望我已经解释得足够好,以便其他 SO'er 能够理解。

我的问题有两个,你们如何以简单的方式处理为本身包含依赖项的依赖项创建对象?

以及..如何分离容器配置来创建用于测试目的的对象?

一如既往,任何评论或相关帖子的链接都值得赞赏。

Using pimple as my DI container, I have been bravely refactoring small classes to rely on DI injection, eliminating the hard-coded dependencies I could see would be easily removed.

My methodology for this task is very simple, but I have no clue if it is proper as I have very little experience with DI and Unit-testing aside from what I learned here in past month.

I created a class, ContainerFactory that subclasses pimple, and in that subclass have created methods that simply returns container for specific object.

The constructor calls the proper creator method depending on type:

function __construct($type=null, $mode = null){

 if(isset($type)){  
    switch ($type) {
      case 'DataFactory':
         $this->buildDataFactoryContainer($mode);     
        break;
      case 'DbConnect':
         $this->buildDbConnectContainer($mode);  
        break;
     default:
        return false;
    }
  }
}

The method signature for container object creation is as follows:

public function buildDataFactoryContainer($mode=null)

The idea being that I can set $mode to test when calling this container, and have it load test values instead of actual runtime settings. I wanted to avoid writing separate container classes for testing, and this is an easy way I thought to not have test related code all over.

I could instead have subclassed ContainerFactory ie: ContainerFactoryTesting extends ContainerFactory and override in that instead of mixing test code with app code and cluttering method signatures with $mode=null, but that is not the point of this post. Moving on, to create a container for a particular object, I simply do this:

 // returns container with DataFactory dependencies, holds $db and $logger objects.
 $dataFactoryContainer = new ContainerFactory('DataFactory');

// returns container with test settings.
$dataFactoryTestContainer = new ContainerFactory('DataFactory','test');

// returns container with DbConnect dependencies, holds dbconfig and $logger objects.
$dbConnectContainer = new ContainerFactory('DbConnect');

I just ran into an issue that makes me suspect that the strategy I am building upon is flawed.

Looking at the above, DataFactory contains $db object that holds database connections.
I am now refactoring out this dbclass to remove its dependencies on a $registry object,
but how will I create $dataFactoryContainer, when I add $db object that needs $dbConnectContainer?

For example, in datafactory container I add a dbconnect instance, but IT will now need a container passed to it ...

I realise my English is not that great and hope I have explained well enough for a fellow SO'er to understand.

My question is two-fold, how do you guys handle creating objects for dependencies that themselves contain dependencies, in a simple manner?

And .. how do you separate container configuration for creation of objects for testing purposes?

As always, any comment or link to relevant post appreciated.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

孤千羽 2024-11-17 09:40:55

您不应该为所有内容创建单独的容器,而应使用单个容器。您可以创建一个容器“扩展”,它基本上只是在容器上设置服务。

这是我建议的设置的广泛示例。当您只有一个容器时,递归解决依赖关系是微不足道的。如您所见,security.authentication_provider 依赖于db,而db 又依赖于db.config

由于闭包的惰性,很容易定义服务,然后再定义它们的配置。此外,覆盖它们也很容易,正如您在 TestExtension 中看到的那样。

可以将您的服务定义拆分为多个单独的扩展,以使它们更具可重用性。这几乎就是 Silex 微框架 所做的事情(它使用 pimple)。

我希望这能回答您的问题。

ExtensionInterface

class ExtensionInterface
{
    function register(Pimple $container);
}

AppExtension

class AppExtension
{
    public function register(Pimple $container)
    {
        $container['mailer'] = $container->share(function ($container) {
            return new Mailer($container['mailer.config']);
        });

        $container['db'] = $container->share(function ($container) {
            return new DatabaseConnection($container['db.config']);
        });

        $container['security.authentication_provider'] = $container->share(function ($container) {
            return new DatabaseAuthenticationProvider($container['db']);
        });
    }
}

ConfigExtension

class ConfigExtension
{
    private $config;

    public function __construct($configFile)
    {
        $this->config = json_decode(file_get_contents($configFile), true);
    }

    public function register(Pimple $container)
    {
        foreach ($this->config as $name => $value) {
            $container[$name] = $container->protect($value);
        }
    }
}

config.json

{
    "mailer.config": {
        "username": "something",
        "password": "secret",
        "method":   "smtp"
    },
    "db.config": {
        "username": "root",
        "password": "secret"
    }
}

TestExtension

class TestExtension
{
    public function register(Pimple $container)
    {
        $container['mailer'] = function () {
            return new MockMailer();
        };
    }
}

ContainerFactory

class ContainerFactory
{
    public function create($configDir)
    {
        $container = new Pimple();

        $extension = new AppExtension();
        $extension->register($container);

        $extension = new ConfigExtension($configDir.'/config.json');
        $extension->register($container);

        return $container;
    }
}

应用程序

$factory = new ContainerFactory();
$container = $factory->create();

测试引导程序

$factory = new ContainerFactory();
$container = $factory->create();

$extension = new TestExtension();
$extension->register($container);

You should not create separate containers for everything, but instead use a single container. You can create a container "Extension" which basically just sets services on your container.

Here is an extensive example of my suggested setup. When you have a single container, recursively resolving dependencies is trivial. As you can see, the security.authentication_provider depends on db, which depends on db.config.

Due to the laziness of closures it's easy to define services and then define their configuration later. Additionally it's also easy to override them, as you can see with the TestExtension.

It would be possible to split your service definitions into multiple separate extensions to make them more reusable. That is pretty much what the Silex microframework does (it uses pimple).

I hope this answers your questions.

ExtensionInterface

class ExtensionInterface
{
    function register(Pimple $container);
}

AppExtension

class AppExtension
{
    public function register(Pimple $container)
    {
        $container['mailer'] = $container->share(function ($container) {
            return new Mailer($container['mailer.config']);
        });

        $container['db'] = $container->share(function ($container) {
            return new DatabaseConnection($container['db.config']);
        });

        $container['security.authentication_provider'] = $container->share(function ($container) {
            return new DatabaseAuthenticationProvider($container['db']);
        });
    }
}

ConfigExtension

class ConfigExtension
{
    private $config;

    public function __construct($configFile)
    {
        $this->config = json_decode(file_get_contents($configFile), true);
    }

    public function register(Pimple $container)
    {
        foreach ($this->config as $name => $value) {
            $container[$name] = $container->protect($value);
        }
    }
}

config.json

{
    "mailer.config": {
        "username": "something",
        "password": "secret",
        "method":   "smtp"
    },
    "db.config": {
        "username": "root",
        "password": "secret"
    }
}

TestExtension

class TestExtension
{
    public function register(Pimple $container)
    {
        $container['mailer'] = function () {
            return new MockMailer();
        };
    }
}

ContainerFactory

class ContainerFactory
{
    public function create($configDir)
    {
        $container = new Pimple();

        $extension = new AppExtension();
        $extension->register($container);

        $extension = new ConfigExtension($configDir.'/config.json');
        $extension->register($container);

        return $container;
    }
}

Application

$factory = new ContainerFactory();
$container = $factory->create();

Test Bootstrap

$factory = new ContainerFactory();
$container = $factory->create();

$extension = new TestExtension();
$extension->register($container);
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文