yii2 隨筆(七)依賴注入——(3)yii2的依賴注入
阿新 • • 發佈:2019-02-01
yii2的依賴注入的核心程式碼在 yii\di,在這個包(資料夾)下面有3個檔案,分別是Container.php(容器),Instance.php(例項),ServiceLocator(服務定位器),現在我們討論一下前兩個,服務定位器可以理解一個服務的登錄檔,這個不影響我們討論依賴注入,它也是依賴注入的一種應用。
我們還是從程式碼開始講解yii2是怎麼使用依賴注入的。
通過閱讀上面程式碼,Yii::createObject()是把合格的“原材料”,交給“容器($container)”,來生成目標物件的,那麼容器就是我們“依賴注入”生產物件的地方。那麼$container是什麼時候引入的呢(注意這裡用的是 static::$container, 而不是 self::$container)?還記得在首頁匯入yii框架時的語句麼?// yii\base\application //這個是yii2的依賴注入使用入口,引數的解釋請參考原始碼,這裡不多解釋 public static function createObject($type, array $params = []) { if (is_string($type)) {//type 是字串的話,它就把type當做一個物件的“原材料”,直接把它傳給容器並通過容器得到想要的物件。 return static::$container->get($type, $params); } elseif (is_array($type) && isset($type['class'])) {//type 是陣列,並且有class的鍵,經過簡單處理後,得到物件的“原材料”,然後把得到的“原材料”傳給容器並通過容器得到想要的物件。 $class = $type['class']; unset($type['class']); return static::$container->get($class, $params, $type); } elseif (is_callable($type, true)) {//如果type是可呼叫的結構,就直接呼叫 return call_user_func($type, $params); } elseif (is_array($type)) {//如果type是array,並且沒有'class'的鍵值,那麼就丟擲異常 throw new InvalidConfigException('Object configuration must be an array containing a "class" element.'); } else {//其他情況,均丟擲另一個異常,說type不支援的配置型別 throw new InvalidConfigException("Unsupported configuration type: " . gettype($type)); } }
//匯入yii框架
require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
程式碼如下
//引入基本的yii框架 require(__DIR__ . '/BaseYii.php'); //只是做了繼承,這裡給我們留了二次開發的餘地,雖然很少能用到 class Yii extends \yii\BaseYii { } //設定自動載入 spl_autoload_register(['Yii', 'autoload'], true, true); //註冊 classMap Yii::$classMap = require(__DIR__ . '/classes.php'); //註冊容器 Yii::$container = new yii\di\Container();
你看的沒錯!就是最後一句話,yii2 把 yii\di\Container 的實現拿給自己使用。接下來,我們討論一下容器是怎麼實現的?
接著上面的 static::$container->get() 的方法,在講解get方法之前,我們要先了解一下容器的幾個屬性,這將有助於理解get的實現
$_singletons; // 單例陣列,它的鍵值是類的名字,如果生成的物件是單例,則把他儲存到這個數組裡,值為null的話,表示它還沒有被例項化 $_definitions;// 定義陣列,它的鍵值是類的名字,值是生成這個類所需的“原材料”,在set 或 setSingleton的時候寫入 $_params; // 引數,它的鍵值是類的名字,值是生成這個類所需的額外的“原材料”,在set 或 setSingleton的時候寫入 $_reflections; //反射,它的鍵值是類的名字,值是要生成的物件的反射控制代碼,在生成物件的時候寫入 $_dependencies;//依賴,它的鍵值是類的名字,值是要生成物件前的一些必備“原材料”,在生成物件的時候,通過反射函式得到。
ok,如果你夠細心地話,理解了上面的幾個屬性,估計你就對yii2的容器有個大概的瞭解了,這裡還是從get開始。
public function get($class, $params = [], $config = [])
{
if (isset($this->_singletons[$class])) {//檢視將要生成的物件是否在單例裡,如果是,則直接返回
// singleton
return $this->_singletons[$class];
} elseif (!isset($this->_definitions[$class])) {//如果沒有要生成類的定義,則直接生成,yii2自身大部分走的是這部分,並沒有事先在容器裡註冊什麼,那麼配置檔案是在哪裡註冊呢?還記的文章最開始的時候的"服務定位器"麼?我們在服務定位器裡講看到這些。
return $this->build($class, $params, $config);
}
//如果已經定義了這個類,則取出這個類的定義
$definition = $this->_definitions[$class];
if (is_callable($definition, true)) {//如果定義是可呼叫的結構
//先整合一下引數,和$_params裡是否有這個類的引數,如果有則和傳入的引數以傳入覆蓋定義的方式整和在一起
//然後再檢查整合後的引數是否符合依賴,就是說是否有必填的引數,如果有直接丟擲異常,否則返回引數。檢查依賴的時候,需要判斷是否為例項(Instance),如果是,則要實現例項。注意:這裡出現了Instance。
$params = $this->resolveDependencies($this->mergeParams($class, $params));
//把引數專遞給可呼叫結果,返回結果
$object = call_user_func($definition, $this, $params, $config);
} elseif (is_array($definition)) {//如果定義是一個數組
//把代表要生成的class取出
$concrete = $definition['class'];
//登出這個鍵值
unset($definition['class']);
//把定義 和 配置整合成新的定義
$config = array_merge($definition, $config);
//整合引數
$params = $this->mergeParams($class, $params);
//如果傳入的$class 和 定義裡的class完全一樣,則直接生成,build第一個引數確保為真實的類名,而傳入的$type可能是別名
if ($concrete === $class) {
$object = $this->build($class, $params, $config);
} else {//如果是別名,則回撥自己,生成物件,因為這時的類也有可能是別名
$object = $this->get($concrete, $params, $config);
}
} elseif (is_object($definition)) {//如果定義是一個物件,則代表這個類是個單例,儲存到單例裡,並返回這個單例,這裡要自己動腦想一下,為什麼是個物件就是單例?只可意會不可言傳,主要是我也組織不好語言怎麼解釋它。
return $this->_singletons[$class] = $definition;
} else {//什麼都不是則丟擲異常
throw new InvalidConfigException("Unexpected object definition type: " . gettype($definition));
}
//判斷這個類的名字是否在單例裡,如果在,則把生成的物件放到單例裡
if (array_key_exists($class, $this->_singletons)) {
// singleton
$this->_singletons[$class] = $object;
}
//返回生成的物件
return $object;
}
研究到這裡,我們發現 get 函式僅僅是個“入口”而已,主要的功能在build裡
//建立物件
protected function build($class, $params, $config)
{
//通過類名得到反射控制代碼,和依賴(依賴就是所需引數)
//所以前面提到,傳輸buile的第一個引數必須為有效的“類名”否則,會直接報錯
list ($reflection, $dependencies) = $this->getDependencies($class);
//把依賴和引數配置,因為依賴可能有預設引數,這裡覆蓋預設引數
foreach ($params as $index => $param) {
$dependencies[$index] = $param;
}
//確保依賴沒問題,所有原材料是否都ok了,否則丟擲異常
$dependencies = $this->resolveDependencies($dependencies, $reflection);
if (empty($config)) {//如果config為空,則返回目標物件
return $reflection->newInstanceArgs($dependencies);
}
if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {//如果目標物件是 Configurable的介面
// set $config as the last parameter (existing one will be overwritten)
$dependencies[count($dependencies) - 1] = $config;
return $reflection->newInstanceArgs($dependencies);
} else {//其他的情況下
$object = $reflection->newInstanceArgs($dependencies);
foreach ($config as $name => $value) {
$object->$name = $value;
}
return $object;
}
}
好了,build到這裡就結束了,下面我們一起看看容器是怎麼得到反射控制代碼和依賴關係的
protected function getDependencies($class)
{
if (isset($this->_reflections[$class])) {//是否已經解析過目標物件了
return [$this->_reflections[$class], $this->_dependencies[$class]];
}
$dependencies = [];//初始化依賴陣列
$reflection = new ReflectionClass($class);//得到目標物件的反射,請參考php手冊
$constructor = $reflection->getConstructor();//得到目標物件的建構函式
if ($constructor !== null) {//如果目標物件有建構函式,則說明他有依賴
//解析所有的引數,注意得到引數的順序是從左到右的,確保依賴時也是按照這個順序執行
foreach ($constructor->getParameters() as $param) {
if ($param->isDefaultValueAvailable()) {//如果引數的預設值可用
$dependencies[] = $param->getDefaultValue();//把預設值放到依賴裡
} else {//如果是其他的
$c = $param->getClass();//得到引數的型別,如果引數的型別不是某類,是基本型別的話,則返回null
//如果,是基本型別,則生成null的例項,如果不是基本型別,則生成該類名的例項。注意:這裡用到了例項(Instance)
$dependencies[] = Instance::of($c === null ? null : $c->getName());
}
}
}
//把引用儲存起來,以便下次直接使用
$this->_reflections[$class] = $reflection;
//把依賴存起來,以便下次直接使用
$this->_dependencies[$class] = $dependencies;
//返回結果
return [$reflection, $dependencies];
}
下面我們來看看容器是怎麼確保依賴關係的
protected function resolveDependencies($dependencies, $reflection = null)
{
//拿到依賴關係
foreach ($dependencies as $index => $dependency) {
//如果依賴是一個例項,因為經過處理的依賴,都是Instance的物件
if ($dependency instanceof Instance) {
if ($dependency->id !== null) {//這個例項有id,則通過這個id生成這個物件,並且代替原來的引數
$dependencies[$index] = $this->get($dependency->id);
} elseif ($reflection !== null) {//如果反射控制代碼不為空,注意這個函式是protected 型別的,所以只有本類或者本類的衍生類可訪問,但是本類裡只有兩個地方用到了,一個是 get 的時候,如果目標物件是可呼叫的結果(is_callable),那麼$reflection===null,另外一個build的時候,$reflection不為空,這個時候代表目標物件有一個必須引數,但是還不是一個例項(Instance的物件),這個時候代表缺乏必須的“原材料”丟擲異常
//則拿到響應的必填引數名字,並且丟擲異常
$name = $reflection->getConstructor()->getParameters()[$index]->getName();
$class = $reflection->getName();
throw new InvalidConfigException("Missing required parameter \"$name\" when instantiating \"$class\".");
}
}
}
//確保了所有的依賴後,返回所有依賴,如果目標是is_callable($definition, true),則不會丟擲異常,僅僅把Instance型別的引數例項化出來。
return $dependencies;
}
看到這裡,我們就可以瞭解了yii2是怎麼使用容器實現“依賴注入”了,那麼有個問題,閉包的依賴怎麼保證呢?我想是因為yii2認為閉包的存在解決的是侷限性的問題,不存在依賴性,或者依賴是交給開發者自行解決的。另外yii2的容器,如果引數是閉包的話,就會出現錯誤,因為對閉包的依賴,解析閉包引數的時候,會得到$dependencies[]
= Instance::of($c === null ? null : $c->getName());得到的就是一個 Closure 的例項,而後面 例項化這個例項的時候,就會出現問題了,所以用yii2的容器實現物件的時候,被實現的物件不能包含閉包引數,如果有閉包引數,則一定要有預設值,或者人為保證會傳入這個閉包引數,繞過自動生成的語句。ok容器的主要函式就有這些了,其他方法,set,setSingleton,has,hasSingleton,clear一看就知道什麼意思,另外這些方法基本上沒有在框架中使用(可以在這些函式寫exit,看看你的頁面會不會空白),或者你用容器自己生成一些東西的話,可以自行檢視這些函式的用法。
最後,我們來看看Instance到底扮演了什麼角色
//yii\di\Instance
//很詫異吧,就是例項化一個自己,注意這個自己是 static,以後你可能需要用到這個地方
public static function of($id)
{
return new static($id);
}
[/php]
那麼這個函式的建構函式呢?
[php]
//禁止外部例項化
protected function __construct($id)
{
//賦值id
$this->id = $id;
}
在容器中,就用到了Instance的這兩個方法,說明Instance在例項中,只是確保了依賴的可用性。此外Instance還提供了其他的函式,其中 get 得到的是當前Instance所對應的id的例項化物件,另外,還有一個靜態函式ensure
//確保 $reference 是 $type型別的,如果不是則丟擲異常
//在框架中多次用到,請自行查詢
//另外,如果$type==null的時候,他也可以當做依賴注入的入口,使用方法請自行檢視原始碼,到現在你應該可以自己看懂這些程式碼了。
public static function ensure($reference, $type = null, $container = null)
{
//...
}