yii2依赖注入容器(一)

在php中,依赖注入以及依赖注入容器,先前我们均有所了解。关于其原理,我们也做过说明,且配有大量的例子加以辅导,如果你还有问题,留言告诉我。

今天我们来看一下yii2内部是如何实现依赖注入容器的。

为什么突然又回到这个问题上了?

接上文对Yii.php的分析,我们应该看Yii.php文件内的最后一行代码了

1
Yii::$container = new yii\di\Container();

其含义是实例化依赖注入容器,赋值给 Yii::$container 属性,也就是说yii2中依赖注入容器指的就是 yii\di\Container 类的实现。

ps: 需要的话,你可以在源码文件上写一下注解,放宽心,尽管写。
注意,Yii::$container 具有全局性,所以我们在应用中使用 Container 时,尽量使用 Yii::$container 操作,而不是这里实例化一个Container,那里实例化一个Container。

关于Container创建对象的操作,我们可以使用 BaseYii::createObject 方法,该方法封装了 yii\di\Container 类的使用,所以我们通常直接用 Yii::createObject 方法创建对象或者调用可回调函数,这在yii2内非常普遍,后面我们会遇到很多 Yii::createObject 创建对象的例子。

为了避免枯燥的分析,我们来两个例子,希望各位看的更明白些。

在 frontend\components 目录下创建两个类文件 T.php 和 Test.php(components目录我们手动创建的),源码分别如下

T.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

namespace frontend\components;

use Yii;

class T
{
public $name = 'This is t';
}
Test.php

<?php

namespace frontend\components;

use Yii;

class Test
{
public $name;

private $_age;

private $_t;

public function __construct($age, T $t)
{
$this->_t = $t;
$this->_age = $age;
}
}

注意看这两个类,并无特殊之处,其中 Test 依赖 T,这一点在Test的构造方法 __construct 中有所表现。

我们看一下如何通过 Yii::createObject 方法实例化 Test 吧,关于Yii::createObject 方法的参数下面我们再做分析

1
2
3
4
5
6
$testObject = Yii::createObject([
'class' => 'frontend\components\Test',
'name' => 'new test name',
], [20]);

print_r($testObject);

结果,希望没有出乎你的意料

1
2
3
4
5
6
7
8
9
10
frontend\components\Test Object
(
[name] => new test name
[_age:frontend\components\Test:private] => 20
[_t:frontend\components\Test:private] => frontend\components\T Object
(
[name] => This is t
)

)

例子各位动动手指写一下,写的时候可以揣摩一下。下面我们来看本文重点,即以上案例的实现过程分析。

首先我们看一下 Yii::createObject 方法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static function createObject($type, array $params = [])
{
if (is_string($type)) {
return static::$container->get($type, $params);
} elseif (is_array($type) && isset($type['class'])) {
$class = $type['class'];
unset($type['class']);
return static::$container->get($class, $params, $type);
} elseif (is_callable($type, true)) {
return static::$container->invoke($type, $params);
} elseif (is_array($type)) {
throw new InvalidConfigException('Object configuration must be an array containing a "class" element.');
}

throw new InvalidConfigException('Unsupported configuration type: ' . gettype($type));
}

该方法接受两个参数

第一个参数 $type 是要创建的对象类型,从 if 接连三个 elseif,可以看出该参数可以是字符串,数组或者 callable 结构
第二个参数是要创建的对象的构造参数,即 __construct 方法的参数,是个数组,比如Test的构造方法,我们传了第一个参数 [20],自然就是 $age 的值,至于第二个参数我们没写,当然也可以写,为了下面讲解 yii\di\Instance,我们先不写
注意到$type,无论是string还是array,其实质都是调用 Yii::$container->get($class) 实例化类。

以我们的例子,其形式如下

1
2
3
4
Yii::createObject([
'class' => 'frontend\components\Test',
'name' => 'new test name',
], [20])

被 Yii::createObject 处理之后,走第一个elseif,其形式是

1
Yii::$container->get('frontend\components\Test', [20], ['name' => 'new test name']);

下面我们深入敌后,来看 Container::get 方法。

首先我们注意到 Container有5个属性

1
2
3
4
5
6
7
8
9
10
// 保存单例对象
private $_singletons = [];
// 保存对象的定义
private $_definitions = [];
// 保存对象构造方法的参数
private $_params = [];
// 保存对象依赖的反射类信息
private $_reflections = [];
// 保存对象依赖的其他类信息
private $_dependencies = [];

单纯的看这5个属性,我们很难发现其所以然,这里先过一眼。我们下面会做分析,但看得出来,这5个属性都是为了避免程序运行时重复解析而缓存的局部信息,因为重复解析会耗时间,耗内存,慢慢的,也就降低了应用的性能,这是非常不好的事情。

以下面这行代码为例,我们继续 Container::get方法的探索。

1
Yii::$container->get('frontend\components\Test', [20], ['name' => 'new test name']);

注意get方法的第三个参数是类 $class 的属性以及对应的属性值,这其实是“注入”的一种表现,下面我们会介绍这一步是怎么实现的。

继续从这行代码追起,看get方法

1
2
3
4
5
6
if (isset($this->_singletons[$class])) {
// singleton
return $this->_singletons[$class];
} elseif (!isset($this->_definitions[$class])) {
return $this->build($class, $params, $config);
}

本文我们重点分析Container::get方法的前6行代码,这6行代码的含义如下

判断 Container::_singletons 属性是否存在 $class 的定义,即$class对应的单例是否存在,因为我们是首次实例化Test类,所以肯定不满足了
如果 Container::_definitions 属性不存在 $class 的定义,即是否定义过以何种形式实例化 $class,哈,这正符合我们的条件,下面我们走到 Container::build 方法

1
$this->build('frontend\components\Test', [20], ['name' => 'new test name']);

Container::build 方法是实例化类的方法,对于容器,我们知道,build方法要做的核心应该包括下面3点

获取Test的依赖
解析依赖
实例化对象
我们粗略的看下build方法的实现,看下面代码的注解部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
protected function build($class, $params, $config)
{
/* @var $reflection ReflectionClass */
// 获取 $class 的依赖,返回 $class 的反射信息和依赖信息
list ($reflection, $dependencies) = $this->getDependencies($class);

// 依赖信息是通过构造方法获取的,所以如果我们通过 get 方法有给构造函数传递参数,理应以我们传递的为准,即覆盖掉 getDependencies 获取的
foreach ($params as $index => $param) {
$dependencies[$index] = $param;
}

// 解析依赖
$dependencies = $this->resolveDependencies($dependencies, $reflection);
if (!$reflection->isInstantiable()) {
throw new NotInstantiableException($reflection->name);
}
// 如果$config为空,即不需要给 $class "注入" 属性和属性值,直接实例化
if (empty($config)) {
return $reflection->newInstanceArgs($dependencies);
}

// 检查 $class 是否实现了接口 yii\base\Configurable,我们的Test肯定是否了
if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {
// set $config as the last parameter (existing one will be overwritten)
$dependencies[count($dependencies) - 1] = $config;
return $reflection->newInstanceArgs($dependencies);
} else {
// 实例化 $class,并"注入"属性,最后返回我们的对象 $object
$object = $reflection->newInstanceArgs($dependencies);
foreach ($config as $name => $value) {
$object->$name = $value;
}
return $object;
}
}

整个过程我们明白了,但是 build方法内获取依赖以及解析依赖部分我们有必要看一下,因为这涉及到一个新的源码类 yii\di\Instance 。

首先我们看获取类的依赖 Container::getDependencies方法,该方法的核心实现我们并不陌生。我们在 php之控制反转容器(Ioc Container)一文中最后实现Container容器部分的操作非常雷同。

跟我们手动实现的Container容器相比,注意下面标记的4个地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected function getDependencies($class)
{
// ①、判断 Container::_reflections 属性是否有保存 $class 的反射信息,如果有就直接返回,不用再解析,毕竟获取类的反射信息也是要消耗时间
if (isset($this->_reflections[$class])) {
return [$this->_reflections[$class], $this->_dependencies[$class]];
}

$dependencies = [];
$reflection = new ReflectionClass($class);

$constructor = $reflection->getConstructor();
if ($constructor !== null) {
foreach ($constructor->getParameters() as $param) {
// ②、获取构造函数的参数时,通过 isDefaultValueAvailable 方法判断参数的默认值
if ($param->isDefaultValueAvailable()) {
$dependencies[] = $param->getDefaultValue();
} else {
$c = $param->getClass();
// ③、调用 yii\di\Instance::of 方法处理依赖的类名,Instance::of 方法返回 Instace 类的实例
$dependencies[] = Instance::of($c === null ? null : $c->getName());
}
}
}
// ④、将反射信息和依赖信息分别保存在 Container::_reflections和Container::_dependencies属性,防止重复实例化该类时重复解析反射信息和依赖信息
$this->_reflections[$class] = $reflection;
$this->_dependencies[$class] = $dependencies;

return [$reflection, $dependencies];
}

注意第③点的 yii\di\Instance,这个类起什么作用呢?

我们准备先说一说依赖解析方法 Container::resolveDependencies 再聊一聊这个事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected function resolveDependencies($dependencies, $reflection = null)
{
foreach ($dependencies as $index => $dependency) {
if ($dependency instanceof Instance) {
if ($dependency->id !== null) {
$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;
}

这个方法很简单,在该方法中,循环 $class 的依赖信息,判断每一个参数,如果是 yii\di\Instance的实例并且 yii\di\Instace::id属性非空时,再次通过 Container::get方法获取对象。

这是什么意思?

我们用 Test 这个例子来理一下头绪。

首先看一下此时 Test 的依赖信息的数据结构,即 Container::resolveDependencies 方法的第一个参数, $dependencies

1
2
3
4
5
6
7
8
9
Array
(
[0] => 20
[1] => yii\di\Instance Object
(
[id] => frontend\components\T
)

)

这样就清晰多了,如此我们便不难理解 resolveDependencies 方法中再次用 $this->get($dependency->id) 的作用,实例化依赖对象嘛。

前前后后合计着 yii\di\Instance 只是临时保存依赖的类名或者接口名而已,这样一来,Container::getDependencies 方法的第③点我们也就明白了。

通常,在配置依赖注入容器时,我们可以通过 Instance 来引用类名、接口名或者别名(并非是我们上文介绍的alias别名,这里只是一个名字而已,比如”className”),随后会再次被 Container容器解析为实际的对象。

比如

1
2
3
4
5
$container = new \yii\di\Container;
$container->set('cache', [
'class' => 'yii\caching\DbCache',
'db' => \yii\di\Instance::of('db')
]);

当然它还有其他含义,我们后面再做分析。

这第一部分,从实例化 Test 说起算是结束了,但是Container容器我们还只说了一半,下一章节我们继续看 Container。