今天本來打算研究一下yii2.0的AR模型的實現原理,然而,計劃趕不上變化,突然就想先研究一下yii2.0的資料庫組件建立的過程。通過對yii源碼的學習,瞭解了yii組件註冊與建立的過程,並發現原來yii組件註冊之後並不是馬上就去建立的,而是待到實際需要使用某個組件的時候再去建立對應的組件執行個體的。本文大概記錄一下這個探索的過程。
要瞭解yii組件的註冊與建立,當然要從yii入口檔案index.php說起了,整個檔案代碼如下:
<?phpdefined('YII_DEBUG') or define('YII_DEBUG', true);defined('YII_ENV') or define('YII_ENV', 'dev');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();
可以看到入口檔案引入了幾個設定檔,並將所有設定檔的內容都合并到$config這個配置數組中,然後使用這個配置數組作為參數去建立一個應用執行個體。若將這個配置數組列印出來,就會看到,“components”下標對應的元素包含了yii組件的參數資訊(這裡只一小部分):
這些組件的資訊是在引入進來的幾個設定檔中配置的,Yii組件就是使用這些參數資訊進行註冊與建立的。
接下來就進入yii\web\Application類的執行個體化過程了,yii\web\Application類沒有建構函式,但是它繼承了\yii\base\Application類:
所以會自動執行\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);}
這裡要順便說一下預初始化方法preInit(),它的代碼如下:
public function preInit(&$config){ /* 此處省略對$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']; } }}
這個函數對傳遞給建構函式的配置數組$config進行了一些預先處理操作(這裡省略了),最後使用coreComponents()方法返回的數組對$config數組進行了完善,coreComponents()方法是這樣的:
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'], ];}
其實就是一些核心組件的配置,也就是說這些組件是可以不需要我們在設定檔中配置的,yii會自動進行註冊。
好了,回到\yii\base\Application類的建構函式,這個函數最後調用了\yii\base\Component類的建構函式,但\yii\base\Component類是沒有建構函式的,不過它繼承了\yii\base\Object類:
所以也自動執行了\yii\base\Object類的建構函式:
public function construct($config = []){ if (!empty($config)) { Yii::configure($this, $config); } $this->init();}
這裡主要是調用了\yii\BaseYii類的靜態方法configure():
public static function configure($object, $properties){ foreach ($properties as $name => $value) { $object->$name = $value; } return $object;}
這個方法就是迴圈入口檔案(new yii\web\Application($config))->run();中的$config數組(這個數組的結構參見本文第一個),以數組鍵名作為對象屬性名稱,對應的索引值作為對象屬性值進行賦值操作。所以當迴圈到組件配置參數的時候是這樣子的:$object->components = $value($value為所有組件的配置數組),也就是對$object的components屬性進行賦值操作,那這個$object是哪個類的對象呢?回想最初調用的源頭,其實它就是入口檔案中需要進行執行個體化的\yii\web\Application類的對象啊。然而,這個類和它的祖先類都沒有components這個成員變數啊,不急,又要進行一番繼承套路了,順著yii\web\Application類的繼承關係一層一層往上找可以發現\yii\web\Application類最終也繼承了\yii\base\Object類,\yii\base\Object類是支援屬性的,所以yii\web\Application類也支援屬性(關於屬性,可以參考我的另一篇博文:yii2之屬性),當賦值操作找不到components成員變數時會調用setComponents()方法,又去找這個方法的所在,終於在它的祖先類\yii\di\ServiceLocator中找到了setComponents()方法,沒錯,對應用執行個體的components屬性進行賦值操作其實就是調用這個方法!
好了,現在就來看看setComponents()這個方法到底幹了啥:
public function setComponents($components){ foreach ($components as $id => $component) { $this->set($id, $component); }}
其實很簡單,就是迴圈各個組件的配置數組,調用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)); }}
其實就是把組件配置存入$_definitions這個私人成員變數(即註冊),然後呢?然後就沒有下文了。。。
搞了半天,原來yii建立應用執行個體的時候只是進行組件的註冊,並沒有實際建立組件,那麼組件執行個體是什麼時候進行建立的?在哪裡進行建立的呢?別急。從上面推導的這個過程我們知道\yii\di\ServiceLocator類是\yii\web\Application類的祖先類,所以其實yii的應用執行個體其實就是一個服務定位器,比如我們想訪問資料庫組件的時候,我們可以這樣來訪問:Yii::$app->db,這個Yii::$app就是yii應用執行個體,也就是\yii\web\Application類的執行個體,但是\yii\web\Application類和它的父類、祖先類都找不到db這個屬性啊。哈哈,別忘了,php讀取不到類屬性的時候會調用魔術方法get(),所以開始尋找\yii\web\Application繼承關係最近的祖先類中的get()方法,最後在\yii\di\ServiceLocator類中找到了,也就是說,Yii::$app->db最終會調用\yii\di\ServiceLocator類中的get()方法:
public function get($name){ if ($this->has($name)) { return $this->get($name); } else { return parent::get($name); }}
get()方法首先調用has()方法(這個不再貼代碼了)判斷組件是否登入,若登入則調用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; } else { return $this->_components[$id] = Yii::createObject($definition); } } elseif ($throwException) { throw new InvalidConfigException("Unknown component ID: $id"); } else { return null; }}
其中私人成員變數$_components是儲存已經建立的組件執行個體的,若發現組件已經建立過則直接返回組件樣本,否則使用$_definitions中對應組件的註冊資訊,調用\yii\BaseYii::createObject()方法進行組件建立,這個方法最終會調用依賴注入容器\yii\di\Container的get()方法,接著就是依賴注入建立對象的過程了,關於這個過程已經在我的上一篇博文中講解過了,可以參考一下:yii2之依賴注入與依賴注入容器。
好了,yii組件註冊與建立的整個過程就是這樣的。最後總結一下,其實yii建立應用執行個體的時候只是進行了各個組件的註冊,也就是將組件的配置資訊存入\yii\di\ServiceLocator類的私人成員變數$_definitions中,並沒有進行實際建立,等到程式運行過程中真正需要使用到某個組件的時候才根據該組件在$_definitions中儲存的註冊資訊使用依賴注入容器\yii\di\Container進行組件執行個體的建立,然後把建立的執行個體存入私人成員變數$_components,這樣下次訪問相同組件的時候就可以直接返回組件執行個體,而不再需要執行建立過程了。yii的這個組件註冊與建立機制其實是大有裨益的,試想一下,如果在應用執行個體建立的時候就進行所有組件的建立,將會大大增加應用執行個體建立的時間,使用者每次重新整理頁面都會進行應用執行個體的建立的,也就是說使用者每重新整理一次頁面都很慢,這使用者體驗就很不好了,而且很多情況下有很多組件其實是沒有使用到的,但是我們還是花了不少時間去建立這些組件,這是很不明智的,所以yii的做法就是:先把組件參數資訊儲存起來,需要使用到哪些組件再去建立相應的執行個體,大大節省了應用建立的時間,同時也節省了記憶體,這種思路是很值得我們學習的!
總結