6 关注者

依赖注入容器

依赖注入 (DI) 容器是一个对象,它知道如何实例化和配置对象及其所有依赖对象。Martin Fowler的文章很好地解释了为什么DI容器很有用。在这里,我们将主要解释Yii提供的DI容器的用法。

依赖注入

Yii通过类yii\di\Container提供DI容器功能。它支持以下几种依赖注入

  • 构造函数注入;
  • 方法注入;
  • Setter和属性注入;
  • PHP可调用注入;

构造函数注入

DI容器借助构造函数参数的类型提示支持构造函数注入。类型提示告诉容器在用于创建新对象时依赖哪些类或接口。容器将尝试获取依赖类或接口的实例,然后通过构造函数将它们注入到新对象中。例如,

class Foo
{
    public function __construct(Bar $bar)
    {
    }
}

$foo = $container->get('Foo');
// which is equivalent to the following:
$bar = new Bar;
$foo = new Foo($bar);

方法注入

通常,类的依赖项传递给构造函数,并在整个生命周期内在类中可用。使用方法注入,可以提供仅由类的单个方法需要的依赖项,并且将其传递给构造函数可能不可行,或者在大多数用例中会导致过多的开销。

类方法可以像以下示例中的doSomething()方法一样定义

class MyClass extends \yii\base\Component
{
    public function __construct(/*Some lightweight dependencies here*/, $config = [])
    {
        // ...
    }

    public function doSomething($param1, \my\heavy\Dependency $something)
    {
        // do something with $something
    }
}

您可以通过自己传递\my\heavy\Dependency的实例或使用yii\di\Container::invoke()来调用该方法,如下所示

$obj = new MyClass(/*...*/);
Yii::$container->invoke([$obj, 'doSomething'], ['param1' => 42]); // $something will be provided by the DI container

Setter和属性注入

Setter和属性注入通过配置支持。在注册依赖项或创建新对象时,您可以提供一个配置,容器将使用该配置通过相应的setter或属性注入依赖项。例如,

use yii\base\BaseObject;

class Foo extends BaseObject
{
    public $bar;

    private $_qux;

    public function getQux()
    {
        return $this->_qux;
    }

    public function setQux(Qux $qux)
    {
        $this->_qux = $qux;
    }
}

$container->get('Foo', [], [
    'bar' => $container->get('Bar'),
    'qux' => $container->get('Qux'),
]);

信息:yii\di\Container::get()方法将其第三个参数作为应应用于正在创建的对象的配置数组。如果类实现了yii\base\Configurable接口(例如yii\base\BaseObject),则配置数组将作为最后一个参数传递给类构造函数;否则,配置将在对象创建之后应用。

PHP可调用注入

在这种情况下,容器将使用注册的PHP可调用函数来构建类的新的实例。每次调用yii\di\Container::get()时,都会调用相应的可调用函数。可调用函数负责解析依赖项并将其适当地注入到新创建的对象中。例如,

$container->set('Foo', function ($container, $params, $config) {
    $foo = new Foo(new Bar);
    // ... other initializations ...
    return $foo;
});

$foo = $container->get('Foo');

为了隐藏构建新对象的复杂逻辑,您可以使用静态类方法作为可调用对象。例如,

class FooBuilder
{
    public static function build($container, $params, $config)
    {
        $foo = new Foo(new Bar);
        // ... other initializations ...
        return $foo;
    }
}

$container->set('Foo', ['app\helper\FooBuilder', 'build']);

$foo = $container->get('Foo');

通过这样做,想要配置Foo类的人就不需要知道它是如何构建的了。

注册依赖项

您可以使用 yii\di\Container::set() 来注册依赖项。注册需要一个依赖项名称以及一个依赖项定义。依赖项名称可以是类名、接口名或别名;依赖项定义可以是类名、配置数组或 PHP 可调用对象。

$container = new \yii\di\Container;

// register a class name as is. This can be skipped.
$container->set('yii\db\Connection');

// register an interface
// When a class depends on the interface, the corresponding class
// will be instantiated as the dependent object
$container->set('yii\mail\MailInterface', 'yii\symfonymailer\Mailer');

// register an alias name. You can use $container->get('foo')
// to create an instance of Connection
$container->set('foo', 'yii\db\Connection');

// register an alias with `Instance::of`
$container->set('bar', Instance::of('foo'));

// register a class with configuration. The configuration
// will be applied when the class is instantiated by get()
$container->set('yii\db\Connection', [
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// register an alias name with class configuration
// In this case, a "class" or "__class" element is required to specify the class
$container->set('db', [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// register callable closure or array
// The callable will be executed each time when $container->get('db') is called
$container->set('db', function ($container, $params, $config) {
    return new \yii\db\Connection($config);
});
$container->set('db', ['app\db\DbFactory', 'create']);

// register a component instance
// $container->get('pageCache') will return the same instance each time it is called
$container->set('pageCache', new FileCache);

提示:如果依赖项名称与对应的依赖项定义相同,则无需使用 DI 容器注册它。

通过 set() 注册的依赖项将在每次需要依赖项时生成一个实例。您可以使用 yii\di\Container::setSingleton() 注册仅生成单个实例的依赖项

$container->setSingleton('yii\db\Connection', [
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

解析依赖项

注册完依赖项后,您可以使用 DI 容器创建新对象,容器将自动通过实例化它们并将它们注入到新创建的对象中来解析依赖项。依赖项解析是递归的,这意味着如果依赖项具有其他依赖项,这些依赖项也将自动解析。

您可以使用 get() 创建或获取对象实例。该方法接受一个依赖项名称,该名称可以是类名、接口名或别名。依赖项名称可以通过 set()setSingleton() 注册。您可以选择提供类构造函数参数列表和 配置 来配置新创建的对象。

例如

// "db" is a previously registered alias name
$db = $container->get('db');

// equivalent to: $engine = new \app\components\SearchEngine($apiKey, $apiSecret, ['type' => 1]);
$engine = $container->get('app\components\SearchEngine', [$apiKey, $apiSecret], ['type' => 1]);

// equivalent to: $api = new \app\components\Api($host, $apiKey);
$api = $container->get('app\components\Api', ['host' => $host, 'apiKey' => $apiKey]);

在幕后,DI 容器所做的工作远不止创建新对象。容器将首先检查类构造函数以找出依赖的类或接口名称,然后自动递归地解析这些依赖项。

以下代码显示了一个更复杂的示例。UserLister 类依赖于实现 UserFinderInterface 接口的对象;UserFinder 类实现了此接口并依赖于 Connection 对象。所有这些依赖项都是通过类构造函数参数的类型提示来声明的。通过正确的依赖项注册,DI 容器能够自动解析这些依赖项并通过简单的 get('userLister') 调用创建一个新的 UserLister 实例。

namespace app\models;

use yii\base\BaseObject;
use yii\db\Connection;
use yii\di\Container;

interface UserFinderInterface
{
    function findUser();
}

class UserFinder extends BaseObject implements UserFinderInterface
{
    public $db;

    public function __construct(Connection $db, $config = [])
    {
        $this->db = $db;
        parent::__construct($config);
    }

    public function findUser()
    {
    }
}

class UserLister extends BaseObject
{
    public $finder;

    public function __construct(UserFinderInterface $finder, $config = [])
    {
        $this->finder = $finder;
        parent::__construct($config);
    }
}

$container = new Container;
$container->set('yii\db\Connection', [
    'dsn' => '...',
]);
$container->set('app\models\UserFinderInterface', [
    'class' => 'app\models\UserFinder',
]);
$container->set('userLister', 'app\models\UserLister');

$lister = $container->get('userLister');

// which is equivalent to:

$db = new \yii\db\Connection(['dsn' => '...']);
$finder = new UserFinder($db);
$lister = new UserLister($finder);

实际用法

当您在应用程序的 入口脚本 中包含 Yii.php 文件时,Yii 会创建一个 DI 容器。可以通过 Yii::$container 访问 DI 容器。当您调用 Yii::createObject() 时,该方法实际上会调用容器的 get() 方法来创建一个新对象。如前所述,DI 容器会自动解析依赖项(如果有)并将它们注入到获取的对象中。因为 Yii 在其大部分核心代码中使用 Yii::createObject() 来创建新对象,这意味着您可以通过处理 Yii::$container 来全局自定义对象。

例如,让我们全局自定义 yii\widgets\LinkPager 的默认分页按钮数量。

\Yii::$container->set('yii\widgets\LinkPager', ['maxButtonCount' => 5]);

现在,如果您在视图中使用以下代码使用该小部件,则 maxButtonCount 属性将初始化为 5,而不是类中定义的默认值 10。

echo \yii\widgets\LinkPager::widget();

不过,您仍然可以覆盖通过 DI 容器设置的值

echo \yii\widgets\LinkPager::widget(['maxButtonCount' => 20]);

注意:小部件调用中给定的属性将始终覆盖 DI 容器中的定义。即使您指定一个数组,例如 'options' => ['id' => 'mypager'] 这些也不会与其他选项合并,而是会替换它们。

另一个示例是利用 DI 容器的自动构造函数注入。假设您的控制器类依赖于其他一些对象,例如酒店预订服务。您可以通过构造函数参数声明依赖项,并让 DI 容器为您解析它。

namespace app\controllers;

use yii\web\Controller;
use app\components\BookingInterface;

class HotelController extends Controller
{
    protected $bookingService;

    public function __construct($id, $module, BookingInterface $bookingService, $config = [])
    {
        $this->bookingService = $bookingService;
        parent::__construct($id, $module, $config);
    }
}

如果您从浏览器访问此控制器,您将看到一个错误,抱怨无法实例化 BookingInterface。这是因为您需要告诉 DI 容器如何处理此依赖项

\Yii::$container->set('app\components\BookingInterface', 'app\components\BookingService');

现在,如果您再次访问控制器,将创建一个 app\components\BookingService 实例并将其作为控制器构造函数的第三个参数注入。

从 Yii 2.0.36 开始,当使用 PHP 7 时,操作注入可用于 Web 和控制台控制器

namespace app\controllers;

use yii\web\Controller;
use app\components\BookingInterface;

class HotelController extends Controller
{    
    public function actionBook($id, BookingInterface $bookingService)
    {
        $result = $bookingService->book($id);
        // ...    
    }
}

高级实际用法

假设我们在开发 API 应用程序,并且有

  • app\components\Request 类扩展了 yii\web\Request 并提供了其他功能
  • app\components\Response 类扩展了 yii\web\Response,并且在创建时应将其 format 属性设置为 json
  • app\storage\FileStorageapp\storage\DocumentsReader 类实现了一些处理位于某些文件存储中的文档的逻辑

    class FileStorage
    {
        public function __construct($root) {
            // whatever
        }
    }
      
    class DocumentsReader
    {
        public function __construct(FileStorage $fs) {
            // whatever
        }
    }
    

可以一次配置多个定义,将配置数组传递给 setDefinitions()setSingletons() 方法。这些方法会遍历配置数组,分别为每个项目调用 set()setSingleton()

配置数组格式为

  • key:类名、接口名或别名。该键将作为第一个参数 $class 传递给 set() 方法。
  • value:与 $class 关联的定义。可能的取值在 set() 文档中针对 $definition 参数进行了描述。将作为第二个参数 $definition 传递给 set() 方法。

例如,让我们配置我们的容器以遵循上述要求

$container->setDefinitions([
    'yii\web\Request' => 'app\components\Request',
    'yii\web\Response' => [
        'class' => 'app\components\Response',
        'format' => 'json'
    ],
    'app\storage\DocumentsReader' => function ($container, $params, $config) {
        $fs = new app\storage\FileStorage('/var/tempfiles');
        return new app\storage\DocumentsReader($fs);
    }
]);

$reader = $container->get('app\storage\DocumentsReader'); 
// Will create DocumentReader object with its dependencies as described in the config 

提示:从 2.0.11 版本开始,可以使用应用程序配置以声明方式配置容器。请查看 配置 指南文章的 应用程序配置 小节。

一切正常,但是如果我们需要创建 DocumentWriter 类,我们将复制粘贴创建 FileStorage 对象的行,这显然不是最聪明的方法。

解析依赖项 小节所述,set()setSingleton() 可以选择将依赖项的构造函数参数作为第三个参数。要设置构造函数参数,可以使用 __construct() 选项

让我们修改我们的示例

$container->setDefinitions([
    'tempFileStorage' => [ // we've created an alias for convenience
        'class' => 'app\storage\FileStorage',
        '__construct()' => ['/var/tempfiles'], // could be extracted from some config files
    ],
    'app\storage\DocumentsReader' => [
        'class' => 'app\storage\DocumentsReader',
        '__construct()' => [Instance::of('tempFileStorage')],
    ],
    'app\storage\DocumentsWriter' => [
        'class' => 'app\storage\DocumentsWriter',
        '__construct()' => [Instance::of('tempFileStorage')]
    ]
]);

$reader = $container->get('app\storage\DocumentsReader'); 
// Will behave exactly the same as in the previous example.

您可能会注意到 Instance::of('tempFileStorage') 表示法。这意味着 容器 将隐式提供一个使用 tempFileStorage 名称注册的依赖项,并将其作为 app\storage\DocumentsWriter 构造函数的第一个参数传递。

注意:setDefinitions()setSingletons() 方法自 2.0.11 版本起可用。

配置优化的另一个步骤是将某些依赖项注册为单例。通过 set() 注册的依赖项将在每次需要时实例化。某些类在运行时不会更改状态,因此可以将其注册为单例以提高应用程序性能。

一个很好的例子可能是 app\storage\FileStorage 类,它使用简单的 API(例如 $fs->read()$fs->write())对文件系统执行一些操作。这些操作不会更改内部类状态,因此我们可以创建其实例一次并多次使用它。

$container->setSingletons([
    'tempFileStorage' => [
        'class' => 'app\storage\FileStorage',
        '__construct()' => ['/var/tempfiles']
    ],
]);

$container->setDefinitions([
    'app\storage\DocumentsReader' => [
        'class' => 'app\storage\DocumentsReader',
        '__construct()' => [Instance::of('tempFileStorage')],
    ],
    'app\storage\DocumentsWriter' => [
        'class' => 'app\storage\DocumentsWriter',
        '__construct()' => [Instance::of('tempFileStorage')],
    ]
]);

$reader = $container->get('app\storage\DocumentsReader');

何时注册依赖项

因为在创建新对象时需要依赖项,所以应尽早注册它们。以下是推荐的做法

  • 如果您是应用程序的开发人员,您可以使用应用程序配置注册您的依赖项。请阅读 配置 指南文章的 应用程序配置 小节。
  • 如果您是可再发行 扩展 的开发人员,您可以在扩展的引导类中注册依赖项。

总结

依赖注入和 服务定位器 都是流行的设计模式,允许以松耦合和更易于测试的方式构建软件。我们强烈建议您阅读 Martin 的文章 以更深入地了解依赖注入和服务定位器。

Yii 在依赖注入 (DI) 容器之上实现了其 服务定位器。当服务定位器尝试创建新的对象实例时,它会将调用转发到 DI 容器。后者将如上所述自动解析依赖项。

发现错别字或您认为此页面需要改进?
在 github 上编辑它 !