yii2之依賴注入與依賴注入容器


一、為什么需要依賴注入

  首先我們先不管什么是依賴注入,先來分析一下沒有使用依賴注入會有什么樣的結果。假設我們有一個gmail郵件服務類GMail,然后有另一個類UserUser類需要使用發郵件的功能,於是我們在User類中定義一個成員變量$mailServer,並且在聲明這個變量的時候就給它賦值一個GMail類對象,或者在User構造函數中進行GMail類實例化與賦值。這樣寫程序會有什么問題呢?試想一下,每次當我們需要把User使用的郵件服務改為其他類型郵件服務的時候,我們需要頻繁修改User類的成員變量$mailServer,這樣是不好的。問題的根源就在於,我們不該把User類的成員變量$mailServer的實例化寫死在User類內部,而應該在調用User類的時候可以動態決定賦值給$mailServer的對象類型,依賴注入就是來解決這個問題的。

 

二、依賴注入是什么

  所謂依賴注入,實質上就是當某個類對象需要使用另一個類實例的時候,不在類內部實例化另一個類,而將實例化的過程放在類外面實現,實例化完成后再賦值給類對象的某個屬性。這樣的話該類不需要知道賦值給它的屬性的對象具體屬於哪個類的,當需要改變這個屬性的類型的時候,無需對這個類的代碼進行任何改動,只需要在使用該類的地方修改實例化的代碼即可。

  依賴注入的方式有兩種:1.構造函數注入,將另一個類的對象作為參數傳遞給當前類的構造函數,在構造函數中給當前類屬性賦值;2.屬性注入,可以將該類某個屬性設置為public屬性,也可以編寫這個屬性的setter方法,這樣就可以在類外面給這個屬性賦值了。

 

三、依賴注入容器

  仔細思考一下,我們會發現,雖然依賴注入解決了可能需要頻繁修改類內部代碼的問題,但是卻帶來了另一個問題。每次我們需要用到某個類對象的時候,我們都需要把這個類依賴的類都實例化,所以我們需要重復寫這些實例化的代碼,而且當依賴的類又依賴於其他類的時候,我們還要找出所有依賴類依賴的其他類然后實例化,可想而知,這是一個繁瑣低效而又麻煩且容易出錯的過程。這個時候依賴注入容器應運而生,它就是來解決這個問題的。

  依賴注入容器可以幫我們實例化和配置對象及其所有依賴對象,它會遞歸分析類的依賴關系並實例化所有依賴,而不需要我們去為這個事情費神。

  在yii2.0中,yii\di\Container就是依賴注入容器,這里先簡單說一下這個容器的使用。我們可以使用該類的set()方法來注冊一個類的依賴,把依賴信息傳遞給它就可以了,如果希望這個類是單例的,則可以使用setSingleton()方法注冊依賴。注冊依賴之后,當你需要這個類的對象的時候,使用Yii::createObject(),把類的配置參數傳遞過去,yii\di\Container即會幫你解決這個類的所有依賴並創建一個對象返回。

 

四、yii依賴注入容器 - 依賴注冊

  好了,下面開始分析一下yii的依賴注入容器的實現原理。首先來看一下Container的幾個成員變量:

/**
 * @var array 存儲單例對象,數組的鍵是對象所屬類的名稱
 */
private $_singletons = [];

/**
 * @var array 存儲依賴定義,數組的鍵是對象所屬類的名稱
 */
private $_definitions = [];

/**
 * @var array 存儲構造函數參數,數組的鍵是對象所屬類的名稱
 */
private $_params = [];
/**
 * @var array 存儲類的反射類對象,數組的鍵是類名或接口名
 */
private $_reflections = [];

/**
 * @var array 存儲類的依賴信息,數組的鍵是類名或接口名
 */
private $_dependencies = [];

其中前三個是用於依賴注冊的時候存儲一些類參數和依賴定義的,后兩個則是用於存儲依賴信息的,這樣使用同一個類的時候不用每次都進行依賴解析,直接使用這兩個變量緩存的依賴信息即可。

  接下來看看依賴注冊的兩個方法:

/**
 * 在DI容器注冊依賴(注冊之后每次請求都將返回一個新的實例)
 * @param string $class:類名、接口名或別名
 * @param array $definition:類的依賴定義,可以是一個PHP回調函數,一個配置數組或者一個表示類名的字符串
 * @param array $params:構造函數的參數列表,在調用DI容器的get()方法獲取類實例的時候將被傳遞給類的構造函數
 * @return \yii\di\Container
 */
public function set($class, $definition = [], array $params = [])
{
	$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);//保存類配置信息
	$this->_params[$class] = $params;//保存構造函數參數列表
	unset($this->_singletons[$class]);//若存在單例依賴信息則刪除
	return $this;
}

/**
 * 在DI容器注冊依賴(注冊之后每次請求都將返回同一個實例)
 * @param string $class:類名、接口名或別名
 * @param array $definition:類的依賴定義,可以是一個PHP回調函數,一個配置數組或者一個表示類名的字符串
 * @param array $params:構造函數的參數列表,在調用DI容器的get()方法獲取類實例的時候將被傳遞給類的構造函數
 * @return \yii\di\Container
 */
public function setSingleton($class, $definition = [], array $params = [])
{
	$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
	$this->_params[$class] = $params;
	$this->_singletons[$class] = null;//賦值null表示尚未實例化
	return $this;
}

這兩個方法很簡單,就是把依賴注冊傳入的參數信息保存下來,提供給實例化過程使用。這兩個方法中都調用了normalizeDefinition()方法,這個方法只是用於規范依賴定義的,源碼如下:

/**
 * 規范依賴定義
 * @param string $class:類名稱
 * @param array $definition:依賴定義
 * @return type
 * @throws InvalidConfigException
 */
protected function normalizeDefinition($class, $definition)
{
	if (empty($definition)) {//若為空,將$class作為類名稱
		return ['class' => $class];
	} elseif (is_string($definition)) {//若是字符串,默認其為類名稱
		return ['class' => $definition];
	} elseif (is_callable($definition, true) || is_object($definition)) {//若是PHP回調函數或對象,直接作為依賴的定義
		return $definition;
	} 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));
	}
}

 

五、yii依賴注入容器 - 對象實例化

  接下來就是重頭戲了,yii依賴注入容器是怎么根據依賴注冊的信息實現對象實例化的呢?我們一步一步來分析。在第三部分我們說到,當需要創建一個類對象的時候,我們調用的時候Yii::createObject()方法,這個方法里面調用的是Containerget()方法。為了使得講解的思路更清晰,這里我們先來看一下Container的另外兩個方法,getDependencies()resolveDependencies(),它們分別用於解析類的依賴信息和解決類依賴,會在對象實例化的過程中被調用。

  下面先來看看getDependencies()方法:

/**
 * 解析指定類的依賴信息(利用PHP的反射機制)
 * @param string $class:類名、接口名或別名
 * @return type
 */
protected function getDependencies($class)
{
	if (isset($this->_reflections[$class])) {//存在該類的依賴信息緩存,直接返回
		return [$this->_reflections[$class], $this->_dependencies[$class]];
	}

	$dependencies = [];
	$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();
				$dependencies[] = Instance::of($c === null ? null : $c->getName());
			}
		}
	}

	//保存該類的反射類對象和依賴信息
	$this->_reflections[$class] = $reflection;
	$this->_dependencies[$class] = $dependencies;

	return [$reflection, $dependencies];
}

首先判斷該類是已被解析過,如果是,直接返回緩存中該類的依賴信息,否則,利用PHP的反射機制對類的依賴進行分析,最后將分析所得依賴信息緩存,具體步驟已在代碼中注明。其中Instance類實例用於表示一個指定名稱的類的對象引用,也就是說getDependencies()方法調用之后,得到的$dependencies只是某個類的依賴信息,指明這個類依賴於哪些類,還沒有將這些依賴的類實例化,這個工作是由resolveDependencies()方法來完成的。

   再來看看resolveDependencies()方法:

/**
 * 解決依賴
 * @param array $dependencies:依賴信息
 * @param ReflectionClass $reflection:放射類對象
 * @return type
 * @throws InvalidConfigException
 */
protected function resolveDependencies($dependencies, $reflection = null)
{
	foreach ($dependencies as $index => $dependency) {//遍歷依賴信息數組,把所有的對象引用都替換為對應類的實例對象
		if ($dependency instanceof Instance) {
			if ($dependency->id !== null) {//組件id不為null,以id為類名實例化對象
				$dependencies[$index] = $this->get($dependency->id);
			} elseif ($reflection !== null) {//若id為null但$reflection不為null,通過$reflection獲取構造函數類型,報錯。。
				$name = $reflection->getConstructor()->getParameters()[$index]->getName();
				$class = $reflection->getName();
				throw new InvalidConfigException("Missing required parameter \"$name\" when instantiating \"$class\".");
			}
		}
	}
	return $dependencies;
}

這個方法就是遍歷getDependencies()方法得到的關於某個類的依賴信息數組,對每個依賴的類調用Containerget()方法來獲取對象實例化。前面說到get()函數實例化過程中會調用這個方法,而這里又調用了get()方法,所以已經可以知道,依賴解析的過程其實是一個遞歸解析的過程。

  再回頭來看看get()方法:

/**
 * DI容器返回一個請求類的實例
 * @param string $class:類名
 * @param array $params:構造函數參數值列表,按照構造函數中參數的順序排列
 * @param array $config:用於初始化對象屬性的配置數組
 * @return type
 * @throws InvalidConfigException
 */
public function get($class, $params = [], $config = [])
{
	if (isset($this->_singletons[$class])) {//若存在此類的單例則直接返回單例
		return $this->_singletons[$class];
	} elseif (!isset($this->_definitions[$class])) {//若該類未注冊依賴,調用build()函數,使用PHP的反射機制獲取該類的依賴信息,解決依賴,創建類對象返回
		return $this->build($class, $params, $config);
	}

	$definition = $this->_definitions[$class];

	if (is_callable($definition, true)) {//若依賴定義是一個PHP函數則直接調用這個函數創建對象
		$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;
}

首先判斷是否存在所需類的單例,若存在則直接返回單例,否則判斷該類是否已注冊依賴,若已注冊依賴,則根據注冊的依賴定義創建對象,具體每一步已在代碼注釋說明。其中mergeParams()方法只是用來合並用戶指定的參數和依賴注冊信息中的參數。若未注冊依賴,則調用build()方法,這個方法又干了些什么呢?看源碼:

/**
 * 創建指定類的對象
 * @param string $class:類名稱
 * @param array $params:構造函數參數列表
 * @param array $config:初始化類對象屬性的配置數組
 * @return type
 * @throws NotInstantiableException
 */
protected function build($class, $params, $config)
{
	list ($reflection, $dependencies) = $this->getDependencies($class);//獲取該類的依賴信息

	foreach ($params as $index => $param) {//將構造函數參數列表加入該類的依賴信息中
		$dependencies[$index] = $param;
	}

	$dependencies = $this->resolveDependencies($dependencies, $reflection);//解決依賴,實例化所有依賴的對象
	if (!$reflection->isInstantiable()) {//類不可實例化
		throw new NotInstantiableException($reflection->name);
	}
	if (empty($config)) {//配置數組為空,使用依賴信息數組創建對象
		return $reflection->newInstanceArgs($dependencies);
	}

	if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {
		$dependencies[count($dependencies) - 1] = $config;//按照 Object 類的要求,構造函數的最后一個參數為 $config 數組
		return $reflection->newInstanceArgs($dependencies);
	} else {//先使用依賴信息創建對象,再使用$config配置初始化對象屬性
		$object = $reflection->newInstanceArgs($dependencies);
		foreach ($config as $name => $value) {
			$object->$name = $value;
		}
		return $object;
	}
}

首先,由於沒有類的依賴信息,調用getDependencies()方法分析得到依賴信息。然后調用resolveDependencies()方法解決依賴,實例化所有依賴類對象,最后就是創建對象了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM