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

1、爲何須要依賴注入php

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

 

2、依賴注入是什麼緩存

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

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

 

3、依賴注入容器函數

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

  依賴注入容器能夠幫咱們實例化和配置對象及其全部依賴對象,它會遞歸分析類的依賴關係並實例化全部依賴,而不須要咱們去爲這個事情費神。this

  在yii2.0中,yii\di\Container就是依賴注入容器,這裏先簡單說一下這個容器的使用。咱們可使用該類的set()方法來註冊一個類的依賴,把依賴信息傳遞給它就能夠了,若是但願這個類是單例的,則可使用setSingleton()方法註冊依賴。註冊依賴以後,當你須要這個類的對象的時候,使用Yii::createObject(),把類的配置參數傳遞過去,yii\di\Container即會幫你解決這個類的全部依賴並建立一個對象返回。spa

 

4、yii依賴注入容器 - 依賴註冊orm

  好了,下面開始分析一下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));
	}
}

 

5、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()方法解決依賴,實例化全部依賴類對象,最後就是建立對象了。

相關文章
相關標籤/搜索