简介

Laravel 的核心是一个 IOC 容器,文档上称为服务容器;服务容器提供整个框架的一系列服务,如RouteDB等服务,都是存储在容器中的;接下来将结合Container.php源码举例讲解 IOC 容器。

IOC

IOC 是(Inversion of controller)的缩写,叫做控制反转,如何理解控制反转?看下面的这段解析:

控制反转 是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection, DI)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。

也就是说,这个调控系统中存放一些对象的实体,或者对象的描述,在对象创建的时候将对象所依赖的对象的引用传递过去。在 Laravel 中Service Container是一个高效的调控系统,实现了依赖对象的自动注入。接下来看看 Laravel 是如何实现自动依赖注入的。

依赖注入

Laravel 中实现的依赖注入都是通过Service Container服务容器来统一管理的,实现依赖注入的方式可以是在构造函数中或者setter方法中;但是服务容器只存储了对象的描述,那究竟如何把所有的依赖函数注入进来呢?查看Container.php的源码,在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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public function build($concrete, array $parameters = [])
{
// If the concrete type is actually a Closure, we will just execute it and
// hand back the results of the functions, which allows functions to be
// used as resolvers for more fine-tuned resolution of these objects.
if ($concrete instanceof Closure) {
return $concrete($this, $parameters);
}

$reflector = new ReflectionClass($concrete);

// If the type is not instantiable, the developer is attempting to resolve
// an abstract type such as an Interface of Abstract Class and there is
// no binding registered for the abstractions so we need to bail out.
if (! $reflector->isInstantiable()) {
if (! empty($this->buildStack)) {
$previous = implode(', ', $this->buildStack);

$message = "Target [$concrete] is not instantiable while building [$previous].";
} else {
$message = "Target [$concrete] is not instantiable.";
}

throw new BindingResolutionException($message);
}

$this->buildStack[] = $concrete;

$constructor = $reflector->getConstructor();

// If there are no constructors, that means there are no dependencies then
// we can just resolve the instances of the objects right away, without
// resolving any other types or dependencies out of these containers.
if (is_null($constructor)) {
array_pop($this->buildStack);

return new $concrete;
}

$dependencies = $constructor->getParameters();

// Once we have all the constructor's parameters we can create each of the
// dependency instances and then use the reflection instances to make a
// new instance of this class, injecting the created dependencies in.
$parameters = $this->keyParametersByArgument(
$dependencies, $parameters
);

$instances = $this->getDependencies(
$dependencies, $parameters
);

array_pop($this->buildStack);

return $reflector->newInstanceArgs($instances);
}

在第10行代码中会看到,用到了 PHP 的ReflectionClass反射类,反射解析如下:

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

更多反射的理解,可以查看官方文档。通过反射机制去解析一个类,能够获取一个类里面的属性、方法、构造函数、以及构造函数需要的参数。

查看这个方法,可以知道,通过反射获取到构造函数所依赖的类,使用到了getDependencies方法来获取所有依赖,最后通过newInstanceArgs实例化所有的依赖类。

到这里,我们知道了服务容器是通过反射机制来自动注入所有的依赖关系,这也是 Laravel 服务容器强大之处。接下来将结合Container.php源码举例说明。

IOC 容器举例

下面的服务容器代码是简写版的,方便调试使用,基本实现了 Laravel 容器的核心思想,再结合例子讲解。容器源码和案例在 github 上 [laravel-container]。

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
<?php
/**
* User: MinHow
* Date: 2017/1/23
*/
//容器类,简写版的Laravel的容器类,方便测试
class Container implements ArrayAccess
{
//用于存储提供实例的回调函数
protected $bindings = [];

//绑定接口和生成相应实例的回调函数
public function bind($abstract, $concrete = null, $shared = false)
{
//如果提供的参数不是回调函数,则产生默认的回调函数
if (! $concrete instanceof Closure) {
$concrete = $this->getClosure($abstract, $concrete);
}

//将返回的 $concrete 赋值到 $bindings[$abstract] 上
$this->bindings[$abstract] = compact('concrete', 'shared');
}

//默认生成实例的回调函数
protected function getClosure($abstract, $concrete)
{
return function ($c) use ($abstract, $concrete) {
$method = ($abstract == $concrete) ? 'build' : 'make';

return $c->$method($concrete);
};
}
//从容器中解析函数
public function make($abstract)
{
$concrete = $this->getConcrete($abstract);

//判断是否能实例化
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}

return $object;
}
//是否能实例化
protected function isBuildable($concrete, $abstract)
{
return $concrete === $abstract || $concrete instanceof Closure;
}

//获取绑定的回调函数
protected function getConcrete($abstract)
{
if (! isset($this->bindings[$abstract])) {
return $abstract;
}

return $this->bindings[$abstract]['concrete'];
}

//实例化对象
public function build($concrete)
{
//当 $concrete 为闭包,直接返回执行的方法
if ($concrete instanceof Closure) {
return $concrete($this);
}
//先构建反射类
$reflector = new ReflectionClass($concrete);
//当该类是抽象类或者借口时,抛出异常,这里为了方便测试,去掉异常抛出
if (! $reflector->isInstantiable()) {
echo $message = "Target [$concrete] is not instantiable";
}
//获取类的构造器
$constructor = $reflector->getConstructor();
//如果没有构造器,直接实例化这个实体类
if (is_null($constructor)) {
return new $concrete;
}
//获取构造器参数构成的数组
$dependencies = $constructor->getParameters();
//获取依赖的实例对象
$instances = $this->getDependencies($dependencies);
//返回实例化传入类实体的实体对象 (即 $concrete)
return $reflector->newInstanceArgs($instances);
}

//解决通过反射机制实例化对象时的依赖
protected function getDependencies(array $parameters)
{
$dependencies = [];
foreach ($parameters as $parameter) {
$dependency = $parameter->getClass();
if (is_null($dependency)) {
$dependencies[] = NULL;
} else {
$dependencies[] = $this->resolveClass($parameter);
}
}

return (array) $dependencies;
}

//解析依赖类
protected function resolveClass(ReflectionParameter $parameter)
{
return $this->make($parameter->getClass()->name);
}

//判断 bindings[$key] 是否存在,例:isset($container['data'])
public function offsetExists($key)
{
return isset($this->bindings[$key]);
}

//取得实例化对象,调用 make() 方法,例:$container['data']
public function offsetGet($key)
{
return $this->make($key);
}

//设置对象,例:$container['data'] = $value
public function offsetSet($key, $value)
{
// 如果 $value 不是闭包函数,就先将 $value 重新赋值为一个闭包
if (! $value instanceof Closure) {
$value = function () use ($value) {
return $value;
};
}
// 如果 $value 是一个闭包函数,就直接绑定
$this->bind($key, $value);
}

//释放 bindings[$key],例:unset($container['data'])
public function offsetUnset($key)
{
unset($this->bindings[$key]);
}
}

上面是服务容器的源码,下面是调用容器的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
/**
* User: MinHow
* Date: 2017/12/2
*
* 运行 php test.php
*/
//注册自动加载
spl_autoload_register('autoload');

function autoload($class)
{
require './' . $class . '.php';
}
//引入Container容器类
require '../Container.php';

$app = new Container();//实例化容器
$app->bind('Car', 'Benz');//Car 是接口,Benz 是 Benz 类
$app->bind('driver', 'Driver');//driver 是 Driver 类的别名

$driver = $app->make('driver');//通过解析,得到了 Driver 类的实例

$driver->driveCar();//因为之前已经把 Car 接口绑定了 Benz,所以调用 driveCar 方法,会显示 'Driving Benz!'

案例是一个工厂模式的例子,分别定义了BenzBmw两个类,都实现了汽车类的接口;最后调用Driver驾驶员类的driveCar方法。
在18行实例化容器后,向$app容器中注入需要测试的类:

1
2
$app->bind('Car', 'Benz');//Car 是接口,Benz 是 Benz 类
$app->bind('driver', 'Driver');//driver 是 Driver 类的别名

当执行完上述两行代码后,输出$app容器如下:

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
46
47
48
49
object(Container)#1 (1) {
["bindings":protected]=>
array(2) {
["Car"]=>
array(2) {
["concrete"]=>
object(Closure)#2 (3) {
["static"]=>
array(2) {
["abstract"]=>
string(3) "Car"
["concrete"]=>
string(4) "Benz"
}
["this"]=>
*RECURSION*
["parameter"]=>
array(1) {
["$c"]=>
string(10) "<required>"
}
}
["shared"]=>
bool(false)
}
["driver"]=>
array(2) {
["concrete"]=>
object(Closure)#3 (3) {
["static"]=>
array(2) {
["abstract"]=>
string(6) "driver"
["concrete"]=>
string(6) "Driver"
}
["this"]=>
*RECURSION*
["parameter"]=>
array(1) {
["$c"]=>
string(10) "<required>"
}
}
["shared"]=>
bool(false)
}
}
}

可以看到,容器里的bindings已经绑定了Cardriver两个数组,当执行$driver = $app->make('driver');这段代码的
时候,就通过容器类的make方法,通过闭包函数回调解析依赖的类,大概流程如下:

  1. 解析driver的时候,根据容器类的driver,获取到Driver的字符串。
  2. 递归将Driver传入build方法,这里的Driver就是class Driver,然后通过反射方法new ReflectionClass('Driver'),实例化了Driver类。
  3. 通过getConstructorgetParameters方法,知道class Driver和接口Car存在依赖关系。
  4. 再通过getDependencies方法循环实例化构造函数所有的依赖,就得到了Benz的实例。
  5. $instances的存放的就是Benz的实例,最后通过$reflector->newInstanceArgs($instances)方法得到了Driver的实例。

到这里,已经解决了$app->make('driver')方法所需要的所有依赖,并返回实例化的Driver的类,最后调用$driver->driveCar()方法,输出Driving Benz!

在案例中,如果想切换驾驶的车辆,只需要把绑定的’Benz’改成’Bmw’,即可输出Driving BMW!,其他的程序不需要任何改动;这对于程序的扩展是非常有帮助。

总结

Laravel 的IOC 容器是个高级的 IOC 容器,框架的各种功能模块都是由它负责的。依赖注入是控制反转的一种实现,实现代码解耦,也便于单元测试,因为它并不需要了解自身所依赖的类,而只需要知道所依赖的类实现了自身所需要的方法就可以了。

最后更新: 2018年01月06日 17:05

原始链接: http://blog.minhow.com/articles/laravel/ioc-container/

× 请我吃糖~
打赏二维码