上文,我们了解到yii2的配置,实际是对 yii\web\Application 对象的配置。
但是,在进行 yii\baseYii::configure 方法的讲解时,我们疑问:如果我们在配置文件 main.php 内添加了一个 yii\web\Application 不存在的属性 key,会不会报错?
答:不一定。
假设我们先添加一个属性 a , 让其值等于 1111
2
3
4
5
6return [
'a' => 111,
'id' => 'app-frontend',
'basePath' => dirname(__DIR__),
// ...
];
浏览器访问一下首页,会发现报错提示1
Setting unknown property: yii\web\Application::a
这很正常,也很好理解,即是对属性的配置,没有就报错嘛,不稀奇。
但是,这只是表面现象。main.php 文件内我们也对 components 项配置了却没有报错,这是为什么?
有些php基础的同学应该都能明白,这肯定是 php魔术方法 起到的效果,我们在这个系列的开篇有跟大家补充这方面的知识,还不懂的可以回去再补补。
我们来看yii2是如何实现这个功能的。
首先我们应该从 yii\web\Application 开始查找相应的 set 方法的实现。经过一层层父类筛选,我们发现 yii\base\Component 类有对 set 方法的实现。
【应用的生命周期之复杂的父类关系 】一文中我们说过,yii\base\Component 类对属性、行为、事件实现的三大基石在yii2内非常重要。我们暂时先只看其对属性的实现,行为和事件的分析我们准备后面再谈。下面截取了 yii\base\Component::__set 方法的部分代码,如下1
2
3
4
5
6
7
8
9
10
11
12
13
14public function __set($name, $value)
{
$setter = 'set' . $name;
if (method_exists($this, $setter)) {
// set property
$this->$setter($value);
return;
}
// 涉及到行为 事件的相应代码暂时省略,我们后期再看
throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name);
}
对于文初我们添加配置项“a”,从 yii\base\Component::__set 方法实现来说,会先判断 yii\web\Application::setA 方法是否存在,很明显这并不存在,所以 throw 抛出了异常,这与我们的报错信息一致。
但,言外之意, yii\web\Application::setComponents 方法就存在咯?
没错,你可以在 yii\di\ServiceLocator 类中找到 setComponents 方法。
脑子有点乱,怎么一会是这个类,一会是那个类?
还记得我们前面介绍的 yii\web\Application 繁琐的父类关系吗?1
yii\web\Application => yii\base\Application => yii\base\Module => yii\di\ServiceLocator => yii\base\Component => yii\base\Object => yii\base\Configurable
所以,无论是 yii\base\Component 还是 yii\di\ServiceLocator ,这都可以从 yii\web\Application 找到内在关系。
那么,yii\di\ServiceLocator到底是什么呢?
ServiceLocator , 服务定位器,定位服务的容器。
我们来解释一下:首先“服务”,指的是可以提供某项功能的代码实现,代指 component,比如request组件,user组件等;“定位”即查找,可以从这个“容器”中快速找到具体某个component的意思。
既然要从ServiceLocator中定位component,自然就少不了先向ServiceLocator中添加component,我们称之为“注册”,所以 ServiceLocator 的实现包括以下两部分:
注册component
访问component
单纯就上面这两点的说明,我们好像在哪见过?
没错,我们介绍 依赖注入容器 的时候,提到过类似的实现方案,这里我们就不准备再上php事例代码了,下面直接介绍 ServiceLocator 的实现。
首先,ServiceLocator 有两个属性,_components和_definitions,两者分别在程序运行时存储component的实例和component的定义。
我们先看注册component的实现,即 ServiceLocator::set 方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public function set($id, $definition)
{
unset($this->_components[$id]);
if ($definition === null) {
unset($this->_definitions[$id]);
return;
}
if (is_object($definition) || is_callable($definition, true)) {
// an object, a class name, or a PHP callable
$this->_definitions[$id] = $definition;
} elseif (is_array($definition)) {
// a configuration array
if (isset($definition['class'])) {
$this->_definitions[$id] = $definition;
} else {
throw new InvalidConfigException("The configuration for the \"$id\" component must contain a \"class\" element.");
}
} else {
throw new InvalidConfigException("Unexpected configuration type for the \"$id\" component: " . gettype($definition));
}
}
简单描述一下ServiceLocator::set 的实现
unset($this->_components[$id]),删除_components属性中相同的实例,确保component唯一
component对应的定义$definition,不能为null,如果是对象或者是php callable,ServiceLocator::_definitions 属性直接存储,如果是数组且数组包含class项,则把 $definition直接保存到 ServiceLocator::_definitions 属性,否则抛出异常
其他情况抛出异常
set方法的实现依然很简单,主要负责把component的定义保存起来即可。
通过ServiceLocator获取component的实现方式上,我们看三个方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public function __get($name)
{
if ($this->has($name)) {
return $this->get($name);
} else {
return parent::__get($name);
}
}
public function has($id, $checkInstance = false)
{
// ...
}
public function get($id, $throwException = true)
{
// ...
}
这三个方法,其实都是指 ServiceLocator::get 方法,但是 __get 魔术方法却能为我们提供更多未知component的入口。
component注册好了,我们总得获取component实例吧,下面重点看一下 ServiceLocator::get 方法的实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public function get($id, $throwException = true)
{
if (isset($this->_components[$id])) {
return $this->_components[$id];
}
if (isset($this->_definitions[$id])) {
$definition = $this->_definitions[$id];
if (is_object($definition) && !$definition instanceof Closure) {
return $this->_components[$id] = $definition;
} else {
return $this->_components[$id] = Yii::createObject($definition);
}
} elseif ($throwException) {
throw new InvalidConfigException("Unknown component ID: $id");
} else {
return null;
}
}
解读一下上面代码的含义:
首先,该方法会先判断 ServiceLocator::_components 属性是否有要找的component,如果有,则直接返回,避免重复 get;如果没有,检查 ServiceLocator::_definitions 属性是否存在 component 的定义,如果有,获取相应的定义 $definition,如果 $definition 本身就是我们一开始存储的 object或者php callable,直接返回,否则调用 yii\BaseYii::createObject 方法实例化 $definition的class项。我们清楚的知道,yii\BaseYii::createObject 的实质是调用 Container容器实例化类,所以说 ServiceLocator 服务定位容器和 yii\di\Container 依赖注入容器的关联也在于此。
另外,有一个需要注意的点,这里提醒一下大家:
如果是调用 yii\web\Application未知的属性,yii\di\ServiceLocator::get 方法会先被调起,即会先查找该属性是否是component,如果是,则通过 yii\di\ServiceLocator::get方法获取,如果不是,并没有直接抛出异常,而是继续调用ServiceLocator的父类 yii\base\Component::get 方法处理,这一点见 yii\di\ServiceLocator::__get 方法的实现。
而 yii\base\Component::__get 方法,代码处理之后(忽略行为的实现)如下1
2
3
4
5
6
7
8
9
10
11
12public function __get($name)
{
$getter = 'get' . $name;
if (method_exists($this, $getter)) {
// read property, e.g. getName()
return $this->$getter();
}
// other code
throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name);
}
该方法会先判断 yii\web\Application的getComponent 方法是否存在(Component代指具体的component组件,比如 request等),如果存在则直接调用,不存在则抛出异常。
现在我们再看配置文件的 components 项,即 yii\di\ServiceLocator::setComponents方法1
2
3
4
5
6public function setComponents($components)
{
foreach ($components as $id => $component) {
$this->set($id, $component);
}
}
该方法的实现有点无聊了,其实就是批量set,了解了 yii\di\ServiceLocator::set 方法的实现几乎就没有任何问题了。
不过还是要提醒的是,这里注册的component有很多,我们在 yii\base\Application::preInit 方法中介绍过,$config[‘components’] 的结果是 yii\base\Application::coreComponents方法和 yii\web\coreComponents方法以及自定义component的合集。
默认的,应该包含以下核心component,都被注册到了 ServiceLocator,所以后面如果遇到 Yii::$app->get(‘component’) 的代码不要不知道是干嘛的哦。1
2
3
4
5
6
7
8
9
10
11
12
13request
response
session
user
errorHandler
log
view
formatter
i18n
mailer
urlManager
assetManager
security
从 yii\di\SeriveLocator::set 方法的实现上我们也看到,这些components仅仅是添加其定义到 yii\di\ServiceLocator::_definitions 属性,并没有实例化操作,所以性能上的影响,可以忽略。
其实,不止 components 属性不存在,包括 frontend/config/main-local.php 内modules的配置,modules项也不存在。那么相应的是不是也存在一个setModules方法呢?
没错,yii\base\Module::_modules属性、对应的注册方法yii\base\Module::setModule和yii\base\Module::setModules方法以及获取注册的module方法 yii\base\Module::getModule和yii\base\getModules方法均有类似的实现,各位可以自己去看看,我们就不说了。
这里提一下现阶段已经注册的两个module,dev环境,默认是在 frontend/config/main-local.php 文件内添加的。1
2yii\debug\Module
yii\gii\Module
本来今天还准备再谈一谈行为和事件的实现,看来只能下一节再说了。