PHP的容器 -- 依赖注入(DI) 和 控制反转(IoC)

本文内容大部分参考自:对PHP框架中的容器的理解

PHP的容器 – 依赖注入(DI) 和 控制反转(IoC)

DI - Dependency Injection 依赖注入
IoC - Inversion of Control 控制反转

依赖注入和控制反转是对同一件事情的不同描述,从某个方面讲,就是它们描述的角度不同。

  • 依赖注入是从应用程序的角度在描述,可以把依赖注入,即:应用程序依赖容器创建并注入它所需要的外部资源;
  • 而控制反转是从容器的角度在描述,即:容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源。

实现一个容器

//根据我们的上面的分析,容器至少需要俩个操作,分别是将类绑定到容器中以及将类从容器中取出的操作
class Container
{
    //容器类列表
    public static $generator_list = [];

    // 绑定类到容器中 
    public static function bind($class_name, $generator)
    {
        if (is_callable($generator)) {
            self::$generator_list[$class_name] = $generator;
        } else {
            throw new Exception('对象生成器不是可以调用的类型');
        }
    }

    // 生成类对象
    public static function make($class_name, $params = [])
    {
        if (! isset(self::$generator_list[$class_name])) {
            throw new Exception($class_name.'类没有被绑定注册');
        }
        return call_user_func_array(self::$generator_list[$class_name], $params);
    }
}

使用容器

//我们先看一下bind()函数,该函数对应上面说到的绑定操作,就是将一个类放到$generator_list中,仔细看一下,你会发现,该函数并不是把一个类或者一个对象直接传递进去,而是传入了两个参数,一个是参数的名字,一个是生成器。
//生成器说白了就是一个函数,这个函数是用来负责实例化需要绑定的类的。
//说到这里,有同学可能有点疑惑,为什么要这样,为什么不直接传一个对象进去那?
//原因是类的实例化的过程是需要传递参数的,传递一个生成器进去,我们在实例化这个类的时候就可以修改参数了。
//下面就是一个绑定示例,大家可以看一下。
    Container::bind("A",function($param){
       return A($param);
    })
    self::$generator_list["A"] = function($param){
       return A($param);
    };
//这样,我们的绑定操作基本就说完了,下面看make()函数。之前也已经提到过了,make()函数就是将所需要的对象从这个容器中取出来。该函数也需要传递两个参数进去,一个是class_name也就是需要取出的类的名称,一个是params,也就是实例化对象的时候需要传递的参数。
//下面的一行代码是整个函数的关键所在:
call_user_func_array(self::$generator_list[$class_name], $params);
self::$generator_list[$class_name]对应的是类的生成器,$params对应的是类实例化所需要的参数,
//call_user_func_array()该函数是PHP的内置函数,通过该函数我们可以执行self::$generator_list[$class_name]对应的是类的生成器函数,这样我们也就是完成了所需类的实例化。(ps:对call_user_func_array()函数不清楚的同学可以先去看一下手册)

测试一下

//将类A的生成器函数(匿名函数/闭包)绑定到容器中
Container::bind('A', function($name='') {
    return new A($title);
});
//在容器类中获取类A的对象
$Obj = Container::make('A', ['aaa']);
//打印出得到的这个对象
var_dump($Obj);
//打印结果如下:
object(A)#2 (1) {
  ["name"]=>
  string(4) "aaa"
}
//我们在打印出self::$generator_list中的数据看一下:
array(1) {
  ["A"]=>
  object(Closure)#1 (1) {
    ["parameter"]=>
    array(1) {
      ["$name"]=>
      string(10) ""
    }
  }
}

怎么样,看到上面打印出来的节后是不是就清楚一些了那…上面我们分析了一下容器类的具体的执行方式,上面的代码比较的简单,也灭有涉及到类相互依赖的问题,相比大家肯定想看一下类相互依赖的时候,容器类是怎么为我们解决依赖的,我们下面就写一个例子再分析一下,其实容器的代码我们基本不需要在动了。

//最开始的我们就举了一个B类依赖于A类的例子,现在我们继续使用这个例子来说明一下
//绑定A类到容器中
Container::bind('A', function($name='') {
    return new A($title);
});
//绑定B类到容器中
Container::bind('B', function($module,$params=[]) {
    return new B(Container::make($module,$params));
});
//上面B类的绑定方式大家可能觉得有点怪,这是因为B类依赖于A类,所以我们在B类的生成器对象中(匿名函数)中需要得到A类的实例传参给B,
//怎么获取A类的实例那,简单,因为A类也存在于容器中,所以我们直接调用make()函数就可以获取A类的实例对象了,
//但是在实例化A类的时候,构造函数可能需要参数,为了能够得到这些参数,我们就需要在B类的生成器对象中将这些参数传递进来。
//下面我们调用一下B类
$Obj= Container::make('B', ['A', ['aaa']]);
//上面我们就获取到了B类的实例化的对象了,是不是很简单,有兴趣的同学可以将上面的结果打印出来看一下。
//我们再分析一下上面的步骤,想要获取B类的实例化对象,直接通过make()进行获取,
//因为B依赖于A,所以需要传递A到生成器函数中,但是A有需要其他的参数,所以我们还需要继续传递其他参数进去,所以参数就是一个二维数组
//上面对参数有疑问的同学可以按照上面的流程分析一遍,就清楚了

Tips

想要实现容器的自动依赖注入,需要使用反射,通过反射,获取类构造函数所需的参数,分析出所依赖的类,然后在容器中获取其所依赖的类,其实就是一层一层的找需要什么,需要什么就在容器中找什么,找到了就作为参数传递过去,这样就实现了自动注入解决了依赖的问题。