上文我们介绍了 Container 的核心实现,但是我们倒过来看1
Container::resolveDependencies -> Instance::of -> Container::getDependencies -> Container::build -> Container::get
上文也仅仅从 Container::get 方法的前6行代码顺势分析而已,无论我们再怎么用get方法模拟测试,也都是重复这个步骤,况且我们一开始介绍的 Container 的5个属性,目前数数也就用到了 Container::_reflections 属性和 Container::_dependencies属性。
也就是说 Container 容器我们还没有分析完,今天我们准备再聊一聊相比上文简单些的内容。
从 get 方法第7行代码说起,即下面这行代码1
2$definition = $this->_definitions[$class];
Container::_definitions 属性,保存着类实现方式的定义。
也就是说我们可以用 Container 先定义 $class 的实现,再通过 get 实例化类时,直接按照 $class 的定义去实例化,即没有必要走上文的老路,又是反射又是依赖的。
那何为类的定义?说到类的定义,就有必要聊一聊 Container::set 方法。
该方法接收类名、接口名、别名、含有配置的类、别名以及callable 作为参数,基本上满足我们所有的需求。
还以上文的Test类和T类为例(二者的定义参考上文)1
2
3
4
5
6Container::set
Yii::$container->set('testName', [
'class' => 'frontend\components\Test',
'name' => 'test name',
], [18]);
获取1
2
3
4$testObject = Yii::$container->get('testName', [20], [
'name' => 'new test name'
]);
print_r($testObject);
结果1
2
3
4
5
6
7
8
9
10frontend\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
)
)
我们分析一下上面的例子,从set说起。
set的实现,注解都在备注里1
2
3
4
5
6
7
8
9
10public function set($class, $definition = [], array $params = [])
{
// 规范化类以及其定义,并以 $class 为下标,保存于 Container::_definitions 属性
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
// 以 $class 为下标,将$class定义时的参数 $params 保存于 Container::_params 属性
$this->_params[$class] = $params;
// 重新定义的 $class,自然没必要保留单例的对象
unset($this->_singletons[$class]);
return $this;
}
set方法接收3个参数
$class 可以是类名 接口名,也可以是我们指定的一个别名
$definition $class实现的定义,可以是 string|array|callable,也可以不指定,具体需要通过 Container::normalizeDefinition 方法规范处理
最后一个参数是$class构造方法需要的参数
规范化类定义的含义,其实就是担心第二个参数 $definition 格式的问题,更能反映 set 的灵活性。这一点可以从 Container::normalizeDefinition 方法中看得出来,这个方法你们可以找到看一看。
Container关于类定义的实现看起来很简单,只依靠两个属性 Container::_definitions和Container::_params,并没有涉及更多其他方法以及复杂的流程。
例子中,我们定义的一个别名叫做 testName 的 $class,并为该别名定义的 $definition 是一个数组,且第一个构造参数的值是18,注意此时我们仅仅是定义,并没有实例化类。
定义结束,我们看一下如何通过 Container 容器为别名叫做 testName 的类实例化?其实说白了就是实例化第二个参数 $definition 数组下标 class 指定的类。
ps:有些人傻傻分不清楚什么时候使用斜杠/ 什么时候使用反斜杠\?
其实这算是再简单不过的问题了。作为文件路径,应该使用斜杠 / ,作为类名,自然使用反斜杠 \ 。
比如这里定义 testName 时,我们指定 class=frontend\components\Test,如果使用斜杠就是错误的。
实例化操作,我们还是调用 Container::get 方法操作,所以我们重新来看一下此时的 get 方法。
get方法前6行代码的if和elseif显然都不满足,从第7行代码看起。
首先先获取$class相关的定义,即我们在 set 方法中,以 $class为下标,保存的 $class 规范化之后的定义,此时 $definition = $this->_definitions[$class]; 的结果应该是1
2
3
4$definition = [
'class' => 'frontend\components\Test',
'name' => 'test name',
];
与此同时,get方法的另外两个参数 $params 和 $config 如下1
2
3$params = [20]; // 构造参数
$config = ['name' => 'new test name']; // 类的属性以及对应的属性值
继续get方法,我们把分析的逻辑都写在注解里,下面的代码你可以从上往下一行行的看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
54public function get($class, $params = [], $config = [])
{
if (isset($this->_singletons[$class])) {
// singleton
return $this->_singletons[$class];
} elseif (!isset($this->_definitions[$class])) {
return $this->build($class, $params, $config);
}
$definition = $this->_definitions[$class];
// 如果 $definition 是 callable,满足这个if
if (is_callable($definition, true)) {
// 调用 Container::resolveDependencies 方法解决依赖
$params = $this->resolveDependencies($this->mergeParams($class, $params));
// 调用 $definition
$object = call_user_func($definition, $this, $params, $config);
}
// 此时我们的$definition满足数组这一条件
elseif (is_array($definition)) {
$concrete = $definition['class'];
unset($definition['class']);
// 合并 $definition 中相同的 key,实质是以调用get方法时传递的 k-v 为准,k-v分别是对象的属性和属性值
$config = array_merge($definition, $config);
// $params是构造参数值,这里就是覆盖set时所定义的构造参数值,以当前get传递的构造参数值为准
$params = $this->mergeParams($class, $params);
// 以我们当前的例子,$class=testName,$concrete=frontend\components\Test,所以第一次走else
// 如果我们当初直接定义的是类名 Yii::$container->set('frontend\components\Test'),此时就应该走下面这个if,然后直接build实例化了
if ($concrete === $class) {
$object = $this->build($class, $params, $config);
} else {
// 按照我们例子,走到这里之后,会继续调用 get 方法处理,即这是一个递归
// 如果继续走下去,那么下一个get就会循环我们上文介绍的build,即从get方法的第4行代码走起
// 不管怎么走,还是要build实例化 $class,如此,我们也就得到 $object 啦
$object = $this->get($concrete, $params, $config);
}
}
// 如果 $definition 是对象,将其保存到 Container::_singletons 属性
elseif (is_object($definition)) {
return $this->_singletons[$class] = $definition;
}
// 啥也不是,抛出异常
else {
throw new InvalidConfigException('Unexpected object definition type: ' . gettype($definition));
}
// 如果$class已经是单例了,则覆盖掉
if (array_key_exists($class, $this->_singletons)) {
// singleton
$this->_singletons[$class] = $object;
}
// 返回最终的$object
return $object;
}
从上我们可以看出,set一个别名的时候,还是需要二次get类名的(第一个get别名,无法build)。
至此,Container 容器的里里外外大家心里都应该有些明白了。
为什么要分析这些呢?Container容器也算是最基本的实现,先做个了解。无论是对php基础薄弱的同学还是对后面我们分析yii2的生命周期,都非常有帮助。