Yii2源码分析 之 组件加载过程
平时可能我们对于这样的写法已经司空见惯了 Yii::$app->db
或者 Yii::$app->redis
,那么到底 db
和redis
组件是怎么加载的呢,带着这样的疑问,再查询了源码和资料后发现了,这些组件并不是在系统运行的时候就直接创建的,而是在使用到的时候才去创建,本文记录下源码学习过程。
首先自然从入口文件index.php开始
require __DIR__ . '/../../vendor/autoload.php';
require __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php';
require __DIR__ . '/../../common/config/bootstrap.php';
require __DIR__ . '/../config/bootstrap.php';
$config = yii\helpers\ArrayHelper::merge(
require __DIR__ . '/../../common/config/main.php',
require __DIR__ . '/../../common/config/main-local.php',
require __DIR__ . '/../config/main.php',
require __DIR__ . '/../config/main-local.php'
);
(new yii\web\Application($config))->run();
可以看到所有的配置被合并之后交给了yii\web\Application
,我们配置的db
组件和redis
组件自然也被带过去了,所以自然先去这个类的构造函数,但是跳转过去之后发现这个类没有构造函数,所以自然去找它的父类yii\base\Application
的构造函数。
public function __construct($config = [])
{
Yii::$app = $this;
static::setInstance($this);
$this->state = self::STATE_BEGIN;
$this->preInit($config);
$this->registerErrorHandler($config);
Component::__construct($config);
}
分析这段代码:
Yii::$app = $this;
首先把当前实例yii\web\Application
赋值给了Yii::$app
static::setInstance($this);
是yii\base\Module
的方法,主要是把当前类和实例的关系保存到loadedModules
数组里。
$this->preInit($config);
主要是做一些初始化操作,比如设置时区和runtime路径,并且会合并系统的核心组件到$config
中。
$this->registerErrorHandler($config);
则主要是接管默认的异常和错误处理。
到目前位置好像还是没看到我们配置文件中的组件在哪里加载并实例化的,接着分析构造函数的最后一句代码。
Component::__construct($config);
这段代码调用的是yii\base\Component
的构造函数,但是这个类没有构造函数,实际调用的是其父类yii\base\BaseObject
的构造函数。
public function __construct($config = [])
{
if (!empty($config)) {
Yii::configure($this, $config);
}
$this->init();
}
首先看第一句Yii::configure($this, $config);
实际调用的是yii\baseYii
的configure
方法
public static function configure($object, $properties)
{
foreach ($properties as $name => $value) {
$object->$name = $value;
}
return $object;
}
这段代码的意思就是缓存遍历配置项数组 $config
比如 id
、name
、components
然后赋值给yii\web\Application
实例,我们的db
、redis
组件都在 components
数组中,所以也就是调用了 Yii::$app->components = $value
,但是查找yii\web\Application
类,并没有发现 components
属性啊。
别着急,我们先把yii\web\Application
的继承链画出来。
根据常识、在PHP中给类不存在的属性赋值,会调用__set
魔术方法,所以我们在原型链上找到yii\web\Application
最近的父类的__set()
方法,最后发现最近的__set()
方法在 yii\base\Component
中。
public function __set($name, $value)
{
$setter = 'set' . $name;
if (method_exists($this, $setter)) {
// set property
$this->$setter($value);
return;
} elseif (strncmp($name, 'on ', 3) === 0) {
// on event: attach event handler
$this->on(trim(substr($name, 3)), $value);
return;
} elseif (strncmp($name, 'as ', 3) === 0) {
// as behavior: attach behavior
$name = trim(substr($name, 3));
$this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value));
return;
}
// behavior property
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name)) {
$behavior->$name = $value;
return;
}
}
if (method_exists($this, 'get' . $name)) {
throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name);
}
throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name);
}
我们只需要看第一个if语句中的代码,如果发现有setcomponents
方法,则去调用setcomponents
它,那么yii\web\Application
的父类中有没有setcomponents
方法呢?有,在yii\di\ServiceLocator
类中。
public function setComponents($components)
{
foreach ($components as $id => $component) {
$this->set($id, $component);
}
}
这个代码做的事情也很简单,就是遍历components
中的各个配置数组,调用set
方法,set
方法做了什么呢?
public 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;
$this->_definitions[$id]['class'] = $definition['__class'];
unset($this->_definitions[$id]['__class']);
} elseif (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));
}
}
分析上面的代码可以看出,其实这个方法就是清空_components
属性,然后把配置文件中componetns
的各项组件配置保存到_definitions
属性中,然后就完了???
通过上面一通分析,我们可以看出,在应用运行的时候,Yii并没有去实例化组件,仅仅是把他们的保存了起来,那么当我们访问Yii::$app->db
的属性时会发生什么呢?根据PHP常识,访问一个类中不存在的属性时,会调用__get
魔术方法,所以我们只需要找到yii\web\Application
或者离他最近的父类中的__get
方法就行了,最后在yii\di\ServiceLocator
中找到了__get
方法。
public function __get($name)
{
if ($this->has($name)) {
return $this->get($name);
}
return parent::__get($name);
}
首先是has
方法,判断db
是否存在。
public function has($id, $checkInstance = false)
{
return $checkInstance ? isset($this->_components[$id]) : isset($this->_definitions[$id]);
}
$checkInstance
默认值为false,也就是判断_definitions
数组中,是否有db
元素,根据上面的分析,我们已经把所有的components都保存在了_definitions
数组中,所以这个判断自然返回true。
所以继续直接返回了$this->get(db);
,我们来看下get
方法。
public 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;
}
return $this->_components[$id] = Yii::createObject($definition);
} elseif ($throwException) {
throw new InvalidConfigException("Unknown component ID: $id");
}
return null;
}
分析下代码我们得知,最终执行了这一句 return $this->_components[$id] = Yii::createObject($definition);
,也就是当我们调用Yii::$app->db
时,最终是调用Yii::createObject
并且把返回结果保存在了_components
属性中,这样下次再调用Yii::$app->db
时,就直接返回了_components
中的db
元素。 至于Yii::createObject
做了什么则不在本次分析之中,我们下次再说。
最后总结一下: 当我们首次运行应用时,我们所有在配置文件中定义好的组件components
只是保存在了yii\di\ServiceLocator
的_definitions
属性中,并没有创建这些组件,而是当我们真正第一次调用组件时才去创建组件,并且保存在_components
属性中,这样下次再调用相同组件的时候,就不用在此创建,而是直接返回组件的实例。
这种组件的懒加载还是值得我们学习的,用到的时候再去创建实例,创建好之后保存起来供下次使用,这对系统的性能是有好处的。因为这样节省了创建实例的时间和内存。