1. 程式人生 > >yii2 隨筆(七)依賴注入——(3)yii2的依賴注入

yii2 隨筆(七)依賴注入——(3)yii2的依賴注入

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);
//註冊 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)
{
    //...
}