yii依赖注入和依赖注入容器

依赖注入和依赖注入容器

为了下降代码耦合程度,提升项目的可维护性,Yii采用多许多当下最流行又相对成熟的设计模式,包括了依赖注入(Denpdency Injection, DI)和服务定位器(Service Locator)两种模式。 关于依赖注入与服务定位器, Inversion of Control Containers and the Dependency Injection pattern 给出了很详细的讲解,这里结合Web应用和Yii具体实现进行探讨,以加深印象和理解。 这些设计模式对于提升自身的设计水平颇有帮助,这也是咱们学习Yii的一个重要出发点。php

有关概念

在了解Service Locator 和 Dependency Injection 以前,有必要先来了解一些高大上的概念。 别担忧,你只须要有个大体了解就OK了,若是展开来讲,这些东西能够单独写个研究报告:html

依赖倒置原则(Dependence Inversion Principle, DIP)
DIP是一种软件设计的指导思想。传统软件设计中,上层代码依赖于下层代码,当下层出现变更时, 上层代码也要相应变化,维护成本较高。而DIP的核心思想是上层定义接口,下层实现这个接口, 从而使得下层依赖于上层,下降耦合度,提升整个系统的弹性。这是一种经实践证实的有效策略。
控制反转(Inversion of Control, IoC)
IoC就是DIP的一种具体思路,DIP只是一种理念、思想,而IoC是一种实现DIP的方法。 IoC的核心是将类(上层)所依赖的单元(下层)的实例化过程交由第三方来实现。 一个简单的特征,就是类中不对所依赖的单元有诸如 $component = new yii\component\SomeClass() 的实例化语句。
依赖注入(Dependence Injection, DI)
DI是IoC的一种设计模式,是一种套路,按照DI的套路,就能够实现IoC,就能符合DIP原则。 DI的核心是把类所依赖的单元的实例化过程,放到类的外面去实现。
控制反转容器(IoC Container)
当项目比较大时,依赖关系可能会很复杂。 而IoC Container提供了动态地建立、注入依赖单元,映射依赖关系等功能,减小了许多代码量。 Yii 设计了一个 yii\di\Container 来实现了 DI Container。
服务定位器(Service Locator)
Service Locator是IoC的另外一种实现方式, 其核心是把全部可能用到的依赖单元交由Service Locator进行实例化和建立、配置, 把类对依赖单元的依赖,转换成类对Service Locator的依赖。 DI 与 Service Locator并不冲突,二者能够结合使用。 目前,Yii2.0把这DI和Service Locator这两个东西结合起来使用,或者说经过DI容器,实现了Service Locator。

是否是云里雾里的?没错,所谓“高大上”的玩意每每就是这样,看着很炫,很唬人。 卖护肤品的难道会跟你说其实皮肤表层是角质层,不具吸取功能么?这玩意又不考试,大体意会下就OK了。 万一哪天要在妹子面前要装一把范儿的时候,张口也能来这么几个“高大上”就好了。 但具体的内涵,咱们仍是要要经过下面的学习来加深理解,毕竟要把“高大上”的东西用好,发挥出做用来。mysql

依赖注入

首先讲讲DI。在Web应用中,很常见的是使用各类第三方Web Service实现特定的功能,好比发送邮件、推送微博等。 假设要实现当访客在博客上发表评论后,向博文的做者发送Email的功能,一般代码会是这样:sql

 

// 为邮件服务定义抽象层
interface EmailSenderInterface
{
    public function send(...);
}

// 定义Gmail邮件服务
class GmailSender implements EmailSenderInterface
{
    ...

    // 实现发送邮件的类方法
    public function send(...)
    {
        ...
    }
}

// 定义评论类
class Comment extend yii\db\ActiveRecord
{
    // 用于引用发送邮件的库
    private $_eMailSender;

    // 初始化时,实例化 $_eMailSender
    public function init()
    {
        ...
        // 这里假设使用Gmail的邮件服务
       $this->_eMailSender = GmailSender::getInstance();
        ...
    }

    // 当有新的评价,即 save() 方法被调用以后中,会触发如下方法
    public function afterInsert()
    {
        ...
        //
        $this->_eMailSender->send(...);
        ...
    }
}

 

 

上面的代码只是一个示意,大体是这么个流程。数据库

那么这种常见的设计方法有什么问题呢? 主要问题在于 Comment 对于 GmailSender 的依赖(对于EmailSenderInterface的依赖不可避免), 假设有一天忽然不使用Gmail提供的服务了,改用Yahoo或自建的邮件服务了。 那么,你不得不修改 Comment::init() 里面对 $_eMailSender 的实例化语句:swift

$this->_eMailSender = MyEmailSender::getInstance(); 

这个问题的本质在于,你今天写完这个Comment,只能用于这个项目,哪天你开发别的项目要实现相似的功能, 你还要针对新项目使用的邮件服务修改这个Comment。代码的复用性不高呀。 有什么办法能够不改变Comment的代码,就能扩展成对各类邮件服务都支持么? 换句话说,有办法将Comment和GmailSender解耦么?有办法提升Comment的普适性、复用性么?设计模式

依赖注入就是为了解决这个问题而生的,固然,DI也不是惟一解决问题的办法,毕竟条条大路通罗马。 Service Locator也是能够实现解耦的。数组

在Yii中使用DI解耦,有2种注入方式:构造函数注入、属性注入。缓存

构造函数注入

构造函数注入经过构造函数的形参,为类内部的抽象单元提供实例化。 具体的构造函数调用代码,由外部代码决定。具体例子以下:数据结构

// 这是构造函数注入的例子
class Comment extend yii\db\ActiveRecord
{
    // 用于引用发送邮件的库
    private $_eMailSender;

    // 构造函数注入
    public function __construct($emailSender)
    {
        ...
        $this->_eMailSender = $emailSender;
        ...
    }

    // 当有新的评价,即 save() 方法被调用以后中,会触发如下方法
    public function afterInsert()
    {
        ...
        //
        $this->_eMailSender->send(...);
        ...
    }
}

// 实例化两种不一样的邮件服务,固然,他们都实现了EmailSenderInterface
sender1 = new GmailSender();
sender2 = new MyEmailSender();

// 用构造函数将GmailSender注入
$comment1 = new Comment(sender1);
// 使用Gmail发送邮件
$comment1.save();

// 用构造函数将MyEmailSender注入
$comment2 = new Comment(sender2);
// 使用MyEmailSender发送邮件
$comment2.save();

 

 

 

上面的代码对比原来的代码,解决了Comment类对于GmailSender等具体类的依赖,经过构造函数,将相应的实现了 EmailSenderInterface接口的类实例传入Comment类中,使得Comment类能够适用于不一样的邮件服务。 今后之后,不管要使用何何种邮件服务,只需写出新的EmailSenderInterface实现便可, Comment类的代码再也不须要做任何更改,多爽的一件事,扩展起来、测试起来都省心省力。

属性注入

与构造函数注入相似,属性注入经过setter或public成员变量,将所依赖的单元注入到类内部。 具体的属性写入,由外部代码决定。具体例子以下:

// 这是属性注入的例子
class Comment extend yii\db\ActiveRecord
{
    // 用于引用发送邮件的库
    private $_eMailSender;

    // 定义了一个 setter()
    public function setEmailSender($value)
    {
        $this->_eMailSender = $value;
    }

    // 当有新的评价,即 save() 方法被调用以后中,会触发如下方法
    public function afterInsert()
    {
        ...
        //
        $this->_eMailSender->send(...);
        ...
    }
}

// 实例化两种不一样的邮件服务,固然,他们都实现了EmailSenderInterface
sender1 = new GmailSender();
sender2 = new MyEmailSender();

$comment1 = new Comment;
// 使用属性注入
$comment1->eMailSender = sender1;
// 使用Gmail发送邮件
$comment1.save();

$comment2 = new Comment;
// 使用属性注入
$comment2->eMailSender = sender2;
// 使用MyEmailSender发送邮件
$comment2.save();

上面的Comment若是将 private $_eMailSender 改为 public $eMailSender 并删除 setter函数, 也是能够达到一样的效果的。

与构造函数注入相似,属性注入也是将Comment类所依赖的EmailSenderInterface的实例化过程放在Comment类之外。 这就是依赖注入的本质所在。为何称为注入?从外面把东西打进去,就是注入。什么是外,什么是内? 要解除依赖的类内部就是内,实例化所依赖单元的地方就是外。

DI容器

从上面DI两种注入方式来看,依赖单元的实例化代码是一个重复、繁琐的过程。 能够想像,一个Web应用的某一组件会依赖于若干单元,这些单元又有可能依赖于更低层级的单元, 从而造成依赖嵌套的情形。那么,这些依赖单元的实例化、注入过程的代码可能会比较长,先后关系也须要特别地注意, 必须将被依赖的放在须要注入依赖的前面进行实例化。 这实在是一件既没技术含量,又吃力不出成果的工做,这类工做是高智商(懒)人群的天敌, 咱们是不会去作这么无聊的事情的。

就像极其不想洗衣服的人发明了洗衣机(我臆想的,未考证)同样,为了解决这一无聊的问题,DI容器被设计出来了。 Yii的DI容器是 yii\di\Container ,这个容器继承了发明人的高智商, 他知道如何对对象及对象的全部依赖,和这些依赖的依赖,进行实例化和配置。

DI容器中的内容

DI容器中实例的表示

容器顾名思义是用来装东西的,DI容器里面的东西是什么呢?Yii使用 yii\di\Instance 来表示容器中的东西。 固然Yii中还将这个类用于Service Locator,这个在讲Service Locator时再具体谈谈。

yii\di\Instance 本质上是DI容器中对于某一个类实例的引用,它的代码看起来并不复杂:

 

class Instance
{
    // 仅有的属性,用于保存类名、接口名或者别名
    public $id;

    // 构造函数,仅将传入的ID赋值给 $id 属性
    protected function __construct($id)
    {
    }

    // 静态方法建立一个Instance实例
    public static function of($id)
    {
        return new static($id);
    }

    // 静态方法,用于将引用解析成实际的对象,并确保这个对象的类型
    public static function ensure($reference, $type = null, $container = null)
    {
    }

    // 获取这个实例所引用的实际对象,事实上它调用的是
    // yii\di\Container::get()来获取实际对象
    public function get($container = null)
    {
    }
}

  

对于 yii\di\Instance ,咱们要了解:

  • 表示的是容器中的内容,表明的是对于实际对象的引用。
  • DI容器能够经过他获取所引用的实际对象。
  • 类仅有的一个属性 id 通常表示的是实例的类型。

DI容器的数据结构

在DI容器中,维护了5个数组,这是DI容器功能实现的基础:

 

// 用于保存单例Singleton对象,以对象类型为键
private $_singletons = [];

// 用于保存依赖的定义,以对象类型为键
private $_definitions = [];

// 用于保存构造函数的参数,以对象类型为键
private $_params = [];

// 用于缓存ReflectionClass对象,以类名或接口名为键
private $_reflections = [];

// 用于缓存依赖信息,以类名或接口名为键
private $_dependencies = [];

  

DI容器的5个数组内容和做用如 DI容器5个数组示意图 所示。

 

注册依赖

使用DI容器,首先要告诉容器,类型及类型之间的依赖关系,声明一这关系的过程称为注册依赖。 使用 yii\di\Container::set()yii\di\Container::setSinglton() 能够注册依赖。

DI容器是怎么管理依赖的呢?要先看看 yii\di\Container::set()yii\Container::setSinglton()

 

public function set($class, $definition = [], array $params = [])
{
    // 规范化 $definition 并写入 $_definitions[$class]
    $this->_definitions[$class] = $this->normalizeDefinition($class,
        $definition);

    // 将构造函数参数写入 $_params[$class]
    $this->_params[$class] = $params;

    // 删除$_singletons[$class]
    unset($this->_singletons[$class]);
    return $this;
}

public function setSingleton($class, $definition = [], array $params = [])
{
    // 规范化 $definition 并写入 $_definitions[$class]
    $this->_definitions[$class] = $this->normalizeDefinition($class,
        $definition);

    // 将构造函数参数写入 $_params[$class]
    $this->_params[$class] = $params;

    // 将$_singleton[$class]置为null,表示还未实例化
    $this->_singletons[$class] = null;
    return $this;
}

  

这两个函数功能相似没有太大区别,只是 set() 用于在每次请求时构造新的实例返回, 而 setSingleton() 只维护一个单例,每次请求时都返回同一对象。

表如今数据结构上,就是 set() 在注册依赖时,会把使用 setSingleton() 注册的依赖删除。 不然,在解析依赖时,你让Yii到底是依赖续弦仍是原配?所以,在DI容器中,依赖关系的定义是惟一的。 后定义的同名依赖,会覆盖前面定义好的依赖。

从形参来看,这两个函数的 $class 参数接受一个类名、接口名或一个别名,做为依赖的名称。 $definition 表示依赖的定义,能够是一个类名、配置数组或一个PHP callable。

这两个函数,本质上只是将依赖的有关信息写入到容器的相应数组中去。 在 set()setSingleton() 中,首先调用 yii\di\Container::normalizeDefinition() 对依赖的定义进行规范化处理,其代码以下:

protected function normalizeDefinition($class, $definition)
 {
     // $definition 是空的转换成 ['class' => $class] 形式
     if (empty($definition)) {
         return ['class' => $class];

     // $definition 是字符串,转换成 ['class' => $definition] 形式
     } elseif (is_string($definition)) {
         return ['class' => $definition];

     // $definition 是PHP callable 或对象,则直接将其做为依赖的定义
     } elseif (is_callable($definition, true) || is_object($definition)) {
         return $definition;

     // $definition 是数组则确保该数组定义了 class 元素
     } elseif (is_array($definition)) {
         if (!isset($definition['class'])) {
             if (strpos($class, '\\') !== false) {
                 $definition['class'] = $class;
             } else {
                 throw new InvalidConfigException(
                     "A class definition requires a \"class\" member.");
             }
         }
         return $definition;
     // 这也不是,那也不是,那就抛出异常算了
     } else {
         throw new InvalidConfigException(
             "Unsupported definition type for \"$class\": "
             . gettype($definition));
     }
 }

  

规范化处理的流程以下:

  • 若是 $definition 是空的,直接返回数组 ['class' => $class]
  • 若是 $definition 是字符串,那么认为这个字符串就是所依赖的类名、接口名或别名, 那么直接返回数组 ['class' => $definition]
  • 若是 $definition 是一个PHP callable,或是一个对象,那么直接返回该 $definition
  • 若是 $definition 是一个数组,那么其应当是一个包含了元素 $definition['class'] 的配置数组。 若是该数组未定义 $definition['class'] 那么,将传入的 $class 做为该元素的值,最后返回该数组。
  • 上一步中,若是 definition['class'] 未定义,而 $class 不是一个有效的类名,那么抛出异常。
  • 若是 $definition 不属于上述的各类状况,也抛出异常。

总之,对于 $_definitions 数组中的元素,它要么是一个包含了”class” 元素的数组,要么是一个PHP callable, 再要么就是一个具体对象。这就是规范化后的最终结果。

在调用 normalizeDefinition() 对依赖的定义进行规范化处理后, set()setSingleton() 以传入的 $class 为键,将定义保存进 $_definition[] 中, 将传入的 $param 保存进 $_params[] 中。

对于 set() 而言,还要删除 $_singleton[] 中的同名依赖。 对于 setSingleton() 而言,则要将 $_singleton[] 中的同名依赖设为 null , 表示定义了一个Singleton,可是并未实现化。

这么讲可能很差理解,举几个具体的依赖定义及相应数组的内容变化为例,以加深理解:

 

$container = new \yii\di\Container;

// 直接以类名注册一个依赖,虽然这么作没什么意义。
// $_definition['yii\db\Connection'] = 'yii\db\Connetcion'
$container->set('yii\db\Connection');

// 注册一个接口,当一个类依赖于该接口时,定义中的类会自动被实例化,并供
// 有依赖须要的类使用。
// $_definition['yii\mail\MailInterface', 'yii\swiftmailer\Mailer']
$container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');

// 注册一个别名,当调用$container->get('foo')时,能够获得一个
// yii\db\Connection 实例。
// $_definition['foo', 'yii\db\Connection']
$container->set('foo', 'yii\db\Connection');

// 用一个配置数组来注册一个类,须要这个类的实例时,这个配置数组会发生做用。
// $_definition['yii\db\Connection'] = [...]
$container->set('yii\db\Connection', [
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// 用一个配置数组来注册一个别名,因为别名的类型不详,所以配置数组中须要
// 有 class 元素。
// $_definition['db'] = [...]
$container->set('db', [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// 用一个PHP callable来注册一个别名,每次引用这个别名时,这个callable都会被调用。
// $_definition['db'] = function(...){...}
$container->set('db', function ($container, $params, $config) {
    return new \yii\db\Connection($config);
});

// 用一个对象来注册一个别名,每次引用这个别名时,这个对象都会被引用。
// $_definition['pageCache'] = anInstanceOfFileCache
$container->set('pageCache', new FileCache);

 

setSingleton() 对于 $_definition$_params 数组产生的影响与 set() 是同样同样的。 不一样之处在于,使用 set() 会unset $_singltons 中的对应元素,Yii认为既然你都调用 set() 了,说明你但愿这个依赖再也不是单例了。 而 setSingleton() 相比较于 set() ,会额外地将 $_singletons[$class] 置为 null 。 以此来表示这个依赖已经定义了一个单例,可是还没有实例化。

set()setSingleton() 来看, 可能还不容易理解DI容器,好比咱们说DI容器中维护了5个数组,可是依赖注册过程只涉及到其中3个。 剩下的 $_reflections$_dependencies 是在解析依赖的过程当中完成构建的。

从DI容器的5个数组来看也好,从容器定义了 set()setSingleton() 两个定义依赖的方法来看也好, 不难猜出DI容器中装了两类实例,一种是单例,每次向容器索取单例类型的实例时,获得的都是同一个实例; 另外一类是普通实例,每次向容器索要普通类型的实例时,容器会根据依赖信息建立一个新的实例给你。

单例类型主要用于节省构建实例的时间、节省保存实例的内存、共享数据等。而普通类型主要用于避免数据冲突。

对象的实例化

对象的实例化过程要比依赖的定义过程复杂得多。毕竟依赖的定义只是往特定的数据结构 $_singletons $_definitions$_params 3个数组写入有关的信息。 稍复杂的东西也就是定义的规范化处理了。其它真没什么复杂的。像你这么聪明的,确定以为这太没挑战了。

而对象的实例化过程要相对复杂,这一过程会涉及到复杂依赖关系的解析、涉及依赖单元的实例化等过程。 且让咱们抽丝剥茧地进行分析。

解析依赖信息

容器在获取实例以前,必须解析依赖信息。 这一过程会涉及到DI容器中还没有提到的另外2个数组 $_reflections$_dependenciesyii\di\Container::getDependencies() 会向这2个数组写入信息,而这个函数又会在建立实例时,由 yii\di\Container::build() 所调用。 如它的名字所示意的, yii\di\Container::getDependencies() 方法用于获取依赖信息,让咱们先来看看这个函数的代码

protected function getDependencies($class)
{
    // 若是已经缓存了其依赖信息,直接返回缓存中的依赖信息
    if (isset($this->_reflections[$class])) {
        return [$this->_reflections[$class], $this->_dependencies[$class]];
    }

    $dependencies = [];

    // 使用PHP5 的反射机制来获取类的有关信息,主要就是为了获取依赖信息
    $reflection = new ReflectionClass($class);

    // 经过类的构建函数的参数来了解这个类依赖于哪些单元
    $constructor = $reflection->getConstructor();
    if ($constructor !== null) {
        foreach ($constructor->getParameters() as $param) {
            if ($param->isDefaultValueAvailable()) {

                // 构造函数若是有默认值,将默认值做为依赖。即然是默认值了,
                // 就确定是简单类型了。
                $dependencies[] = $param->getDefaultValue();
            } else {
                $c = $param->getClass();

                // 构造函数没有默认值,则为其建立一个引用。
                // 就是前面提到的 Instance 类型。
                $dependencies[] = Instance::of($c === null ? null :
                    $c->getName());
            }
        }
    }

    // 将 ReflectionClass 对象缓存起来
    $this->_reflections[$class] = $reflection;

    // 将依赖信息缓存起来
    $this->_dependencies[$class] = $dependencies;

    return [$reflection, $dependencies];
}

  

前面讲了 $_reflections 数组用于缓存 ReflectionClass 实例,$_dependencies 数组用于缓存依赖信息。 这个 yii\di\Container::getDependencies() 方法实质上就是经过PHP5 的反射机制, 经过类的构造函数的参数分析他所依赖的单元。而后通通缓存起来备用。

为何是经过构造函数来分析其依赖的单元呢? 由于这个DI容器设计出来的目的就是为了实例化对象及该对象所依赖的一切单元。 也就是说,DI容器必然构造类的实例,必然调用构造函数,那么必然为构造函数准备并传入相应的依赖单元。 这也是咱们开头讲到的构造函数依赖注入的后续延伸应用。

可能有的读者会问,那不是还有setter注入么,为何不用解析setter注入函数的依赖呢? 这是由于要获取实例不必定须要为某属性注入外部依赖单元,可是却必须为其构造函数的参数准备依赖的外部单元。 固然,有时候一个用于注入的属性必须在实例化时指定依赖单元。 这个时候,必然在其构造函数中有一个用于接收外部依赖单元的形式参数。 使用DI容器的目的是自动实例化,只是实例化而已,就意味着只须要调用构造函数。 至于setter注入能够在实例化后操做嘛。

另外一个与解析依赖信息相关的方法就是 yii\di\Container::resolveDependencies() 。 它也是关乎 $_reflections$_dependencies 数组的,它使用 yii\di\Container::getDependencies() 在这两个数组中写入的缓存信息,做进一步具体化的处理。从函数名来看,他的名字代表是用于解析依赖信息的。 下面咱们来看看它的代码:

protected function resolveDependencies($dependencies, $reflection = null)
{
    foreach ($dependencies as $index => $dependency) {

        // 前面getDependencies() 函数往 $_dependencies[] 中
        // 写入的是一个 Instance 数组
        if ($dependency instanceof Instance) {
            if ($dependency->id !== null) {

                // 向容器索要所依赖的实例,递归调用 yii\di\Container::get()
                $dependencies[$index] = $this->get($dependency->id);
            } elseif ($reflection !== null) {
                $name = $reflection->getConstructor()
                    ->getParameters()[$index]->getName();
                $class = $reflection->getName();
                throw new InvalidConfigException(
                "Missing required parameter \"$name\" when instantiating \"$class\".");
            }
        }
    }
    return $dependencies;
}

 

 

上面的代码中能够看到, yii\di\Container::resolveDependencies() 做用在于处理依赖信息, 将依赖信息中保存的Istance实例所引用的类或接口进行实例化。

综合上面提到的 yii\di\Container::getDependencies()yii\di\Container::resolveDependencies() 两个方法,咱们能够了解到:

  • $_reflections 以类(接口、别名)名为键, 缓存了这个类(接口、别名)的ReflcetionClass。一经缓存,便不会再更改。
  • $_dependencies 以类(接口、别名)名为键,缓存了这个类(接口、别名)的依赖信息。
  • 这两个缓存数组都是在 yii\di\Container::getDependencies() 中完成。这个函数只是简单地向数组写入数据。
  • 通过 yii\di\Container::resolveDependencies() 处理,DI容器会将依赖信息转换成实例。 这个实例化的过程当中,是向容器索要实例。也就是说,有可能会引发递归。

实例的建立

解析完依赖信息,就万事俱备了,那么东风也该来了。实例的建立,秘密就在 yii\di\Container::build() 函数中

protected function build($class, $params, $config)
{
    // 调用上面提到的getDependencies来获取并缓存依赖信息,留意这里 list 的用法
    list ($reflection, $dependencies) = $this->getDependencies($class);

    // 用传入的 $params 的内容补充、覆盖到依赖信息中
    foreach ($params as $index => $param) {
        $dependencies[$index] = $param;
    }

    // 这个语句是两个条件:
    // 一是要建立的类是一个 yii\base\Object 类,
    // 留意咱们在《Yii基础》一篇中讲到,这个类对于构造函数的参数是有必定要求的。
    // 二是依赖信息不为空,也就是要么已经注册过依赖,
    // 要么为build() 传入构造函数参数。
    if (!empty($dependencies) && is_a($class, 'yii\base\Object', true)) {
        // 按照 Object 类的要求,构造函数的最后一个参数为 $config 数组
        $dependencies[count($dependencies) - 1] = $config;

        // 解析依赖信息,若是有依赖单元须要提早实例化,会在这一步完成
        $dependencies = $this->resolveDependencies($dependencies, $reflection);

        // 实例化这个对象
        return $reflection->newInstanceArgs($dependencies);
    } else {
        // 会出现异常的状况有二:
        // 一是依赖信息为空,也就是你前面又没注册过,
        // 如今又不提供构造函数参数,你让Yii怎么实例化?
        // 二是要构造的类,根本就不是 Object 类。
        $dependencies = $this->resolveDependencies($dependencies, $reflection);
        $object = $reflection->newInstanceArgs($dependencies);
        foreach ($config as $name => $value) {
            $object->$name = $value;
        }
        return $object;
    }
}

 

从这个 yii\di\Container::build() 来看:

  • DI容器只支持 yii\base\Object 类。也就是说,你只能向DI容器索要 yii\base\Object 及其子类。 再换句话说,若是你想你的类能够放在DI容器里,那么必须继承自 yii\base\Object 类。 但Yii中几乎开发者在开发过程当中须要用到的类,都是继承自这个类。 一个例外就是上面提到的 yii\di\Instance 类。但这个类是供Yii框架本身使用的,开发者无需操做这个类。
  • 递归获取依赖单元的依赖在于 dependencies = $this->resolveDependencies($dependencies, $reflection) 中。
  • getDependencies()resolveDependencies()build() 所用。 也就是说,只有在建立实例的过程当中,DI容器才会去解析依赖信息、缓存依赖信息。

容器内容实例化的大体过程

与注册依赖时使用 set()setSingleton() 对应,获取依赖实例化对象使用 yii\di\Container::get() ,其代码以下:

public function get($class, $params = [], $config = [])
{
    // 已经有一个完成实例化的单例,直接引用这个单例
    if (isset($this->_singletons[$class])) {
        return $this->_singletons[$class];

    // 是个还没有注册过的依赖,说明它不依赖其余单元,或者依赖信息不用定义,
    // 则根据传入的参数建立一个实例
    } elseif (!isset($this->_definitions[$class])) {
        return $this->build($class, $params, $config);
    }

    // 注意这里建立了 $_definitions[$class] 数组的副本
    $definition = $this->_definitions[$class];

    // 依赖的定义是个 PHP callable,调用之
    if (is_callable($definition, true)) {
        $params = $this->resolveDependencies($this->mergeParams($class,
            $params));
        $object = call_user_func($definition, $this, $params, $config);

    // 依赖的定义是个数组,合并相关的配置和参数,建立之
    } elseif (is_array($definition)) {
        $concrete = $definition['class'];
        unset($definition['class']);

        // 合并将依赖定义中配置数组和参数数组与传入的配置数组和参数数组合并
        $config = array_merge($definition, $config);
        $params = $this->mergeParams($class, $params);

        if ($concrete === $class) {
            // 这是递归终止的重要条件
            $object = $this->build($class, $params, $config);
        } else {
            // 这里实现了递归解析
            $object = $this->get($concrete, $params, $config);
        }

    // 依赖的定义是个对象则应当保存为单例
    } elseif (is_object($definition)) {
        return $this->_singletons[$class] = $definition;
    } else {
        throw new InvalidConfigException(
            "Unexpected object definition type: " . gettype($definition));
    }

    // 依赖的定义已经定义为单例的,应当实例化该对象
    if (array_key_exists($class, $this->_singletons)) {
        $this->_singletons[$class] = $object;
    }

    return $object;
}

get() 用于返回一个对象或一个别名所表明的对象。能够是已经注册好依赖的,也能够是没有注册过依赖的。 不管是哪一种状况,Yii均会自动解析将要获取的对象对外部的依赖。

get() 接受3个参数:

  • $class 表示将要建立或者获取的对象。能够是一个类名、接口名、别名。
  • $params 是一个用于这个要建立的对象的构造函数的参数,其参数顺序要与构造函数的定义一致。 一般用于未定义的依赖。
  • $config 是一个配置数组,用于配置获取的对象。一般用于未定义的依赖,或覆盖原来依赖中定义好的配置。

get() 解析依赖获取对象是一个自动递归的过程,也就是说,当要获取的对象依赖于其余对象时, Yii会自动获取这些对象及其所依赖的下层对象的实例。 同时,即便对于未定义的依赖,DI容器经过PHP的Reflection API,也能够自动解析出当前对象的依赖来。

get() 不直接实例化对象,也不直接解析依赖信息。而是经过 build() 来实例化对象和解析依赖。

get() 会根据依赖定义,递归调用自身去获取依赖单元。 所以,在整个实例化过程当中,一共有两个地方会产生递归:一是 get() , 二是 build() 中的 resolveDependencies()

DI容器解析依赖实例化对象过程大致上是这么一个流程:

  • 以传入的 $class 看看容器中是否已经有实例化好的单例,若有,直接返回这一单例。
  • 若是这个 $class 根本就未定义依赖,则调用 build() 建立之。具体建立过程等下再说。
  • 对于已经定义了这个依赖,若是定义为PHP callable,则解析依赖关系,并调用这个PHP callable。 具体依赖关系解析过程等下再说。
  • 若是依赖的定义是一个数组,首先取得定义中对于这个依赖的 class 的定义。 而后将定义中定义好的参数数组和配置数组与传入的参数数组和配置数组进行合并, 并判断是否达到终止递归的条件。从而选择继续递归解析依赖单元,或者直接建立依赖单元。

get() 的代码能够看出:

  • 对于已经实例化的单例,使用 get() 时只能返回已经实例化好的实例, $params 参数和 $config 参数失去做用。这点要注意,Yii不会提示你,所给出的参数不会发生做用的。 有的时候发现明明已经给定配置数组了,怎么配置不起做用呀?就要考虑是否是由于这个缘由了。
  • 对于定义为数组的依赖,在合并配置数组和构造函数参数数组过程当中, 定义中定义好的两个数组会被传入的 $config$params 的同名元素所覆盖, 这就提供了获取不一样实例的可能。
  • 在定义依赖时,不管是使用 set() 仍是使用 setSingleton() 只要依赖定义为特定对象或特定实例的, Yii均将其视为单例。在获取时,也将返回这一单例。

实例分析

为了加深理解,咱们以官方文档上的例子来讲明DI容器解析依赖的过程。假设有如下代码:

 

 

namespace app\models;

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

// 定义接口
interface UserFinderInterface
{
    function findUser();
}

// 定义类,实现接口
class UserFinder extends Object implements UserFinderInterface
{
    public $db;

    // 从构造函数看,这个类依赖于 Connection
    public function __construct(Connection $db, $config = [])
    {
        $this->db = $db;
        parent::__construct($config);
    }

    public function findUser()
    {
    }
}

class UserLister extends Object
{
    public $finder;

    // 从构造函数看,这个类依赖于 UserFinderInterface接口
    public function __construct(UserFinderInterface $finder, $config = [])
    {
        $this->finder = $finder;
        parent::__construct($config);
    }
}

从依赖关系看,这里的 UserLister 类依赖于接口 UserFinderInterface , 而接口有一个实现就是 UserFinder 类,但这类又依赖于 Connection

那么,按照通常常规的做法,要实例化一个 UserLister 一般这么作:

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

  

就是逆着依赖关系,从最底层的 Connection 开始实例化,接着是 UserFinder 最后是 UserLister 。 在写代码的时候,这个先后顺序是不能乱的。并且,须要用到的单元,你要本身一个一个提早准备好。 对于本身写的可能还比较清楚,对于其余团队成员写的,你还要看他的类到底是依赖了哪些,并一一实例化。 这种状况,若是是个别的、少许的还能够接受,若是有个10-20个的,那就麻烦了。 估计光实例化的代码,就能够写满一屏幕了。

并且,若是是团队开发,有些单元应当是共用的,如邮件投递服务。 不能说你写个模块,要用到邮件服务了,就本身实例化一个邮件服务吧?那样岂不是有N模块就有N个邮件服务了? 最好的方式是使邮件服务成为一个单例,这样任何模块在须要邮件服务时,使用的实际上是同一个实例。 用传统的这种实例化对象的方法来实现的话,就没那么直接了。

那么改为DI容器的话,应该是怎么样呢?他是这样的:

use yii\di\Container;

// 建立一个DI容器
$container = new Container;

// 为Connection指定一个数组做为依赖,当须要Connection的实例时,
// 使用这个数组进行建立
$container->set('yii\db\Connection', [
    'dsn' => '...',
]);

// 在须要使用接口 UserFinderInterface 时,采用UserFinder类实现
$container->set('app\models\UserFinderInterface', [
    'class' => 'app\models\UserFinder',
]);

// 为UserLister定义一个别名
$container->set('userLister', 'app\models\UserLister');

// 获取这个UserList的实例
$lister = $container->get('userLister');

采用DI容器的办法,首先各 set() 语句没有先后关系的要求, set() 只是写入特定的数据结构, 并未涉及具体依赖关系的解析。因此,先后关系不重要,先定义什么依赖,后定义什么依赖没有关系。

其次,上面根本没有在DI容器中定义 UserFinder 对于 Connection 的依赖。 可是DI容器经过对 UserFinder 构造函数的分析,能了解到这个类会对 Connection 依赖。这个过程是自动的。

最后,上面只有一个 get() 看起来好像根本没有实例化其余如 Connection 单元同样,但事实上,DI容器已经安排好了一切。 在获取 userLister 以前, ConnectionUserFinder 都会被自动实例化。 其中, Connection 是根据依赖定义中的配置数组进行实例化的。

通过上面的几个 set() 语句以后,DI容器的 $_params 数组是空的, $_singletons 数组也是空的。 $_definintions 数组却有了新的内容:

$_definitions = [
    'yii\db\Connection' => [
        'class' => 'yii\db\Connection',    // 注意这里
        'dsn' => ...
    ],
    'app\models\UserFinderInterface' => ['class' => 'app\models\UserFinder'],
    'userLister' => ['class' => 'app\models\UserLister']    // 注意这里
];

  

在调用 get('userLister') 过程当中又发生了什么呢?说实话,这个过程不是十分复杂, 可是因为涉及到递归和回溯,写这里的时候,我写了改,改了写,示意图画了好几次,折腾了很久,都不满意, 就怕说不清楚,读者朋友们理解起来费劲。 最后画了一个简单的示意图,请大家对照 DI容器解析依赖获取实例的过程示意图 , 以及前面关于 get() build() getDependencies() resolveDependencies() 等函数的源代码, 了解大体流程。若是有任何疑问、建议,也请在底部留言。

 

 

 

 

DI容器解析依赖获取实例的过程示意图

DI容器解析依赖获取实例的过程示意图 中绿色方框表示DI容器的5个数组,浅蓝色圆边方框表示调用的函数和方法。 蓝色箭头表示读取内存,红色箭头表示写入内存,虚线箭头表示参照的内存对象,粗线绿色箭头表示回溯过程。 图中3个圆柱体表示实例化过程当中,建立出来的3个实例。

对于 get() 函数:

  • 在第1步中调用 get('userLister') 表示要得到一个 userLister 实例。 这个 userLister 不是一个有效的类名,说明这是一个别名。 那么要获取的是这个别名所表明的类的实例。
  • 查找 $_definitions 数组,发现 $_definitions['userLister'] = ['class'=>'app\models\UserLister'] 。 这里 userLister 不等于 app\models\UserLister , 说明要获取的这个 userLister 实例依赖于 app\models\UserLister 。 这是查找依赖定义数组的第一种状况。
  • 而在第2二、23步中, get(yii\db\Connection) 调用 get() 时指定要获取的实例的类型, 与依赖定义数组 $_definitions 定义的所依赖的类型是相同的,都是 yii\db\Connection 。 也就是说,本身依赖于本身,这就基本达到了中止递归调用 get() 的条件,差很少能够开始反溯了。 这是查找依赖定义数组的第二种状况。
  • 第三种状况是第三、4步、第1三、14步查找依赖定义数组,发现依赖不存在。 说明所要获取的类型的依赖关系未在容器中注册。 对于未注册依赖关系的,DI容器认为要么是一个没有外部依赖的简单类型, 要么是一个容器自身能够自动解析其依赖关系的类型。
  • 对于第一种状况,要获取的类型依赖于其余类型的,递归调用 get() 获取所依赖的类型。
  • 对于第2、三种状况,直接调用 build() 尝试获取该类型的实例。

build() 在实例化过程当中,干了这么几件事:

  • 调用 getDependencies() 获取依赖信息。
  • 调用 resolveDependencies() 解析依赖信息。
  • 将定义中的配置数组、构造函数参数与调用 get() 时传入的配置数组和构造参数进行合并。 这一步并未在上面的示意图中体现,请参阅 build() 的源代码部分。
  • 根据解析回来的依赖单元,调用 newInstanceArgs() 建立实例。 请留意第3六、42步,并不是直接由 resolveDependencies() 调用 newInstanceArgs() 。 而是 resolveDependencies() 将依赖单元返回后,由 build() 来调用。就像第31步同样。
  • 将获取的类型实例返回给调用它的 get()

getDependencies() 函数老是被 build() 调用,他干了这么几件事:

  • 建立ReflectionClass,并写入 $_reflections 缓存数组。如第6步中, $_reflections['app\models\UserLister'] = new ReflectionClass('app\models\UserLister')
  • 利用PHP的Reflection API,经过分析构造函数的形式参数,了解到当前类型对于其余单元、默认值的依赖。
  • 将上一步了解到的依赖,在 $_dependencies 缓存数组中写入一个 Instance 实例。如第七、8步。
  • 当一个类型的构造函数的参数列表中,没有默认值、参数都是简单类型时,获得一个 [null] 。 如第28步。

resolveDependencies() 函数老是被 build() 调用,他在实例化时,干了这么几件事:

  • 根据缓存在 $_dependencies 数组中的 Instance 实例的 id , 递归调用容器的 get() 实例化依赖单元。并返回给 build() 接着运行。
  • 对于像第28步之类的依赖信息为 [null] 的,则什么都不干。

newInstanceArgs() 函数是PHP Reflection API的函数,用于建立实例,具体请看 PHP手册

这里只是简单的举例子而已,尚未涉及到多依赖和单例的情形,可是在原理上是同样的。 但愿继续深刻了解的读者朋友能够再看看上面有关函数的源代码就好了,有疑问请随时留言。

从上面的例子中不难发现,DI容器维护了两个缓存数组 $_reflections$_dependencies 。这两个数组只写入一次,就能够无限次使用。 所以,减小了对ReflectionClass的使用,提升了DI容器解析依赖和获取实例的效率。

另外一方面,咱们看到,获取一个实例,步骤其实很多。可是,对于典型的Web应用而言, 有许多模块其实应当注册为单例的,好比上面的 yii\db\Connection 。 一个Web应用通常使用一个数据库链接,特殊状况下会用多几个,因此这些数据库链接通常是给定不一样别名加以区分后, 分别以单例形式放在容器中的。所以,实际获取实例时,步骤会简单得。对于单例, 在第一次 get() 时,直接就返回了。并且,省去不重复构造实例的过程。

这两个方面,都体现出Yii高效能的特色。

上面咱们分析了DI容器,这只是其中的原理部分,具体的运用,咱们将结合 服务定位器(Service Locator) 来说。

转载自:http://www.digpage.com/di.html

相关文章
相关标签/搜索