使用场景
$session = \YII::$app->session
YII
框架中每个组件都是一个单例,通过服务定位模式,在组件使用之时才进行初始化。
准备知识
程序入口
index.php
中实例化 yii\web\Application
(new yii\web\Application($config))->run();
类继承关系
了解下类继承关系,方便后续寻找对应成员函数
class yii\web\Application extends yii\base\Application {}
abstract class \yii\base\Application extends yii\base\Module {}
class yii\base\Module extends yii\di\ServiceLocator {}
class yii\di\ServiceLocator extends yii\base\Component{}
class yii\base\Component extends yii\baseBaseObject{}
组件注册流程
寻找构造函数
由于yii\web\Application
类中无构造函数,所以只能一层一层找父类的构造,首先第一层就是\yii\base\Application
中的构造函数。代码如下:
public function __construct($config = [])
{
Yii::$app = $this;
static::setInstance($this);
$this->state = self::STATE_BEGIN;
//config 配置的初始化
$this->preInit($config);
//注册错误句柄
$this->registerErrorHandler($config);
//组件初始化
Component::__construct($config);
}
以上代码中主要关注preInit
以及Component::__construct
。
关注preInit
和Component::__construct
preInit
需要关注的相关代码如下:
public function preInit(&$config)
{
/* 此处省略其他预处理代码 */
// merge core components with custom components
foreach ($this->coreComponents() as $id => $component) {
if (!isset($config['components'][$id])) {
$config['components'][$id] = $component;
} elseif (is_array($config['components'][$id]) && !isset($config['components'][$id]['class'])) {
$config['components'][$id]['class'] = $component['class'];
}
}
}
首先在perInit
中会获取coreComponents
中的数据,相关核心组件数据如下:
// 父类中的核心组件
abstract class \yii\base\Application extends yii\base\Module
{
public function coreComponents()
{
return [
'log' => ['class' => 'yii\log\Dispatcher'],
'view' => ['class' => 'yii\web\View'],
'formatter' => ['class' => 'yii\i18n\Formatter'],
'i18n' => ['class' => 'yii\i18n\I18N'],
'mailer' => ['class' => 'yii\swiftmailer\Mailer'],
'urlManager' => ['class' => 'yii\web\UrlManager'],
'assetManager' => ['class' => 'yii\web\AssetManager'],
'security' => ['class' => 'yii\base\Security'],
];
}
}
//当前类中的核心组件
class yii\web\Application extends yii\base\Application
{
public function coreComponents()
{
return array_merge(parent::coreComponents(), [
'request' => ['class' => 'yii\web\Request'],
'response' => ['class' => 'yii\web\Response'],
'session' => ['class' => 'yii\web\Session'],
'user' => ['class' => 'yii\web\User'],
'errorHandler' => ['class' => 'yii\web\ErrorHandler'],
]);
}
}
以上这些组件是Yii
的核心组件,Yii
会通过preInit
首先写入到$config['components']
中
Component::__construct
中需要关注的相关代码如下:
Component
类中使用继承来自yii\baseBaseObject
中的构造函数
public function __construct($config = [])
{
if (!empty($config)) {
Yii::configure($this, $config);
}
$this->init();
}
在构造函数中主要是调用了BaseYii
中的静态方法configure
public static function configure($object, $properties)
{
foreach ($properties as $name => $value) {
$object->$name = $value;
}
return $object;
}
当configure
中foreach
循环执行到components
时,$name
为components
,$value
为所有组件的配置;此时给$object
对象的components
属性进行赋值,但是yii\web\Application
和其对应的父类中并没有components
成员变量,当赋值操作找不到components
成员变量时,会去调用父类中__set
方法尝试寻找setComponents
方法,该方法存在于yii\di\ServiceLocator
中,具体代码如下:
public function setComponents($components)
{
foreach ($components as $id => $component) {
$this->set($id, $component);
}
}
setComponents
方法其实就是遍历各个组件的配置,并调用yii\di\ServiceLocator
中的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;
} 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));
}
}
set
方法其实就是将组件配置存入$this->_definition
,方便后续获取。
整个组件注册流程其实就是将所有核心组件配置信息分别注册到yii\di\ServiceLocator
类中的_definition
私有成员中。
组件加载流程
当在代码中调用相关核心组件时,由于不存在对应的成员变量,会一直找父类中的__get
魔术方法,最终在yii\di\ServiceLocator
中有对应的定义。
yii\di\ServiceLocator
中的__get
方法
具体的代码参考如下:
class ServiceLocator extends Component
{
// 用于缓存服务、组件等的实例
private $_components = [];
// 用于保存服务和组件的定义,用于实例化对象
private $_definitions = [];
//将对象ID作为ServiceLocator的属性,可通过$serviceLocator->{ID}直接获取
public function __get($name)
{
if ($this->has($name)) {
return $this->get($name);
}
return parent::__get($name);
}
//检验是否有对象$id
public function has($id, $checkInstance = false)
{
return $checkInstance ? isset($this->_components[$id]) : isset($this->_definitions[$id]);
}
}
整体步骤梳理如下:
__get
方法首先进入has
方法,根据参数$checkInstance
进行后续判断- 若
$checkInstance
为false
(默认值),该方法将返回一个值,指示_components
中是否具有指定的组件定义。 - 若
$checkInstance
为true
,该方法将返回一个值,指示_definitions
中是否有实例化指定的组件。 - 由于一开始
$checkInstance
值为false
,会直接返回isset($this->_definitions[$id])
的结果,如果是Yii
内部核心组件,由于提前自动注册过,返回true
,调用当前类的get
方法。
yii\di\ServiceLocator
中的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];
//有该对象的定义,且定义已经是一个对象,设置$_components并直接返回
if (is_object($definition) && !$definition instanceof Closure) {
return $this->_components[$id] = $definition;
}
// 有定义但未实例化,则交给DI Container去实例化,并且设置$_components
return $this->_components[$id] = Yii::createObject($definition);
} elseif ($throwException) {
throw new InvalidConfigException("Unknown component ID: $id");
}
return null;
}
在get
方法中执行流程如下:
- 对于
_components
中已经实例化记录过的组件,直接返回对应组件实例 - 对于
_definitions
中定义过但是未实现的组件,创建对应组件的实例,并以组件id
作为key
存入_components
中,下次再调用对应组件时,直接从_components
中获取对应实例返回。 - 若
_definitions
中没有对应组件注册信息的组件,抛出异常
注:每个组件都是一个单例,只会被实例化一次
总结
注册过程
Yii
中对于核心组件会提前注册到yii\di\ServiceLocator
中的_definitions
中,组件名称作为key
,组件相关配置作为value
加载过程
当调用核心组件时,首先判断yii\di\ServiceLocator
中的_components
是否已经有实例化好的组件信息,若有直接返回,否则使用_definitions
中对应组件的配置信息进行实例化返回,并将实例化的对象记录到_components
中。