【问题标题】:Is it good practice to have DI container replace a global $registry object?让 DI 容器替换全局 $registry 对象是一种好习惯吗?
【发布时间】:2026-01-17 10:45:01
【问题描述】:

我已经开始重构一个小型应用程序以使用小型 DI 容器而不是 $registry::getstuff();在我的类中调用我将它们注入容器中。

这引发了 2 个问题,

Q1 -> 我扩展Pimple DI class 并创建一个容器,该容器具有特定于每个需要 DI 的对象的依赖项。然后我提供对象the whole shebang,并在构造函数中将其解构,将DI 的对象分配给我正在构建的对象的类属性。

我应该在 new object() 调用中分离对象吗?我只是发现这样更容易,但看到我现在是一个单人团队,我只想确认我有正确的方法。

Q2 -> 如果我在几个主要类上执行此操作,我发现我到处传递的 $registry 对象将不需要,这是使用 DI 的正常结果,没有更多的注册表?我可能在容器中注入了一个或两个单例,但看起来这就是我所需要的,即使是那些也可以很容易地消除,因为 DI 有一个 share() 属性,它返回对象的相同实例,有效地消除了需要对于单身人士。这是摆脱应用程序需要注册表/单例的方法吗,因为如果是这样的话,这很容易。

【问题讨论】:

标签: php unit-testing dependency-injection


【解决方案1】:

第二季度: 如果您在您的 $registry 对象中到处传递......那么您的 Registry 并不是真正的 Registry(正如 Fowler 所描述的那样)。 p>

Registry 或多或少是一个具有 get/set 方法的全局对象(“众所周知”)。 在 PHP 中,实现 Registry 的两个常见原型是

单身

class RegistryAsSingleton
{
    public static function getInstance (){
       //the singleton part
    }

    public function getStuff ()
    {
       //some stuff accessed thanks to the registry
    }
}

到处都是静态方法

class RegistryAsStatic
{
    public static function getStuff()
    {
    }
}

到处传递你的Registry使它,好吧,只是一个对象:一个没有比提供对其他对象的引用更大的目的的容器。

您的 DI 容器(使用您在 OP 中建议的 Pimple)本身就是一种注册表:它是众所周知的,使您能够从任何地方获取组件。

所以是的,我们可以说您的 DI 容器将通过执行相同的功能来消除注册表的要求和必要性。

但是(总有一个但是)

注册表总是有罪的,直到 被证明是无辜的 (马丁·福勒)

如果您使用 DI 容器 来替换您的 Registry,这可能是错误的。

例如:

//probably a Wrong usage of Registry
class NeedsRegistry
{
    public function asAParameter(Registry $pRegistry)
    {
       //Wrong dependency on registry where dependency is on Connection
       $ct = $pRegistry->getConnection();
    }

    public function asDirectAccess ()
    {
       //same mistake, more obvious as we can't use another component
       $ct = Registry::getInstance()->getConnection();
    }
}

//probably a wrong replacement for Registry using DI Container
class NeedsContainer
{
    public function asAParameter(Container $pRegistry)
    {
       //We are dependent to the container with no needs, 
       //this code should be dependent on Connection
       $ct = $pContainer->getConnection();
    }

    public function asDirectAccess ()
    {
       //should not be dependent on container
       $ct = Container::getInstance()->getConnection();
    }
}

为什么会这样?因为你的代码依赖程度不亚于以前,它仍然依赖于一个没有提供明确目标的组件(注册表或容器)(我们可以在这里想到接口)

注册表模式在某些情况下很有用,因为它是一种定义组件或数据(例如全局配置)的简单且相当便宜的方法。

通过删除依赖项来重构上述示例而不依赖 DI 的方法是:

class WasNeedingARegistry
{
    public function asAParameter (Connection $pConnection)
    {
       $pConnection->doStuff();//The real dependency here, we don't care for 
       //a global registry
    }
}

//the client code would be like
$wasNeedingARegistry = new WasNeedingARegistry();
$wasNeedingARegistry->setConnection($connection);

当然,如果客户端代码不知道连接,这可能是不可能的,这可能是您可能首先结束使用注册表的原因。

现在 DI 开始发挥作用

使用 DI 让我们的生活变得更美好,因为它会处理依赖项并使我们能够以随时可用的状态访问依赖项。

您将在代码中的某处配置组件:

$container['connection'] = function ($container) {
    return new Connection('configuration');
};
$container['neededARegistry'] = function ($container) {
    $neededARegistry = new NeededARegistry();
    $neededARegistry->setConnection($container['connection']);
    return $neededARegistry;
};

现在您拥有重构代码所需的一切:

// probably a better design pattern for using a Registry 
class NeededARegistry
{
    public function setConnection(Connection $pConnection)
    {
       $this->connection = $pConnection;
       return $this;
    }

    public function previouslyAsDirectAccess ()
    {
       $this->connection->doStuff();
    }
}

//and the client code just needs to know about the DI container
$container['neededARegistry']->previouslyAsDirectAccess();

“客户端”代码应尽可能隔离。客户端应该负责并注入自己的依赖项(通过set- 方法)。客户端不应该负责处理其依赖项的依赖项。

class WrongClientCode
{
    private $connection;
    public function setConnection(Connection $pConnection)
    {
       $this->connection = $pConnection;
    }

    public function callService ()
    {
       //for the demo we use a factory here
       ServiceFactory::create('SomeId')
                       ->setConnection($this->connection)
                       ->call();
       //here, connection was propagated on the solely 
       // purpose of being passed to the Service
    }
}

class GoodClientCode
{
    private $service;
    public function setService(Service $pService)
    {
       //the only dependency is on Service, no more connection
       $this->service = $pService;
    }

    public function callService ()
    {
       $this->service->setConnection($this->connection)
                     ->call();
    }
}

DI 容器将使用已正确配置其 Connection 的 Service 配置 GoodClientCode

至于 Singleton 方面,是的,它可以让你摆脱它们。 希望这会有所帮助

【讨论】:

  • 嘿。感谢您的精彩反馈。你已经把很多观点说得更清楚了,非常感谢。
  • 非常好,详尽的解释。当我的同事不可避免地问“但我们不能只传递容器吗?”时,我肯定会传递一个指向这个答案的链接。他们非常喜欢 Zend 接受“配置参数”数组的倾向,在很多情况下这些参数应该被命名为参数。
  • 很好的解释。我喜欢在 Stack 上看到坏代码/好代码示例。快速提问:在典型的 MVC 框架中,您将 DI 容器存储在哪里?您是否只有 1 个定义了您的所有服务/组件?还是你有很多?
  • 感谢您的解释,我现在正在尝试理解 DI 容器,这对我很有帮助。
  • 这一行在上一节课中真的正确吗? ` $this->service->setConnection($this->connection)` 为什么GoodClientCode有this->connection