Yii依赖注入容器解析

Yii的DI容器对应类 yii\di\Container

1、注册类定义

使用DI容器,首先需要将类的定义注册到容器中,当你想获取类实例时,DI容器会根据类的定义,帮你实例化
类本身,及其依赖单元

1.1 源码解析

使用 yii\di\Container->set()方法将类的定义注册到属性 $_definitions

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//参数解析:
//$class 类的标识,可以是类名、接口名、别名
//$definition 对于`$class`的定义,其类型可以是:
//- php匿名函数
//- 实例对象
//- 数组
//- 字符串:类名、接口名、别名
//$params 类的构造函数参数列表

//代码解析:
//1、将类的定义进行规划化处理后存储到$this->_definitions[$class]中
//2、将类的构造函数参数存储到$this->_params[$class]中
//3、$this->_singletons存储的是类的实例,如果重新注册类定义,原实例将被销毁

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

//对类的定义进行规范化处理

//如果 $definition是空或者字符串,转换为一个包含了”class” 元素的数组
//如果 $definition是匿名函数或对象,将不做处理
//如果 $definition是数组,若包含“class”元素,不做处理,
//若不包含看$class是否是有效类,是则将其作为”class”的值,不是有效类,直接抛异常

//处理后 $definition的值只能是:
//- 带有“class”元素的数组
//- 匿名函数
//- 实例对象

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 new InvalidConfigException("A class definition requires a \"class\" member.");
}
}
return $definition;
} else {
throw new InvalidConfigException("Unsupported definition type for \"$class\": " . gettype($definition));
}
}

1.2 注册类定义示例(官网示例)

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
36
37
38
39
40
41
$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' => 'utf8',
]);

// 通过类的配置注册一个别名
// 这种情况下,需要通过一个 “class” 元素指定这个类
$container->set('db', [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
]);

// 注册一个 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);

2、获取类的实例

使用 yii\di\Container->get()方法获取类的实例,
DI容器同时会将类的依赖单元实例化,通过类的构造函数注入到类的内部。
这是一个递归的过程,也就是说如果类所依赖的类还依赖其他类,那这些依赖关系都将会被解决

2.1 get()方法源码解析

get()方法中调用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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

//参数解析:
//$class 类名或别名,对应set()方法中的$class
//$params 构造函数参数值列表,这些参数应该按照构造函数声明中出现的顺序提供
//$config key-value形式数组被用来初始化对象属性
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匿名函数,调用之
if (is_callable($definition, true)) {
//$this->mergeParams(),将$params中的值与$this->_params[$class]中的值合并(如果存在相同index,则覆盖)
$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);

//此处使用的是一个递归操作,也就是说,当要获取的对象依赖于其他对象时, Yii会自动获取这些对象及其所依赖的下层对象的实例
//比如
//$container->set('app\models\UserFinderInterface', [
// 'class' => 'app\models\UserFinder',
//]);
//$container->get('app\models\UserFinderInterface');
//这时就会使用递归,我们最终获取的是app\models\UserFinder实例
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)) {
// singleton
$this->_singletons[$class] = $object;
}

return $object;
}

get()总结:

首先判断$this->_singletons 中是否存在当前类实例,有直接返回
如果不存在当前类实例,
判断$_definitions中是否注册了类的定义,
如果没有,直接调用build()方法获取类实例
如果有,根据不同定义类型,做相应处理
(1)类定义为匿名函数:直接调用此函数、获取类实例
(2)类定义为数组:递归调用本身,直到$class的值与$_definitions[$class]中class属性的值相同时
                        或$this->_definitions[$class]为定义时,
                          调用build()方法获取类实例
                          简单来说,因为我们注册类定义时,出来使用类名外,还有可能是别名、接口名,
                          当是别名或接口名是,我们就需要先获取到其对应的类,然后才能获取类的实例
                          此处的递归就是为了解决这个问题(具体见源码分析)
(3)类定义为对象:直接返回该对象   

返回类实例之前会将类实例存储到$this->_singletons中

2.2 build()方法源码解析

1、调用 getDependencies($class) 获取类的依赖信息
2、调用resolveDependencies($dependencies, $reflection)解析类的依赖信息
3、通过 $reflection->isInstantiable() 判断类是否可以实例化,如果不能则抛异常
4、通过 $reflection->newInstanceArgs($dependencies) 创建类的实例

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
36
protected function build($class, $params, $config)
{
/* @var $reflection ReflectionClass */
//$reflection 类的有关信息
//$dependencies 类的依赖信息
list ($reflection, $dependencies) = $this->getDependencies($class);

foreach ($params as $index => $param) {
$dependencies[$index] = $param;
}
//解析依赖信息,将依赖信息中保存的Instance实例所引用的类或接口进行实例化
$dependencies = $this->resolveDependencies($dependencies, $reflection);

//检查类是否可实例化
if (!$reflection->isInstantiable()) {
throw new NotInstantiableException($reflection->name);
}

//如果$config为空,直接根据$dependencies参数创建类的实例
if (empty($config)) {
return $reflection->newInstanceArgs($dependencies);
}

//如果$dependencies不为空,并且类实现了接口yii\base\Configurable则将$config作为最后一个参数
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 {//否则创建类的实例后,循环将$config中元素设置为该实例的属性的值
$object = $reflection->newInstanceArgs($dependencies);
foreach ($config as $name => $value) {
$object->$name = $value;
}
return $object;
}
}

2.3 getDependencies()方法源码解析

获取类的依赖信息

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
36
/**
* Returns the dependencies of the specified class.
* @param string $class class name, interface name or alias name
* @return array the dependencies of the specified class.
* 解析要实例化类的依赖信息
* 通过PHP5 的反射机制, 通过类的构造函数的参数分析他所依赖的单元。然后统统缓存起来备用
*/
protected function getDependencies($class)
{
//如果已经缓存了其依赖信息,直接返回缓存中的依赖信息
if (isset($this->_reflections[$class])) {
return [$this->_reflections[$class], $this->_dependencies[$class]];
}

$dependencies = [];
//通过php反射机制,获取类的有关信息
$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();
$dependencies[] = Instance::of($c === null ? null : $c->getName());
}
}
}

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

return [$reflection, $dependencies];
}

2.4 resolveDependencies()方法源码解析

解析类的依赖信息
对$dependencies,$reflection中的缓存信息作进一步处理:
将依赖信息中保存的Instance实例所引用的类进行实例化,重新存储到$dependencies中(通过递归调用get()方法获取依赖类实例)

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