TP6依赖注入是如何实现的

avatar
开发工程师 @西安众邦网络科技有限公司

TP6依赖注入是如何实现的

先看下app/provider容器文件,次文件会在think\APP实例化的时候

直接从新绑定类到的容器上。复制原有容器中的类

可以先看下think\APP 构造方法中的处理逻辑

/**
 * 架构方法
 * @access public
 * @param string $rootPath 应用根目录
 */
public function __construct(string $rootPath = '')
{
	//设置thinkphp扩展的目录
	$this->thinkPath = dirname(__DIR__) . DIRECTORY_SEPARATOR;
	//项目更目录
	$this->rootPath = $rootPath ? rtrim($rootPath, DIRECTORY_SEPARATOR
	//应用根目录
	$this->appPath = $this->rootPath . 'app' . DIRECTORY_SEPARATOR;
	//runtime根目录
	$this->runtimePath = $this->rootPath . 'runtime' . DIRECTORY_SEPARATO
	//检测app/provider.php文件进行替换容器绑定
	if (is_file($this->appPath . 'provider.php')) {
		$this->bind(include $this->appPath . 'provider.php');
	}
	//将当前容器实例保存到成员变量「$instance」中,也就是容器自己保存自己的一个
	static::setInstance($this);
	//// 保存绑定的实例到「$instances」数组中,见对应分析
	$this->instance('app', $this);
	$this->instance('think\Container', $this);
}

在控制中可以使用app()->db 可以看到think\App中根本没有此属性,php的类中,调用一个类的不存在的属性就会自动进入魔术方法__get(),再来看看app类当中的__get方法,app类中没有找到集成的类中也就是think\Container 中直接搜索__get方法,就能找到。

think\Container
//$name就是就是没有定义的属性的名称
public function __get($name)
{
	return $this->get($name);
}

找到当前类的get方法,首先是检查了以下容器中有没有,没有就直接实例化,进行调用make方法进行创建类的实例化。

/**
  * 获取容器中的对象实例
  * @access public
  * @param string $abstract 类名或者标识
  * @return object
  */
public function get($abstract)
{
	if ($this->has($abstract)) {
		return $this->make($abstract);
	}
	throw new ClassNotFoundException('class not exists: ' . $abstract, $a
}

make主要检测有没实例化过由实例化过后就直接返回使用就行,没有就需要利用类的反射来创建类的实例化,可以看到调用了本类的invokeClass方法

public function make(string $abstract, array $vars = [], bool $newInstance =
{
    
	//如果已经存在实例,且不强制创建新的实例,直接返回已存在的实例
	if (isset($this->instances[$abstract]) && !$newInstance) {
		return $this->instances[$abstract];
	}
    
	//如果有绑定,比如 'http'=> 'think\Http',则 $concrete = 'think\Http'
	if (isset($this->bind[$abstract])) {
		$concrete = $this->bind[$abstract];
		if ($concrete instanceof Closure) {
			$object = $this->invokeFunction($concrete, $vars);
		} else {
			//重走一遍make函数,比如上面http的例子,则会调到后面「invokeClass
			return $this->make($concrete, $vars, $newInstance);
		}
	} else {
		//实例化需要的类,比如'think\Http'
		$object = $this->invokeClass($abstract, $vars);
	}
	if (!$newInstance) {
		$this->instances[$abstract] = $object;
	}
	return $object;
}

invokeClass 方法主要为了绑定参数然后进行实例化类,绑定参数由bindParams方法实现,而bindParams方法中的getObjectParam方法中又会回调make方法。

/**
  * 调用反射执行类的实例化 支持依赖注入
  * @access public
  * @param string $class 类名
  * @param array $vars 参数
  * @return mixed
  */
public function invokeClass(string $class, array $vars = [])
{
	try {
		//通过反射实例化类
		$reflect = new ReflectionClass($class);
		//检查是否有「__make」方法
		if ($reflect->hasMethod('__make')) {
			$method = new ReflectionMethod($class, '__make');
			//检查是否是公有方法且是静态方法
			if ($method->isPublic() && $method->isStatic()) {
				//绑定参数
				$args = $this->bindParams($method, $vars);
                //调用该方法(__make),因为是静态的,所以第一个参数是null
                //因此,可得知,一个类中,如果有__make方法,在类实例化之前会首
                return $method->invokeArgs(null, $args);
			}
		}
        //获取类的构造函数
        $constructor = $reflect->getConstructor();
        //有构造函数则绑定其参数
        $args = $constructor ? $this->bindParams($constructor, $vars) : [
        //根据传入的参数,通过反射,实例化类
        $object = $reflect->newInstanceArgs($args);
        // 执行容器回调
        $this->invokeAfter($class, $object);
        return $object;
	} catch (ReflectionException $e) {
		throw new ClassNotFoundException('class not exists: ' . $class, $
	}
}

getObjectParam方法中是拿到当前类实例化的参数,找到当前参数是否是类,如果是就会直接再次调用make方法,如果下个参数还是个类的实例化结果,会再次进行回调,这就是一个类中可以无限制的注入多个类的原理,所以在使用的当中运用app()->make()来进行获取类的例化,更加方便简洁

/**
  * 获取对象类型的参数值
  * @access protected
  * @param string $className 类名
  * @param array $vars 参数
  * @return mixed
  */
protected function getObjectParam(string $className, array &$vars)
{
    $array = $vars;
    $value = array_shift($array);
    if ($value instanceof $className) {
        $result = $value;
        array_shift($vars);
    } else {
        $result = $this->make($className);
    }
    return $result;
}

总的来说,整个过程大概是这样的:需要实例化 Db 类 ==> 提取构造函数发现其依赖App ==> 开始实例化 App 类(如果发现还有依赖,则一直提取下去,直到依赖全部加载完)==> 将实例化好的依赖(App 类的实例)传入 Db 类来实例化 Db 类。

感谢您的阅读,如果对您有帮助,欢迎关注"CRMEB"掘金号。码云上有我们开源的商城项目,知识付费项目,均是基于PHP开发,学习研究欢迎使用,关注我们保持联系!