您的位置:首页 > 编程语言 > PHP开发

Yii框架依赖注入DI容器

2017-05-10 00:20 661 查看
背景:前几天在segmentfault花了两个小时学习了《自己动手造轮子, 实现一个现代的PHP框架》,当然这个课程只能是给学习者提供一个思路,比如说一个框架应该由哪些组件组成,然后再大致讲解下实现每个组件的思路。课程中涉及到依赖注入容器的实现,我之前并没有研究过,所以上课时简直一脸懵圈,正好最近在学习Yii框架,现在就从Yii框架-依赖注入(DI)容器-开始吧!

基本概念

依赖注入(Dependency Injection)

降低了依赖与被依赖类型之间的耦合

可以单独维护被依赖类型的创建过程

控制反转(Inversion of Control)

转移依赖关系 降低耦合

依赖抽象类型而非具体类型

依赖注入容器(Dependency Injection Container)

管理应用程序中的全局对象

自动维护对象之间的依赖关系 (自动注入依赖)

可以延迟加载对象(仅用到时才创建对象)。

启动容器后,所有对象直接取用

其实我觉得这个依赖注入和控制反转其实是同一个东西,只不过所强调有所不同,IOC强调的是一种降低依赖与被依赖类型之间耦合的模式,而依赖注入则是强调注入这个动作,把被依赖对象从程序中剥离出来,使用动态的注入方式。

代码演示

没有代码的说明,可能对上述概念还会比较模糊,下面先引入问题,再尝试用上述的概念指导改进代码的实现

为什么需要控制反转

假设应用程序有发送短信的需求,如果使用耦合度最高方式,可能是这样的

class Register
{
public $sender;
public function __construct()
{
$this->sender = new Alidayu();
}
public function sendMessage($phone,$content)
{
$this->sender->send($phone,$content);
}
}
class Alidayu
{
public function send($phone,$content)
{
//发送短信
}
}

$register = new Register();
$register->sendMessage('13312341234','注册成功');


当我们不使用阿里大于,而改用其他第三方短信服务时,那我们还需要对Register代码进行改动。显然,Alidayu和Register之间耦合度很高。

控制反转代码实现

依据控制反转的概念,我们在Register中的依赖应当基于抽象,而非基于具体的实现,对上述代码做出修改

Interface Sender
{
public function send($phone,$content);
}
class yunliantong implements Sender
{
public function send($phone,$content)
{
//发送短信
}
}
class Register()
{
public $sender;
public function __construct(Sender $sender)
{
$this->sender = $sender;
}
}
$sender   = new yunliantong();
$register = new Register($sender);
$register->sender->send('1331234567','注册成功');


这就是IOC控制反转,从原来的依赖具体类型,到现在的依赖抽象类型,把被依赖的类型解放出来,允许被依赖的类型独立变化,使用的时候再动态的注入依赖的对象中。

现在,你仔细分析,这个控制反转是和依赖注入是否是同一个东西,实现这个控制翻转,是通过依赖注入这个动作来完成的

现在看来,上述代码似乎并没有什么太大问题了。

但如果这个Register依赖的对象有多个,同时这些依赖的对象又依赖了其他的对象,这样的话,代码有可能是下面这样的。

$db     = new db();
$user   = new user($db);
$Mail   = new Mail();
$Mailer = new qqMailer($Mail);
$sender = new yunliantong();
...
如果还有更多的话 。。。
...
$register = new Register($sender,$Mailer,$user);


这样手动的为对象注入依赖,会带来许多不便,因为我们必需要注意创建对象的顺序,那能不能做到自动的注入依赖?注意,是自动,你想到了什么?

对,就是递归地创建依赖的对象。

比如说A依赖B,B依赖C,C无依赖。

现在我直接取出A的实例,系统自动帮我创建好它们之间的依赖关系,流程应该是这样的。



那系统怎么知道A有没有依赖,如果有依赖,那又怎么知道是依赖何方神圣?答案是使用PHP5 的反射机制,详情见php-manual-reflection

反射是指在PHP运行状态中,扩展分析PHP程序,导出或提取出关于类、方法、属性、参数等的详细信息,包括注释。这种动态获取的信息以及动态调用对象的方法的功能称为反射API。反射是操纵面向对象范型中元模型的API,其功能十分强大,可帮助我们构建复杂,可扩展的应用。

其用途如:自动加载插件,自动生成文档,甚至可用来扩充PHP语言。

为了配合下面的yii DI容器的源码解读,对将使用的类方法进行简单的说明。

使用ReflectionClass返回一个类的内部信息

$reflector = new ReflectionClass('类名称');


使用getConstructor( ) 获取类的构造函数

$constructor = $reflector->getConstrutor();


使用getParameters( ) 获取函数参数

$params = $constructor->getParameters();


使用isDefaultValueAvailable( ) 判断参数是否有默认值

使用getDefaultValue( )获取默认值

使用getClass( )获取参数的暗示的类

foreach($params as $param)
{
if ($param->isDefaultValueAvaliable()) {
echo
16490
$param->getDefaultValue();
}else{
echo $param->getClass();
}
}


使用newInstanceArgs 从指定的参数生成类的新实例.

class A
{
public $name;
public function __construct($name)
{
$this->name = $name;
}
}
$reflector = new ReflectionClass('A');
$object = $reflector->newInstanceArgs(['I AM A']);
print_r($object);//Object ( [name] => I AM A )


小试牛刀

实现用递归注入依赖信息

class simpleDI
{
//依赖
public $_dependencies = [];
//反射
public $_reflection = [];

//获取一个实例
public function get($class)
{
//如果是一个类就实例化
if(class_exists($class)) {
return $this->build($class);
}
throw new Exception("$class is not a class");
}

//实例化一个类
public function build($class)
{
//那么就要先满足依赖 才能实例化成功
list($dependencies,$reflector) = $this->getDependencies($class);
$dependencies = $this->resolveDependencies($dependencies);
if(empty($dependencies)) {
//没有任何依赖 直接创建
return $reflector->newInstanceArgs();
}else{
//有依赖 需要传入参数 才能创建
return $reflector->newInstanceArgs($dependencies);
}
}
//获取依赖
public function getDependencies($class)
{
$dependencies = [];
$reflector = new ReflectionClass($class);
$constructor = $reflector->getConstructor();
if ($constructor !== null) {
$params = $constructor->getParameters();
foreach ($params as $param) {
//构造函数的参数有普通的默认值
if ($param->isDefaultValueAvailable()) {
$dependencies[] = $param->getDefaultValue();
} else {
//构造函数的参数没有默认值 或者 是某一个类的实例
$className = $param->getClass();
$dependencies[] = $className == null? null:self::wrapObject($className);
}
}
}
return [$dependencies,$reflector];
}

//实例化依赖的类
public function resolveDependencies($dependencies)
{
foreach ($dependencies as $index => $dependency) {
if ($dependency instanceof stdClass) {
$dependencies[$index] = $this->get($dependency->className->name);
}
}
return $dependencies;
}

//使用对象包裹 与普通的参数区别开来
public static function wrapObject($className)
{
$object = new stdClass();
$object->className = $className;
return $object;
}
}


class C{}
class B
{
public $object;
public function __construct(C $c)
{
$this->object = $c;
}
}
class A
{
public  $name;
public  $object;
public function __construct(B $b,$name='A')
{
$this->name = $name;
$this->object = $b;
}
}

$D = new simpleDI();
$A = $D->get('A');
//打印
print_r($A);


//打印结果如下
A Object
(
[name] => A
[object] => B Object
(
[object] => C Object
(
)

)

)


显然我们的目的达到了,我们只是调用了
$D->get('A')
就获取到了A的实例,在这个过程,无需手动的为对象注入依赖信息。但离真正容器的样子还是相去甚远,因为上述代码并没有考虑其他类型的依赖对象,也没有考虑命名空间的问题,也没有考虑参数覆盖的问题。

yii2 DI容器的实现

//用于保存对象的引用
class Instance
{
public $id;
public function __construct($id)
{
$this->id = $id;
}
public static function of($id)
{
return new static($id);
}
public static function get($id)
{
return $this->$id;
}
}


//DI容器
class Container
{
/**
* 保存单例对象 键名为类名
*/
private $_singletons = [];
/**
* 保存依赖的定义 键名为类名
*/
private $_definitions = [];
/**
* 保存构造函数的参数 键名为类名
*/
private $_params = [];
/**
* 保存反射 键名为 类名|接口名
*/
private $_reflections = [];
/**
* 缓存依赖信息 键名为类名或为接口的名字
* 键值为这个类的的构造器的参数类型或默认值
*/
private $_dependencies = [];

/**
* 返回请求的实例
*
* 可以通过$params参数提供构造器的参数,通过$config提供对象的配置信息,这会在创建实例的时候会被用到
*
* 如果传递进来的类是实现了[[\yii\base\Configurable]]接口,那么$config这个参数会作为最后一个参数传递到构造函数中
* 如果不是 这个$config 将会在对象实例化只会才生效
*
* 需要注意的是 如果这个class是通过[[setSingleton()]]的方式取注册的,那么此后每一次调用get($class)方法,都会返回同一个实例
* 这意味着 类的实例只在最开始的时候被实例化,而params,config参数每次都生效,
*
* @param string $class 通过[[set()]] 或者 [[setSingleton()]] 方式注册的类名或者是别名
* @param array $params 构造函数的一系列参数 如果想要省去某些参数 那么该参数必须要又默认值
* @param array $config 用于初始化对象属性的一系列的键值对
* @return object 返回所请求的实例
* @throws InvalidConfigException if the class cannot be recognized or correspond to an invalid definition
* @throws NotInstantiableException If resolved to an abstract class or an interface (since 2.0.9)
*/
public function get($class, $params = [], $config = [])
{
//如果单例中有缓存 直接返回
if (isset($this->_singletons[$class])) {
// singleton
return $this->_singletons[$class];
//如果没有注册 则取build获取对象
} elseif (!isset($this->_definitions[$class])) {
return $this->build($class, $params, $config);
}
//取出定义
$definition = $this->_definitions[$class];

//如果是php callable类型 则执行call_user_func生成对象
if (is_callable($definition, true)) {
$params = $this->resolveDependencies($this->mergeParams($class, $params));
$object = call_user_func($definition, $this, $params, $config);
//如果是数组
} elseif (is_array($definition)) {
//则获取真正的类 因为$class可能是别名
$concrete = $definition['class'];
unset($definition['class']);
//合并config参数
$config = array_merge($definition, $config);
//合并params参数
$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)) {
// singleton
$this->_singletons[$class] = $object;
}

return $object;
}

/**
* 向容器注册一个普通类定义
*
* 举例子
*
* ```php
* // 注册一个普通类名(可以跳过).
* $container->set('yii\db\Connection');
*
* // 当一个类依赖一个接口时,用一下的方式注册一个接口
* // 那么该类会作为依赖对象而被实例化
* $container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');
*
* // 注册一个别名  可以使用$container->get('foo')的形式获取到foo对象的类
* // 创建数据库连接的实例
* $container->set('foo', 'yii\db\Connection');
*
* // 使用configuration配置的方式注册一个类
* // 配置在调用get获取实例时生效
* $container->set('yii\db\Connection', [
*     'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
*     'username' => 'root',
*     'password' => '',
*     'charset' => 'utf8',
* ]);
*
* // 既使用别名 又使用configuration的方式注册一个类
* // 这种情况 'class'必需是具体的类
* $container->set('db', [
*     'class' => 'yii\db\Connection',
*     'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
*     'username' => 'root',
*     'password' => '',
*     'charset' => 'utf8',
* ]);
*
* // 注册PHP callable
* // 这个call在$container->get('db')调用时将会被执行
* $container->set('db', function ($container, $params, $config) {
*     return new \yii\db\Connection($config);
* });
* ```

*
* @param string $class 类名, 接口名 , 别名
* @param mixed $definition 与$class相关. 是以下的类型之一
* - a PHP callable: 当调用[[get]]之后将会自动执行.
*  但这个callable是有规定的,它需要是这种形式`function ($container, $params, $config)`,它的`$params`参数是构造器的参数,它的
*  `$config`参数是对象的属性配置,这个callable对象返回的值有[[get()]]返回
* - a configuration array: 包含一系列的键值对,用于初始化新创建对象的属性值. 键名为`class`的元素代表真正要实例化的类. 如果没有指定,那就默认指定'class'=>$class
* - a string: 类名, 接口名 , 数组名
* @param array $params 一系列的构造器参数
* @return $this 返回容器本身
*/
public function set($class, $definition = [], array $params = [])
{
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
$this->_params[$class] = $params;
unset($this->_singletons[$class]);
return $this;
}

/**
* 向容器中注册一个单例,并标记这个class为单例
* 与set方法是一致的
*
* @param string $class 类名, 接口名 , 别名
* @param mixed $definition 与set方法一致
* @param array $params 与set方法一致
* @return $this 容器本身
* @see set()
*/
public function setSingleton($class, $definition = [], array $params = [])
{
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
$this->_params[$class] = $params;
$this->_singletons[$class] = null;
return $this;
}

/**
* 判断系统是否注册了$class
* @param string $class class name, interface name or alias name
* @return bool whether the container has the definition of the specified name..
* @see set()
*/
public function has($class)
{
return isset($this->_definitions[$class]);
}

/**
* 返回一个值,该值指示给定的名称是否已注册单例
* @param string $class class name, interface name or alias name
* @param bool $checkInstance whether to check if the singleton has been instantiated.
* @return bool 指示给定的名称是否已注册单例 ,当checkInstance为true时判断是否实例化该单例
*/
public function hasSingleton($class, $checkInstance = false)
{
return $checkInstance ? isset($this->_singletons[$class]) : array_key_exists($class, $this->_singletons);
}

/**
* 清除指定class的所有定义
* @param string $class class name, interface name or alias name
*/
public function clear($class)
{
unset($this->_definitions[$class], $this->_singletons[$class]);
}

/**
* 规范化定义 因为有多种的get调用方式 需要判断处理
* @param string $class class name
* @param string|array|callable $definition the class definition
* @return array the normalized class definition
* @throws InvalidConfigException if the definition is invalid.
*/
protected function normalizeDefinition($class, $definition)
{
//如果定义为空 则返回['class' => $class]
if (empty($definition)) {
return ['class' => $class];
//如果定义为字符串 返回['class' => $class]
} elseif (is_string($definition)) {
return ['class' => $definition];
//如果时callable类型或对象类型 直接返回 因为他们直接可用
} elseif (is_callable($definition, true) || is_object($definition)) {
return $definition;
//如果时数组
} elseif (is_array($definition)) {
//看看有没有定义class
if (!isset($definition['class'])) {
//如果没有 那就使用$class,但也要看看该$class是否有命名空间
if (strpos($class, '\\') !== false) {
$definition['class'] = $class;
} else {
//class没有命名空间 当前的空间肯定没有class啊 所以就找不到要实例化的类是什么了 抛出异常
throw new InvalidConfigException("A class definition requires a \"class\" member.");
}
}
return $definition;
} else {
//都不是的话 说明调用者没有按规定来注册类
throw new InvalidConfigException("Unsupported definition type for \"$class\": " . gettype($definition));
}
}

/**
* 返回一系列的对象定义或者是公用的实例对象
* @return array (type or ID => definition or instance).
*/
public function getDefinitions()
{
return $this->_definitions;
}

/**
* 创建类的具体实例
* 这个方法会解析这个类的依赖,并且去实例化依赖的对象,同时注入到这个类的新实例对象中
* @param string $class class的名字
* @param array $params 构造器参数
* @param array $config 实例化后生效的配置数组
* @return object 类的实例对象
* @throws NotInstantiableException If resolved to an abstract class or an interface (since 2.0.9)
*/
protected function build($class, $params, $config)
{
/* @var $reflection ReflectionClass */
//解析这个类的依赖之前 你得先获取它的依赖信息 但并没有实例化 只是获取了装有依赖信息的数组
list ($reflection, $dependencies) = $this->getDependencies($class);
// 覆盖构造器的参数 如果有的话
foreach ($params as $index => $param) {
$dependencies[$index] = $param;
}
//这里解析类的依赖信息 把装有依赖信息的数组交给专门处理依赖的方法进行处理
$dependencies = $this->resolveDependencies($dependencies, $reflection);
//如果反射对象不可以执行newInstanceArgs 那只能抛出无法实例化的异常了 因为实例化的机制就是newInstanceArgs
if (!$reflection->isInstantiable()) {
throw new NotInstantiableException($reflection->name);
}
//如果为空 那就可以执行newInstanceArgs返回对象了 因为params的配置已经覆盖到$dependencies中
if (empty($config)) {
return $reflection->newInstanceArgs($dependencies);
}
//如果config不为空

//如果依赖信息不是空的 且通过反射对象$reflection实现了Configurable接口(执行implementInterface判断)
if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {
//那么指定$dependencies的最后一个存放实例化后的配置信息 (原来存在的将会被覆盖)
//至于为什么要这样 据说是yii中的一个规定
$dependencies[count($dependencies) - 1] = $config;
//实例化返回
return $reflection->newInstanceArgs($dependencies);
} else {
//如果不是实现了yii\base\Configurable接口 那么先实例化数组 然后通过循环$config的方式 达到初始化对象属性的目的
$object = $reflection->newInstanceArgs($dependencies);
foreach ($config as $name => $value) {
$object->$name = $value;
}
return $object;
}
}

/**
* 合并用户指定的构造器参数和通过[[set()]]指定的构造器参数
* @param string $class class name, interface name or alias name
* @param array $params the constructor parameters
* @return array the merged parameters
*/
protected function mergeParams($class, $params)
{
if (empty($this->_params[$class])) {
return $params;
} elseif (empty($params)) {
return $this->_params[$class];
} else {
$ps = $this->_params[$class];
foreach ($params as $index => $value) {
$ps[$index] = $value;
}
return $ps;
}
}

/**
* 获取类的依赖信息
* 这里利用了PHP5的反射机制
* @param string $class class name, interface name or alias name
* @return array the dependencies of the specified class.
*/
protected function getDependencies($class)
{
//如果已经注册过的直接返回
if (isset($this->_reflections[$class])) {
return [$this->_reflections[$class], $this->_dependencies[$class]];
}

$dependencies = [];
//实例化$class的反射对象
$reflection = new ReflectionClass($class);
//获取$class的构造信息
$constructor = $reflection->getConstructor();
if ($constructor !== null) {
//获取构造器的参数
foreach ($constructor->getParameters() as $param) {
//判断参数是否由默认值
if ($param->isDefaultValueAvailable()) {
$dependencies[] = $param->getDefaultValue();
} else {
//如果没有默认值 那么有可能是一个类 类型的参数
//如果是 对象类型,执行getClass返回该类的信息
$c = $param->getClass();
//如果是 那就用Instance对象包裹以下 方便后面解析时判断
$dependencies[] = Instance::of($c === null ? null : $c->getName());
}
}
}
//缓存反射
$this->_reflections[$class] = $reflection;
//缓存依赖的信息 包含构造函数的默认值 或者依赖的对象(没有实例化 只是装到Instance中)
$this->_dependencies[$class] = $dependencies;

return [$reflection, $dependencies];
}

/**
* 解析依赖信息 把依赖信息中的对象类型(尚未实例化) 替换成 实例
* @param array $dependencies the dependencies
* @param ReflectionClass $reflection the class reflection associated with the dependencies
* @return array the resolved dependencies
* @throws InvalidConfigException if a dependency cannot be resolved or if a dependency cannot be fulfilled.
*/
protected function resolveDependencies($dependencies, $reflection = null)
{
foreach ($dependencies as $index => $dependency) {
//循环判断 如果它时Instance对象 说明就是一个对象 因为前面getDependencies时特意用Instance包裹而标识为对象的
if ($dependency instanceof Instance) {
if ($dependency->id !== null) {
//如果是对象 那么继续向容器索取该依赖的实例吧 显然这里会进入递归
$dependencies[$index] = $this->get($dependency->id);
} elseif ($reflection !== null) {
//如果丢失了class,说明依赖的对象没法实例化了,那就没法玩了,抛出异常
$name = $reflection->getConstructor()->getParameters()[$index]->getName();
$class = $reflection->getName();
throw new InvalidConfigException("Missing required parameter \"$name\" when instantiating \"$class\".");
}
}
}
return $dependencies;
}

}


更多的关于DI容器的文章

深入理解DIP、IoC、DI以及IoC容器

深入理解Yii2

程序员如何理解DI/Ioc

phalconphp-DI

理解DI容器

理解依赖与注入

最后,编程学习之路还有很长,要想得道,需要自身的努力,一定的天分,适时的得道指引,路漫漫其修远兮,吾将上下而求索啊!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  yii DI容器 依赖注入