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()方法解决依赖,实例化全部依赖类对象,最后就是建立对象了。

相关文章
相关标签/搜索