yii2的依賴注入的核心代碼在 yii\di,在這個包(檔案夾)下面有3個檔案,分別是Container.php(容器),Instance.php(執行個體),ServiceLocator(服務定位器),現在我們討論一下前兩個,服務定位器可以理解一個服務的註冊表,這個不影響我們討論依賴注入,它也是依賴注入的一種應用。
我們還是從代碼開始講解yii2是怎麼使用依賴注入的。
// 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::createObject()是把合格的“原材料”,交給“容器($container)”,來產生目標對象的,那麼容器就是我們“依賴注入”生產對象的地方。那麼$container是什麼時候引入的呢(注意這裡用的是 static::$container, 而不是 self::$container)?還記得在首頁匯入yii架構時的語句嗎?
//匯入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);//註冊 classMapYii::$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){ //...}
以上就是yii2 隨筆(七)依賴注入——(3)yii2的依賴注入的內容,更多相關內容請關注topic.alibabacloud.com(www.php.cn)!