yii2之依赖注入与依赖注入容器
一、为什么需要依赖注入
首先我们先不管什么是依赖注入,先来分析一下没有使用依赖注入会有什么样的结果。假设我们有一个gmail邮件服务类GMail,然后有另一个类User,User类需要使用发邮件的功能,于是我们在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的几个成员变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /** * @var array 存储单例对象,数组的键是对象所属类的名称 */ private $_singletons = []; /** * @var array 存储依赖定义,数组的键是对象所属类的名称 */ private $_definitions = []; /** * @var array 存储构造函数参数,数组的键是对象所属类的名称 */ private $_params = []; /** * @var array 存储类的反射类对象,数组的键是类名或接口名 */ private $_reflections = []; /** * @var array 存储类的依赖信息,数组的键是类名或接口名 */ private $_dependencies = []; |
其中前三个是用于依赖注册的时候存储一些类参数和依赖定义的,后两个则是用于存储依赖信息的,这样使用同一个类的时候不用每次都进行依赖解析,直接使用这两个变量缓存的依赖信息即可。
接下来看看依赖注册的两个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /** * 在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()方法,这个方法只是用于规范依赖定义的,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | /** * 规范依赖定义 * @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()方法,这个方法里面调用的是Container的get()方法。为了使得讲解的思路更清晰,这里我们先来看一下Container的另外两个方法,getDependencies()和resolveDependencies(),它们分别用于解析类的依赖信息和解决类依赖,会在对象实例化的过程中被调用。
下面先来看看getDependencies()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | /** * 解析指定类的依赖信息(利用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()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /** * 解决依赖 * @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()方法得到的关于某个类的依赖信息数组,对每个依赖的类调用Container的get()方法来获取对象实例化。前面说到get()函数实例化过程中会调用这个方法,而这里又调用了get()方法,所以已经可以知道,依赖解析的过程其实是一个递归解析的过程。
再回头来看看get()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | /** * 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()方法,这个方法又干了些什么呢?看源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | /** * 创建指定类的对象 * @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()方法解决依赖,实例化所有依赖类对象,最后就是创建对象了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
2015-09-14 网络地址转换NAT
2015-09-14 虚拟专用网VPN
2015-09-14 动态主机配置协议DHCP
2015-09-14 域名系统DNS