Container build解析机制

思考并回答以下问题:

  • 为什么把concrete存入数组buildStack中,等下用?

简介

build的作用就是根据不同情况生成对象。

实例测试

1.直接使用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
<?php

class Money
{
public function getAmount()
{
return 100;
}
}

class ExampleTest extends TestCase
{
public function testClosure()
{
// 参数是一个闭包
$boss = app()->build(function(){
return new Money();
});

$output = $boss->getAmount();

$this->assertEquals($output, 100);
}
}

2.使用make解析,make通过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
<?php

class Money
{
// 依赖
private $amount = 0;

public function __construct($amount)
{
$this->amount = $amount;
}

public function getAmount()
{
return $this->amount;
}
}

class ExampleTest extends TestCase
{
public function testClosure()
{
$this->app->bind('money', function($app, $parameters){
return new Money($parameters[0]);
});

$boss = app()->make('money', [$amount = 1000]);
}
}

3.使用make解析,make通过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
class Money
{
private $amount = 0;

public function __construct($amount)
{
$this->amount = $amount;
}

public function getAmount()
{
return $this->amount;
}

}

class Dollar extends Money
{
//private $amount = 0;

public function __construct()
{

}

public function getAmount()
{
return 1;
}
}

Class Currency
{
private $dollar;

// 依赖一个类
public function __construct(Dollar $dollar)
{
$this->dollar = $dollar;
}

public function getAmount()
{
return $this->dollar->getAmount();
}
}

public function testFunctionBuildWithDependenceOfClass()
{
$obj = app()->make(Currency::class);
$this->assertEquals($obj->getAmount(), 1);
}

我们读通源码后,我们可以解决很多找不到答案的问题,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Money
{
private $amount = 0;

public function __construct($amount)
{
$this->amount = $amount;
}

public function getAmount()
{
return $this->amount;
}
}

$obj = app()->make(Money::class, [$amount => 1000]);

尝试解析这个这个类,发现总是提示找不到amount依赖,我们追踪下去会发现,with数组中的parameter是:

1
2
3
array(1) {
[0]=>int(1000)
}

由此我们知道我们传入的parameter没有key值,正确的做法是:

1
$obj = app()->make(Money::class, ['amount' => 1000]);

源码

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
/**
* 实例化给定类型的具体实例
*
* @param \Closure|string $concrete
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
//0 -> 参数
public function build($concrete)
{
// 如果$concrete实际上是闭包,我们将执行这个闭包并返回执行的结果,
// 这允许将函数用作解析器以更精细地解析这些对象。
// 1
if ($concrete instanceof Closure)
{
return $concrete($this, $this->getLastParameterOverride());
}

try
{
// 2
$reflector = new ReflectionClass($concrete);
}
catch (ReflectionException $e)
{
throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
}

// 如果该类型不是可实例化的,则开发人员正在尝试解析一个抽象类型,
// 例如Interface或Abstract Class,并且没有为抽象注册任何绑定,因此我们需要补救。
if (! $reflector->isInstantiable())
{
return $this->notInstantiable($concrete);
}

// 3
$this->buildStack[] = $concrete;

$constructor = $reflector->getConstructor();

// 如果没有构造函数,则意味着没有依赖关系,那么我们可以立即解析对象的实例,
// 而无需从这些容器中解析任何其他类型或依赖关系。
if (is_null($constructor))
{
array_pop($this->buildStack);

return new $concrete;
}

// 4
$dependencies = $constructor->getParameters();

// 一旦有了所有构造函数的参数,就可以创建每个依赖项实例,
// 然后使用反射实例创建此类的新实例,将创建的依赖项注入其中。
try
{
$instances = $this->resolveDependencies($dependencies);
}
catch (BindingResolutionException $e)
{
array_pop($this->buildStack);

throw $e;
}

array_pop($this->buildStack);

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

0.还是从参数说起,传入的实例concrete,可以是闭包函数,可以是一个类路径。还是重新提一下,concrete并不是实体类对象,而是可以产生对象的类或者闭包。和实体对象集合instance有区别。

1.如果这个concrete是一个闭包函数,我们就直接执行这个闭包。

1
2
3
4
if ($concrete instanceof Closure) 
{
return $concrete($this, $this->getLastParameterOverride());
}

这里的闭包的形式为:

1
$concrete($this, $this->getLastParameterOverride());

有两个参数,但是不传参数也是可以的。

第二个参数$this->getLastParameterOverride(),我们看下源代码:

1
2
3
4
5
6
7
8
9
/**
* Get the last parameter override.
*
* @return array
*/
protected function getLastParameterOverride()
{
return count($this->with) ? end($this->with) : [];
}

make解析那章,解析的时候把parameters存入了with数组,这个时候就是使用getLastParameterOverride获取with数组的最后一个值,也就是我们make时候刚刚放入的对象的parameters。

2.创建一个php的ReflectionClass对象,包含类的信息,ReflectionClass针对类,ReflectionObject则是对对象。

所以这里concrete是一个类路径。正如前面提到的。

1
$reflector = new ReflectionClass($concrete);

2.1 如果这不是一个可以初始化的类,那么notInstantiable方法会抛出一个异常。

1
2
3
4
if (! $reflector->isInstantiable()) 
{
return $this->notInstantiable($concrete);
}

notInstantiable方法源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Throw an exception that the concrete is not instantiable.
*
* @param string $concrete
* @return void
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function notInstantiable($concrete)
{
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);
}

发现他无非就是组织一个message,然后抛出了一个异常。返回的异常提示是Targe … is not instantiable.

3.这里通过php反射机制,用Concrete类产生对象。如何实现呢?如下步骤。

3.1 首先把concrete存入数组buildStack中,等下用。为什么一定要先存入一个数组呢?下面会讲。如果细看上一章,也许你知道。

1
$this->buildStack[] = $concrete;

3.2 然后利用前面得到的反射对象通过getConstructor方法得到他的构造函数,如果构造函数是空的,就是默认没有参数的构造函数,从buildStack取出concrete使用new创建这个对象返回。

1
2
3
4
5
6
7
8
$constructor = $reflector->getConstructor();

if (is_null($constructor))
{
array_pop($this->buildStack);

return new $concrete;
}

4.否则就是类里有自定义构造函数,或者说可能构造函数存在依赖的情况。我们要先获取依赖再去创建对象。

4.1 通过构造函数我们获取参数依赖。就是构造函数中的参数,如果没有,返回一个空的数组。

1
$dependencies = $constructor->getParameters();

在进行下面的分析前,先明确getParameters()会返回的数组的形式,帮助后面的分析,下面为一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class dollar
{
public function __construct($import)
{

}

public function getAmount()
{
return 100;
}
}

$reflect = new ReflectionClass('dollar');

$constructor = $reflect->getConstructor();

var_dump($constructor->getParameters());

最后他输出的结构是:

1
2
3
4
5
array(1) {
[0] => object(ReflectionParameter)#3 (1) {
["name"]=>string(6) "import"
}
}

4.2 有了上面的例子。使用resolveDependencies方法解析然后获取这个依赖。(不想看具体实现可以跳过到4.3)

1
$instances = $this->resolveDependencies($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
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 从ReflectionParameters解析所有依赖项
*
* @param array $dependencies
* @return array
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function resolveDependencies(array $dependencies)
{
$results = [];

foreach ($dependencies as $dependency)
{
// 如果此依赖项覆盖了这个特殊的build,则将其用作值。
// 否则,我们将继续执行这些决议,并让反射尝试确定结果。
if ($this->hasParameterOverride($dependency))
{
$results[] = $this->getParameterOverride($dependency);

continue;
}

// 如果该类为null,则表示依赖项是字符串或其他一些原始类型,
// 由于它不是类,因此我们无法解析,并且由于无处可去,因此我们将抛出错误。
$result = is_null($dependency->getClass())
? $this->resolvePrimitive($dependency)
: $this->resolveClass($dependency);

if ($dependency->isVariadic())
{
$results = array_merge($results, $result);
}
else
{
$results[] = $result;
}
}

return $results;
}

4.2.1 首先遍历这个依赖,然后使用方法$this->hasParameterOverride($dependency)判断是否参数有覆盖。什么意思呢,看hasParameterOverride源码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Determine if the given dependency has a parameter override.
*
* @param \ReflectionParameter $dependency
* @return bool
*/
protected function hasParameterOverride($dependency)
{
return array_key_exists(
$dependency->name, $this->getLastParameterOverride()
);
}

$this->getLastParameterOverride()上面提到过,获取make时候存入with数组的参数。$dependency->name是什么,就是构造函数中参数的名字,上面的例子中,就是‘import’。简单说就是我们在make解析的时候是不是传递了这个依赖的值(就是make方法的第二个参数parameters)。

4.2.2 继续,如果有这个覆盖,使用getParameterOverride获取这个依赖的值,存入result数组中。

1
$results[] = $this->getParameterOverride($dependency);

严谨的再看下getParameterOverride怎么实现的:

1
2
3
4
5
6
7
8
9
10
/**
* Get a parameter override for a dependency.
*
* @param \ReflectionParameter $dependency
* @return mixed
*/
protected function getParameterOverride($dependency)
{
return $this->getLastParameterOverride()[$dependency->name];
}

很简单还是通过getLastParameterOverride获取with数组,然后在数组中通过name(这里就是import)获取。

4.2.3 然后continue循环下一个依赖。

4.2.4 如果在with数组中找不到对应的依赖。也就是这个依赖不是我们make时候设定的,故意要传入的parameters,可能是其他的类。那么我们先判断这个依赖类是不是存在于当前代码中。

I. 如果不存在,则使用resolvePrimitive方法,看看上下文绑定中有没有对应的值,再看看依赖自己有没有默认值(具体看下面4.2.5resolvePrimitive方法源码分析)。

II. 如果存在使用resolveClass方法,就是使用make函数解析这个依赖(具体看下面4.2.6resolveClass方法源码)。

1
2
3
4
5
// 如果该类为null,则表示依赖项是字符串或其他一些原始类型,
// 由于它不是类,因此我们无法解析,并且由于无处可去,因此我们将抛出错误。
$result = is_null($dependency->getClass())
? $this->resolvePrimitive($dependency)
: $this->resolveClass($dependency);

4.2.5 resolvePrimitive方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 解析非类提示的原始依赖项。
*
* @param \ReflectionParameter $parameter
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function resolvePrimitive(ReflectionParameter $parameter)
{
if (! is_null($concrete = $this->getContextualConcrete('$'.$parameter->name)))
{
return $concrete instanceof Closure ? $concrete($this) : $concrete;
}

if ($parameter->isDefaultValueAvailable())
{
return $parameter->getDefaultValue();
}

$this->unresolvablePrimitive($parameter);
}
  • a. 首先判断这个依赖的name是不是在上下文绑定中(又来了)。
  • b. 如果这个依赖在上下文绑定中,再次提醒,我们记得上下文绑定有两个类型,一个是闭包,一个是类路径。这里如果是闭包直接执行,如果是类路径,返回类路径。
  • c. 否则是否存在依赖有默认值,有默认值直接返回默认值。什么意思呢,这里的例子,如果是$import=”100”是这个情况。
  • d. 都没有那说明没办法解析,执行unresolvablePrimitive方法,直接排除一个无法解析的异常。源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 为无法解析的原始类型抛出异常。
*
* @param \ReflectionParameter $parameter
* @return void
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function unresolvablePrimitive(ReflectionParameter $parameter)
{
$message = "Unresolvable dependency resolving [$parameter] in class {$parameter->getDeclaringClass()->getName()}";

throw new BindingResolutionException($message);
}

4.2.6 resolveClass方法源码:

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
/**
* 解析容器中基于类的依赖关系
*
* @param \ReflectionParameter $parameter
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function resolveClass(ReflectionParameter $parameter)
{
try
{
return $parameter->isVariadic()
? $this->resolveVariadicClass($parameter)
: $this->make($parameter->getClass()->name);
}

// 如果无法解析类实例,则将检查该值是否为可选值,
// 如果为可选值,则将返回可选参数值作为依赖项的值,这与使用标量的方式类似。
catch (BindingResolutionException $e)
{
if ($parameter->isDefaultValueAvailable())
{
return $parameter->getDefaultValue();
}

if ($parameter->isVariadic())
{
return [];
}

throw $e;
}
}
  • a.很明显,既然在代码中存在这个类,使用make函数解析这个类。(又进入解析了,又会调用build。)这个时候才是buildstack有值的时候,就是说在resolve函数中,getContextualConcrete才起到了真正的作用,会检查上下文绑定。回看上一章。
  • b.有一个情况我们要考虑,如果这个类不能被解析,就是解析失败了,(比如:这个类是抽象类)。怎么办,我们上面提过,他会抛出一个BindingResolutionException异常吧,这里捕获这个异常,判断这个依赖$import是不是一个optional的参数,什么意思,还是这个情况 ($import == 100) 有没有默认值,如果有则取他的默认值(100)返回,不解析了,有默认值先用下再说。如果不是,没办法继续抛这个异常。

4.2.7 最后返回包含了所有依赖的result数组,我们前面有种情况,如果有自定义构造方法但是没有传入参数,就是result[]是空,不影响结果。往下看。

4.3 得到了依赖了,我们使用newInstanceArgs方法创建对象不再使用new了,依赖$instances为参数.

4.3.1 这里再次使用array_pop从buildStack取出类。我们一开始为什么要存到buildStack这个数组中,这个时候可以总结了:

逻辑上来说:就是为了给不一样的逻辑情况提供未污染的变量。简单说就是把这个变量放起来,谁先来谁先用。具体情况来说,为了给解析依赖的时候提供一个及时的当前build的concretion。

1
2
3
array_pop($this->buildStack);

return $reflector->newInstanceArgs($instances);

这就是整个流程。

总结

build方法主要是这样的流程:

  • 1.是不是闭包,闭包就执行。
  • 2.如果不是闭包,利用php反射,生成php反射对象。
  • 3.这个类有没有自定义构造函数,如果没有,就直接new返回。
  • 4.如果有自定义构造函数。
    • 4.1.我们先看看有没有在make的时候传入第二个参数,有的话获取返回这个依赖。
    • 4.2.没有传入的话,说明不是我们故意要的,有其他的依赖。
      • a.看看这个依赖能不能解析,不能解析,我们看看上下文绑定有没有,这个依赖的默认值有没有,都没有,没办法抛出异常。
      • b.要是能解析,就直接解析,返回。
    • 4.3.有了依赖了,使用反射通过依赖生成对象。
0%