天天看點

Yii學習筆記 依賴注入和依賴注入容器(一)

依賴注入和依賴注入容器

DI容器的資料結構

在DI容器中,維護了5個數組,這是DI容器功能實作的基礎:

// 用于儲存單例Singleton對象,以對象類型為鍵
	private $_singletons = [];

	// 用于儲存依賴的定義,以對象類型為鍵
	private $_definitions = [];

	// 用于儲存構造函數的參數,以對象類型為鍵
	private $_params = [];

	// 用于緩存ReflectionClass對象,以類名或接口名為鍵
	private $_reflections = [];

	// 用于緩存依賴資訊,以類名或接口名為鍵
	private $_dependencies = [];
           

注入依賴.

這兩個函數,本質上隻是将依賴的有關資訊寫入到容器的相應數組中去。 在 set() 和 setSingleton() 中,

public function set($class, $definition = [], array $params = [])
{
    // 規範化 $definition 并寫入 $_definitions[$class]
    $this->_definitions[$class] = $this->normalizeDefinition($class,
        $definition);

    // 将構造函數參數寫入 $_params[$class]
    $this->_params[$class] = $params;

    // 删除$_singletons[$class]
    unset($this->_singletons[$class]);
    return $this;
}

public function setSingleton($class, $definition = [], array $params = [])
{
    // 規範化 $definition 并寫入 $_definitions[$class]
    $this->_definitions[$class] = $this->normalizeDefinition($class,
        $definition);

    // 将構造函數參數寫入 $_params[$class]
    $this->_params[$class] = $params;

    // 将$_singleton[$class]置為null,表示還未執行個體化
    $this->_singletons[$class] = null;
    return $this;
}
           

首先調用 yii\di\Container::normalizeDefinition() 對依賴的定義進行規範化處理,其代碼如下:

protected function normalizeDefinition($class, $definition)
 {
     // $definition 是空的轉換成 ['class' => $class] 形式
     if (empty($definition)) {
         return ['class' => $class];

     // $definition 是字元串,轉換成 ['class' => $definition] 形式
     } elseif (is_string($definition)) {
         return ['class' => $definition];

     // $definition 是PHP callable 或對象,則直接将其作為依賴的定義
     } elseif (is_callable($definition, true) || is_object($definition)) {
         return $definition;

     // $definition 是數組則確定該數組定義了 class 元素
     } elseif (is_array($definition)) {
         if (!isset($definition['class'])) {
             if (strpos($class, '\\') !== false) {
                 $definition['class'] = $class;
             } else {
                 throw new InvalidConfigException(
                     "A class definition requires a \"class\" member.");
             }
         }
         return $definition;
     // 這也不是,那也不是,那就抛出異常算了
     } else {
         throw new InvalidConfigException(
             "Unsupported definition type for \"$class\": "
             . gettype($definition));
     }
 }
           

注冊依賴

使用DI容器,首先要告訴容器,類型及類型之間的依賴關系,聲明一這關系的過程稱為注冊依賴。 使用 yii\di\Container::set() 和 yii\di\Container::setSinglton() 可以注冊依賴。

依賴的定義隻是往特定的資料結構 $_singletons $_definitions 和 $_params 3個數組寫入有關的資訊。

解析依賴資訊

這一過程會涉及到DI容器中尚未提到的另外2個數組 $_reflections 和 $_dependencies 。

yii\di\Container::getDependencies()

會向這2個數組寫入資訊,而這個函數又會在建立執行個體時,由 yii\di\Container::build() 所調用

protected function getDependencies($class)
{
   // 如果已經緩存了其依賴資訊,直接傳回緩存中的依賴資訊
   if (isset($this->_reflections[$class])) {
       return [$this->_reflections[$class], $this->_dependencies[$class]];
   }

   $dependencies = [];

   // 使用PHP5 的反射機制來擷取類的有關資訊,主要就是為了擷取依賴資訊
   $reflection = new ReflectionClass($class);

   // 通過類的建構函數的參數來了解這個類依賴于哪些單元
   $constructor = $reflection->getConstructor();
   if ($constructor !== null) {
       foreach ($constructor->getParameters() as $param) {
           if ($param->isDefaultValueAvailable()) {

               // 構造函數如果有預設值,将預設值作為依賴。即然是預設值了,
               // 就肯定是簡單類型了。
               $dependencies[] = $param->getDefaultValue();
           } else {
               $c = $param->getClass();

               // 構造函數沒有預設值,則為其建立一個引用。
               // 就是前面提到的 Instance 類型。
               $dependencies[] = Instance::of($c === null ? null :
                   $c->getName());
           }
       }
   }

   // 将 ReflectionClass 對象緩存起來
   $this->_reflections[$class] = $reflection;

   // 将依賴資訊緩存起來
   $this->_dependencies[$class] = $dependencies;

   return [$reflection, $dependencies];
}
           

前面講了 $_reflections 數組用于緩存 ReflectionClass 執行個體,$_dependencies 數組用于緩存依賴資訊。 這個 yii\di\Container::getDependencies() 方法實質上就是通過PHP5 的反射機制, 通過類的構造函數的參數分析他所依賴的單元。然後統統緩存起來備用。

另一個與解析依賴資訊相關的方法就是 yii\di\Container::resolveDependencies()

protected function resolveDependencies($dependencies, $reflection = null)
{
    foreach ($dependencies as $index => $dependency) {

        // 前面getDependencies() 函數往 $_dependencies[] 中
        // 寫入的是一個 Instance 數組
        if ($dependency instanceof Instance) {
            if ($dependency->id !== null) {

                // 向容器索要所依賴的執行個體,遞歸調用 yii\di\Container::get()
                $dependencies[$index] = $this->get($dependency->id);
            } elseif ($reflection !== null) {
                $name = $reflection->getConstructor()
                    ->getParameters()[$index]->getName();
                $class = $reflection->getName();
                throw new InvalidConfigException(
                "Missing required parameter \"$name\" when instantiating \"$class\".");
            }
        }
    }
    return $dependencies;
}
           

$_reflections 以類(接口、别名)名為鍵, 緩存了這個類(接口、别名)的ReflcetionClass。一經緩存,便不會再更改。

$_dependencies 以類(接口、别名)名為鍵,緩存了這個類(接口、别名)的依賴資訊。

這兩個緩存數組都是在 yii\di\Container::getDependencies() 中完成。這個函數隻是簡單地向數組寫入資料。

經過 yii\di\Container::resolveDependencies() 處理,DI容器會将依賴資訊轉換成執行個體。 這個執行個體化的過程中,是向容器索要執行個體。也就是說,有可能會引起遞歸。

執行個體的建立

yii\di\Container::build()
protected function build($class, $params, $config)
{
    // 調用上面提到的getDependencies來擷取并緩存依賴資訊,留意這裡 list 的用法
    list ($reflection, $dependencies) = $this->getDependencies($class);

    // 用傳入的 $params 的内容補充、覆寫到依賴資訊中
    foreach ($params as $index => $param) {
        $dependencies[$index] = $param;
    }
    // 這個語句是兩個條件:
    // 一是要建立的類是一個 yii\base\Object 類,
    // 留意我們在《Yii基礎》一篇中講到,這個類對于構造函數的參數是有一定要求的。
    // 二是依賴資訊不為空,也就是要麼已經注冊過依賴,
    // 要麼為build() 傳入構造函數參數。
    if (!empty($dependencies) && is_a($class, 'yii\base\Object', true)) {
        // 按照 Object 類的要求,構造函數的最後一個參數為 $config 數組
        $dependencies[count($dependencies) - 1] = $config;

        // 解析依賴資訊,如果有依賴單元需要提前執行個體化,會在這一步完成
        $dependencies = $this->resolveDependencies($dependencies, $reflection);

        // 執行個體化這個對象
        return $reflection->newInstanceArgs($dependencies);
    } else {
        // 會出現異常的情況有二:
        // 一是依賴資訊為空,也就是你前面又沒注冊過,
        // 現在又不提供構造函數參數,你讓Yii怎麼執行個體化?
        // 二是要構造的類,根本就不是 Object 類。
        $dependencies = $this->resolveDependencies($dependencies, $reflection);
        $object = $reflection->newInstanceArgs($dependencies);
        foreach ($config as $name => $value) {
            $object->$name = $value;
        }
        return $object;
    }
}
           

容器内容執行個體化的大緻過程

與注冊依賴時使用 set() 和 setSingleton() 對應,擷取依賴執行個體化對象使用 yii\di\Container::get() ,其代碼如下:

public function get($class, $params = [], $config = [])
{
    // 已經有一個完成執行個體化的單例,直接引用這個單例
    if (isset($this->_singletons[$class])) {
        return $this->_singletons[$class];

    // 是個尚未注冊過的依賴,說明它不依賴其他單元,或者依賴資訊不用定義,
    // 則根據傳入的參數建立一個執行個體
    } elseif (!isset($this->_definitions[$class])) {
        return $this->build($class, $params, $config);
    }

    // 注意這裡建立了 $_definitions[$class] 數組的副本
    $definition = $this->_definitions[$class];

    // 依賴的定義是個 PHP callable,調用之
    if (is_callable($definition, true)) {
        $params = $this->resolveDependencies($this->mergeParams($class,
            $params));
        $object = call_user_func($definition, $this, $params, $config);

    // 依賴的定義是個數組,合并相關的配置和參數,建立之
    } elseif (is_array($definition)) {
        $concrete = $definition['class'];
        unset($definition['class']);

        // 合并将依賴定義中配置數組和參數數組與傳入的配置數組和參數數組合并
        $config = array_merge($definition, $config);
        $params = $this->mergeParams($class, $params);

        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)) {
        $this->_singletons[$class] = $object;
    }

    return $object;
}
           

get() 接受3個參數:

$class 表示将要建立或者擷取的對象。可以是一個類名、接口名、别名。

$params 是一個用于這個要建立的對象的構造函數的參數,其參數順序要與構造函數的定義一緻。 通常用于未定義的依賴。

$config 是一個配置數組,用于配置擷取的對象。通常用于未定義的依賴,或覆寫原來依賴中定義好的配置。

get() 解析依賴擷取對象是一個自動遞歸的過程,也就是說,當要擷取的對象依賴于其他對象時, Yii會自動擷取這些對象及其所依賴的下層對象的執行個體。 同時,即使對于未定義的依賴,DI容器通過PHP的Reflection API,也可以自動解析出目前對象的依賴來。

get() 不直接執行個體化對象,也不直接解析依賴資訊。而是通過 build() 來執行個體化對象和解析依賴。

get() 會根據依賴定義,遞歸調用自身去擷取依賴單元。 是以,在整個執行個體化過程中,一共有兩個地方會産生遞歸:一是 get() , 二是 build() 中的 resolveDependencies() 。

DI容器解析依賴執行個體化對象過程大體上是這麼一個流程:

  1. 以傳入的 $class 看看容器中是否已經有執行個體化好的單例,如有,直接傳回這一單例。
2如果這個 $class 根本就未定義依賴,則調用 build() 建立之。

3對于已經定義了這個依賴,如果定義為PHP callable,則解析依賴關系,并調用這個PHP callable。

4如果依賴的定義是一個數組,首先取得定義中對于這個依賴的 class 的定義。 然後将定義中定義好的參數數組和配置數組與傳入的參數數組和配置數組進行合并, 并判斷是否達到終止遞歸的條件。進而選擇繼續遞歸解析依賴單元,或者直接建立依賴單元。