Yii2从入门到幼儿园--服务容器源码解析

Dependence Injection Container

Posted by Cann on October 19, 2017

Yii2从入门到幼儿园01——Dependence Injection Container

什么是依赖注入(Dependence Injection, DI)

简言之,就是将类所需要的依赖在外部实例化后再传入类中,以此降低代码的耦合性。

普通代码:

class test {

    public $emailer;

    public function __construct()
    {
        $this->emailer = new Emailer();
    }

    public function sendEmail($to, $content)
    {
        $this->emailer->send($to, $eontent);
    }
}

new test()->sendEmail('xxx', 'xxx');

依赖注入:

class test {

    public $emailer;

    public function __construct($emailer)
    {
        $this->emailer = $emailer;
    }

    public function sendEmail($emailer, $to, $content)
    {
        $emailer->send($to, $eontent);
    }
}

new test(new Emailer())->sendEmail('xxx', 'xxx');

什么是依赖注入容器(Dependence Injection Container, DI Container)

依赖注入有个很大的缺点,类中所有的依赖都要手动实例化然后传入类中,当代码量变大,层级变多的时候,就会产生很多问题,诸如:依赖实例化代码冗余、实例化需要按照一定顺序、多人协作容易重复实例化等等。

DI Container 便是为了解决这个问题,他将类对外部的依赖转变为对DI Container的依赖。类中所需的依赖不再需要手动传入,而是自动去DI Container中获取。

举个Yii2官方的栗子:

 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);
     }
 }

若要运行上面的代码,传统写法:

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

可以看到,上面三次实例化必须按照顺序,先后顺序丝毫不能错,若层级、依赖变多,代码会变得非常繁杂。

DI Container:

 // 这就是我们上面说的 DI Container
 $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');

可以看到,只要在最初将类中所需的依赖注册进入DI Container后,在实例化的时候,完全不需要关心类的内部是如何调用的。

当然,乍一看好像代码反而比之前多了,但是放到整个项目里便会发现方便很多,代码也清晰很多。我们只需要在一个统一的地方去注册依赖,之后实现业务的时候只需要关注实例的获取就好。

例如Yii2 Config中Component配置,便是一个依赖注入的参数配置过程。

另外,类中的依赖也并非全部都要事先注册进 DI Container,当它发现一个未注册的依赖,会利用PHP的反射机制(Reflection)实现自动注册。它的实现接下来也会说到。

Yii2中DI Container的实现

代码路径 vendor/yiisoft/yii2/di/Container.php

我们可以看到,在Container中,维护了5个数组,这是DI Container实现的基础

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

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

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

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

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

让我们随着DI Container执行的顺序一步步解析它。

注册依赖

注册依赖,也就是之前看到的$container->set(['xx' => 'xx']),它的源码如下

    public function set($class, $definition = [], array $params = [])
    {
        $this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
        $this->_params[$class] = $params;
        unset($this->_singletons[$class]);
        return $this;
    }

    protected function normalizeDefinition($class, $definition)
    {
        if (empty($definition)) {
            return ['class' => $class];
        } elseif (is_string($definition)) {
            return ['class' => $definition];
        } elseif (is_callable($definition, true) || is_object($definition)) {
            return $definition;
        } elseif (is_array($definition)) {
            if (!isset($definition['class'])) {
                if (strpos($class, '\\') !== false) {
                    $definition['class'] = $class;
                } else {
                    // throw error
                }
            }
            return $definition;
        } else {
            // throws error
        }
    }

set()方法接收三个参数 $class:我们为将要注册的依赖取的名称。 $definition:该依赖的定义。该参数包含两部分内容,键名:class,该依赖的真实类名。其他键名:该依赖实例化时,需要传入的构造函数参数 *$definition 的传参类型有许多种,可以是字符串,可以是数组,也可以是对象、回调函数,最后set()都会利用normalizeDefinition()进行规范化处理。最后统一成带有class键名的数组,键值则是这个依赖的真实类名 $params:该依赖的构造函数所需要的参数

我们可以看到,这里的set()只做了两件事

  1. 将用户传入的$definition存入$_definitions
  2. 将用户传入的$parmas存入$_params中。

所以注册依赖的时候是不会去创建实例的,它只是将这个依赖对应的 class name 和它的所需的参数保存起来。实例化这个操作是在get()方法中

获取实例

get()中逻辑比较复杂,我把它分了一下,并且在不影响逻辑的前提下调整了部分代码的先后顺序,以便解析起来更清晰些:

    public function get($class, $params = [], $config = [])
    {
        // 第一部分
        if (isset($this->_singletons[$class])) {
            // singleton
            return $this->_singletons[$class];
        }

        // 第二部分
        if (isset($this->_definitions[$class])) {

            $definition = $this->_definitions[$class];

            if (is_array($definition)) {

                $config = array_merge($definition, $config);
                $params = $this->mergeParams($class, $params);

                if ($class === $definition['class']) {
                    $object = $this->build($class, $params, $config);
                } else {
                    $object = $this->get($definition['class'], $params, $config);
                }
            }

            elseif (is_object($definition) || is_callable($definition, true)) {
                // 对象直接返回、回调执行后返回
            }
            else {
                // throw error
            }
        }

        // 第三部分
        else {
            return $this->build($class, $params, $config);
        }

        // 第四部分
        if (array_key_exists($class, $this->_singletons)) {
            // singleton
            $this->_singletons[$class] = $object;
        }

        return $object;
    }
第一部分

这一部分和第四部分一起解析,暂且不谈

第二部分

当你之前注册过该依赖,你会走到这一步。

如果$definition是已经实例化的对象、回调函数,那就直接返回,或者执行后返回,这没什么好说的。

如果是数组,逻辑就比较复杂了:

它先是取出之前储存在$_definitions和$_params中的配置,并且和本次传入的$config、$params合并,后者会覆盖前者。也就是说即使你在注册依赖时设置了配置,之后在获取实例时也仍然可以重新定义它:

    $config = array_merge($definition, $config);
    $params = $this->mergeParams($class, $params);

接下来这个判断主要是为了别名服务的:

    if ($class === $definition['class']) {
        $object = $this->build($class, $params, $config);
    } else {
        $object = $this->get($definition['class'], $params, $config);
    }

总之,最后会执行到build()这个核心方法,最终的实例便是这里创建出来的:

若要创建这个实例,首先,需要获取这个实例的所有依赖:

    protected function build($class, $params, $config)
    {
        list ($reflection, $dependencies) = $this->getDependencies($class);
        // DO SOMETHING
    }

    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 {
                    // 如果参数没有默认值,分析它的类型,若设置了类型,将类型名称放置到Instance实例中
                    // Instance仅仅是一个容器,在后面又会将其中的值取出来
                    // Instance是为了标识这是一个依赖
                    $c = $param->getClass();
                    $dependencies[] = Instance::of($c === null ? null : $c->getName());
                }
            }
        }

        $this->_reflections[$class] = $reflection;
        $this->_dependencies[$class] = $dependencies;

        return [$reflection, $dependencies];
    }

这里使用了反射,解析了该类的构造函数中的所有参数,接下来,会将这些参数中属于依赖的部分进行实例化:

    protected function build($class, $params, $config)
    {
        // DO SOMETHING

        // 将我们手动传入和注册依赖时配置的参数作为依赖导入
        foreach ($params as $index => $param) {
            $dependencies[$index] = $param;
        }

        // 实例化这个类的所有依赖
        $dependencies = $this->resolveDependencies($dependencies, $reflection);

        // DO SOMETHING
    }

    protected function resolveDependencies($dependencies, $reflection = null)
    {
        foreach ($dependencies as $index => $dependency) {
            // 这里将我们刚刚传入Instance中的参数类型名称又取了出来,并且递归获取了它的实例
            if ($dependency instanceof Instance) {
                if ($dependency->id !== null) {
                    $dependencies[$index] = $this->get($dependency->id);
                } elseif ($reflection !== null) {
                    // throws error
                }
            }
        }
        return $dependencies;
    }

这时候,我们已经有了真实的类名、以及这个类所需的所有依赖和参数,接下来只要实例化它就好了:

    protected function build($class, $params, $config)
    {
        // DO SOMETHINE

        // 如果$config为空,直接返回这个类的实例
        if (empty($config)) {
            return $reflection->newInstanceArgs($dependencies);
        }

        // yii\base\Configurable 这个类官方是这么解释的:
        // Configurable is the interface that should be implemented by classes who support configuring its properties through the last parameter to its constructor.
        if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {
            $dependencies[count($dependencies) - 1] = $config;
            return $reflection->newInstanceArgs($dependencies);
        } else {
            // 将$config中的参数作为属性赋予这个实例
            $object = $reflection->newInstanceArgs($dependencies);
            foreach ($config as $name => $value) {
                $object->$name = $value;
            }
            return $object;
        }
    }
第三部分

之前我们说过,依赖并不是一定要先进行注册操作,通过解析源码,我们会发现,注册依赖的过程仅仅是为依赖初始化了部分参数。

当你没有注册过依赖,会走到这一步,它会根据传入的$class,也就是真实的类名去执行build(),最终生成出一个实例。

第四部分

在实际应用中,有许多类是需要单例模式的,例如Emailer 、Logger之类的。

而第四部分加上第一部分,都是为了维护部分依赖的单例模式

注册依赖除了set()还有一个方法:setSingleton,这个方法便是用来注册单例的:

    public function setSingleton($class, $definition = [], array $params = [])
    {
        $this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
        $this->_params[$class] = $params;
        $this->_singletons[$class] = null;
        return $this;
    }

对比会发现,set()setSingleton()的唯一区别便是以下这一句

    unset($this->_singletons[$class]); // set()
    $this->_singletons[$class] = null; // setSingleton():

所以set()是走不到第四部分的,也就不会将实例放入单例数组:$_singletons中。 而setSingleton()除了在第一次实例化的时候会走到这一步,之后便在第一部分就将之前创建的实例返回了。 以此实现部分依赖的单例模式