yii2.0的依赖注入器

 提示:转载请注明原文链接

 本文永久链接:https://360us.net/article/19.html

依赖注入英文全称是dependency injection,简称DI。它还有个名字叫控制反转(Inversion of Control),英文简称IOC。这些都是来自于Java的东西。

yii的依赖注入器就是一个知道如何去实例化和配置对象及其依赖对象的一个对象。


依赖注入

yii是通过类yii\di\Container来提供依赖注入特性的。它支持如下种类的依赖注入方式:

  • 构造方法注入。

  • Setter和属性注入。

  • 回调函数注入。


构造方法注入

通过构造函数参数类型提示注入。当创建一个新对象的时候,类型提示可以告诉注入器那些类或者接口是它所依赖的,注入器会尝试去得到被依赖类或者接口的实例,然后通过构造函数注入到新的对象里面去。

例如:

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

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

// 等价于下面的代码:
$bar = new Bar;
$foo = new Foo($bar);


Setter和Property注入

setterproperty注入是通过Configurations来实现的。

当注册依赖项,或者创建新对象时,可以提供一些配置给注入器使用,注入器会通过相应的setters或者properties把依赖注入。

例如:

use yii\base\Object;

class Foo extends Object
{
    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'),
]);


回调函数注入

这节,注入器会使用一个已经注册的PHP回调函数来创建一个类的实例。

这个回调函数负责解决依赖关系,并注入到新创建的对象里面。

例如:

$container->set('Foo', function () {
    return new Foo(new Bar);
});

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


注册依赖关系

你可以通过yii\di\Container::set() 去注册依赖关系。

注册的时候需要一个依赖名和对应的依赖定义。

依赖名可以是类名、接口名、或者是别名;依赖定义可以是类名、配置数组或者是PHP回调函数。

例如:

$container = new \yii\di\Container;

//类名注册,这可以跳过
$container->set('yii\db\Connection');

// 注册一个接口
//当一个类依赖这个接口时,对应的类将会实例化它
$container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');

//这侧一个别名,可以通过 $container->get('foo')来创建Connection的实例
$container->set('foo', 'yii\db\Connection');

//通过配置来注册类
//当通过get()实例化这个类的时候,配置将会应用
$container->set('yii\db\Connection', [
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8mb4',
]);

//用配置注册一个别名
//这种情况下,class项必须指定一个类
$container->set('db', [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8mb4',
]);

//注册PHP回调
//每次调用$container->get('db')时,回调都会被调用
$container->set('db', function ($container, $params, $config) {
    return new \yii\db\Connection($config);
});

//注册组件实例
//每次调用$container->get('pageCache')时,会返回同一个实例。
$container->set('pageCache', new FileCache);

提示:假如依赖名和对应的依赖定义相同,不必使用依赖注入器。

通过set()注册的依赖关系,每次都会生成一个实例。

可以通过yii\di\Container::setSingleton()来注册依赖关系,这样每次就只会生成一个单例:

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


解析依赖关系

一旦你注册了依赖关系,可以通过依赖注入器来创建对象,注入器会自动解析依赖关系,并注入到新的对象里。

如果依赖关系是递归的,即一个依赖还依赖其他的东西,这些也会被自动解析。

你可以使用yii\di\Container::get()去创建新对象。

这个方法需要一个依赖名,你可以提供类构造函数的参数和配置给新创建的对象。

例如:

// "db"是预先注册好的别名
$db = $container->get('db');

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

注入器会先检查类构造函数,找出依赖的类或接口,然后会自动递归的解析这些依赖。

下面是一个更复杂的例子。

UserLister类依赖于一个实现UserFinderInterface接口的对象;UserFinder类实现了这个接口但是它又依赖于Connection对象。

所有这些依赖关系都是通过类构造函数的类型提示来声明的。

namespace app\models;

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

interface UserFinderInterface
{
    function findUser();
}

class UserFinder extends Object implements UserFinderInterface
{
    public $db;

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

    public function findUser()
    {
    }
}

class UserLister extends Object
{
    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');

// 等价于以下代码:
$db = new \yii\db\Connection(['dsn' => '...']);
$finder = new UserFinder($db);
$lister = new UserLister($finder);


实践

在应用的入口包含Yii.php的时候,Yii已经创建了一个DI容器。这个DI容器可以通过Yii::$container来访问。

当你调用Yii::createObject()创建对象的时候,这个方法实际上调用的是容器的get()方法。

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容器的自动构造函数。

假如你的控制器依赖于一些其他的对象,比如一个宾馆预订服务。

你可以通过构造函数参数去声明依赖关系。

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的一个实例会被创建,并作为控制器的第三个参数被注入。


何时注册依赖

当创建一个对象的时候,依赖是必须的,所以依赖的注册要比新建对象早。

以下是推荐的做法:

  • 假如你是一个应用的开发者,你可以在应用的入口或者在被入口文件包含的文件里面注册。

  • 假如你是一个扩展的开发者,你可以再扩展的启动类里面注册。


总结

依赖注入和服务定位都是可以创建松散耦合和可测试软件的流行的设计模式。

这里有一篇Martin 2004年写的文章,里面详细阐释了依赖注入和服务定位模式。

Yii也实现了服务定位器。当用服务定位器去创建对象的时候,它也会调用DI容器。


 评论
暂无评论