使用場景
$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
中。