Container make解析机制

思考并回答以下问题:

  • make解析的时候会调用build函数实例化对象。
  • make($abstract, array $parameters = []);

简介

make和resolve都是从容器中解析实例(这个实例是指concrete)出来。简单说就是从容器中把前面bind进去的东西拿出来用

这里需要明确的是,make解析的时候会调用build函数实例化对象,就是说理论上如果绑定的是一个字符串,Laravel默认这是一个可以实例化对象的类路径。

而我们如果想要绑定一个纯粹的字符串或者数字,我们可以使用闭包函数。让闭包返回我们需要的类型。具体看下面的源码。

把resolve和make放在一起是因为其实在Container类中,make就是resolve的一个包装。

我们看看make方法:很简单直接调用了resolve方法,类似的还有makeWith方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 从容器解析给定类型
*
* @param string $abstract
* @param array $parameters
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
1
2
3
4
5
6
7
8
9
10
11
/**
* An alias function name for make().
*
* @param string $abstract
* @param array $parameters
* @return mixed
*/
public function makeWith($abstract, array $parameters = [])
{
return $this->make($abstract, $parameters);
}

源码

先整体看下resolve函数源码:

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
/**
* 从容器解析给定类型
*
* @param string $abstract
* @param array $parameters
* @param bool $raiseEvents
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
$abstract = $this->getAlias($abstract);

$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);

// 如果当前正在以单例方式管理类型的实例,我们将仅返回一个现有实例,
// 而不是实例化新实例,以便开发人员每次都可以继续使用相同的对象实例。
if (isset($this->instances[$abstract]) && ! $needsContextualBuild)
{
return $this->instances[$abstract];
}

$this->with[] = $parameters;

$concrete = $this->getConcrete($abstract);

// 我们已经准备好实例化为绑定注册的具体类型的实例。
// 这将实例化类型,并递归地解决其所有“嵌套”依赖项,直到所有问题都解决为止。
if ($this->isBuildable($concrete, $abstract))
{
$object = $this->build($concrete);
}
else
{
$object = $this->make($concrete);
}

// 如果我们为此类型定义了任何扩展程序,则需要遍历它们并将其应用于正在构建的对象。
// 这允许扩展服务,例如更改配置或装饰对象。
foreach ($this->getExtenders($abstract) as $extender)
{
$object = $extender($object, $this);
}

// 如果将请求的类型注册为单例,我们将要缓存“内存”中的实例,
// 这样我们以后就可以返回它,而无需在每次后续请求上创建对象的全新实例。
if ($this->isShared($abstract) && ! $needsContextualBuild)
{
$this->instances[$abstract] = $object;
}

if ($raiseEvents)
{
$this->fireResolvingCallbacks($abstract, $object);
}

// 返回之前,我们还将解析标记设置为“ true”,并弹出此构建的参数替代。
// 完成这两件事之后,我们将准备好返回完整构造的类实例。
$this->resolved[$abstract] = true;

array_pop($this->with);

return $object;
}

还是从参数说起:

  • 参数$abstract,获取在容器中的服务的名字,或者叫id。
  • 参数$parameters,有些实例对象实例化的时候会需要参数,这个$parameters就是我们传入的参数。

举例:看代码,上一章我们知道,bind只是绑定一个闭包,啥也不干,所以不用传入参数,因为压根没有实例化对象。但是当我们这里要make解析的时候,即实例化Boss::class的时候,我们要把这个Object类型的对象传进去,Boss::class才能实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

class Boss()
{
private $obj;

// 这里构造函数需要一个对象才能实例化。
public function __construct(Object $obj)
{
$this->obj = $obj;
}
}

app()->bind('Boss', Boss::class);

app()->make('Boss', [new Object()]);

1.获取$abstract的别名。请参看。

1
$abstract = $this->getAlias($abstract);

2.设置一个变量$needsContextualBuild来做标记,标记当前这个解析的实例需不需要上下文绑定。

1
2
3
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);

在上下文绑定那章我们也说了,上下文绑定其实就是依赖绑定,就是判断当前的make的实例需不需要依赖。满足下面两个条件中的任意一个就需要:

  • a.传入的参数不为空。很好理解,你都传入参数了,这个参数上面刚刚讲了就是为了当前实例化的时候传入作为依赖的。
  • b.通过函数getContextualConcrete,获取到了当前解析的这个类,是否已经有了上下文绑定的依赖。(就是事先已经使用上下文绑定过了),这个其实虚的没有任何作用,往下细看。

2.1.那让我们看看getContextualConcrete($abstract)方法如何获取事先绑定的上下文依赖的。

getContextualConcrete代码如下:

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
/**
* 获取给定抽象类的上下文concrete绑定。
*
* @param string $abstract
* @return \Closure|string|null
*/
protected function getContextualConcrete($abstract)
{
if (! is_null($binding = $this->findInContextualBindings($abstract)))
{
return $binding;
}

// 接下来,我们需要查看是否可以在给定抽象类型的别名下绑定上下文绑定。 因此,我们将需要检查此类型是否存在别名,然后对其进行旋转并检查这些别名的上下文绑定。
if (empty($this->abstractAliases[$abstract]))
{
return;
}

foreach ($this->abstractAliases[$abstract] as $alias)
{
if (! is_null($binding = $this->findInContextualBindings($alias)))
{
return $binding;
}
}
}

2.1.1.首先判断是否在上下文绑定的数组中存在abstract的实例concrete,如果有就返回。直接从数组中找。

2.1.2.如果没有,看看这个$abstractAliases数组里面有没有$abstract别名,这个数组前面“别名”章节我们提过,和$aliases数据保存相反格式,保存abstract和alias关系的数组。注意,后面的数组value值才是别名,键值‘app’是abstract,格式如下:

1
2
3
4
5
6
7
8
9
$abstractAliases = [
app = {array} [3]
0 = "Illuminate\Foundation\Application"
1 = "Illuminate\Contracts\Container\Container"
2 = "Illuminate\Contracts\Foundation\Application"
blade.compiler = {array} [1]
0 = "Illuminate\View\Compilers\BladeCompiler"
...
]

继续看源代码。如果这个数组是空的,直接返回了。

2.1.3.如果这个数组不是空的,遍历所有abstract的别名,这个别名在binding数组中是否存在。

简单说就是abstract如果不在上下文绑定的数组中,那么看看abstract的别名是否在上下文绑定数组中。最后判断一下返回。

2.1.3.1重点来了,我们去看看findInContextualBindings源码:

1
2
3
4
5
6
7
8
9
10
/**
* Find the concrete binding for the given abstract in the contextual binding array.
*
* @param string $abstract
* @return \Closure|string|null
*/
protected function findInContextualBindings($abstract)
{
return $this->contextual[end($this->buildStack)][$abstract] ?? null;
}

还记得上下文绑定那章的存储结构就是这样:contextual[when][give]=implement。这里就是取对应的值。

但是我们发现他在取[give]值的时候它使用了end($this->buildStack)。buildStack是build的实例的堆栈,我们上下文绑定的流程中完全没有这个绑定。也就是说我们从resolve进来你是找不到这个值的,这完全是虚的没有任何作用,getContextualConcrete不会取得任何值。他的存在其实是给build函数创建依赖对象的时候,会递归再次回来make解析依赖类用的。看下一章build方法解析。

总结第二点,其实我们这里主要判断是就是有没有parameters,getContextualConcrete似乎完全不会取得任何值。

3.回到主线resolve函数,如果在数组instances中已经存在这个abstract的对象了并且不需要上下文绑定,直接调用这个instances中的值返回。我们前面章节知道instances数组是保存可以shared的实体对象。既然有了,并且没有依赖,就直接返回。

这里有个问题,如果有依赖,instances中的值为什么不能直接返回,因为依赖可能会变化,仔细想想是不是。你前面使用instance传入的有依赖的对象的参数,和这次我们要求的对象传入的依赖参数,可能是不同的。比如以前存储的new A(‘1’),这次需要的newA(‘2’),一个对象参数不同。

1
2
3
4
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) 
{
return $this->instances[$abstract];
}

这里有一个问题,通过instance()方法是可以保存任何类型数据的。但是如果instances数组中没有事先存在的值,那么make解析的字符串默认被当做一个类路径的。(后面章节有instace绑定源码分析)举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用instance存入字符串绑定。成功
$this->app->instance('money', "11");

$re = $this->app->make('money'); // success

// 通过闭包绑定字符串类型的值 成功
$this->app->bind('money', function(){
return "11";
});

$re = $this->app->make('money'); // success

// 直接绑定字符串,同时instances数组中不存在任何值,11被当做一个类路径处理。失败
$this->app->bind('money', '11');
$re = $this->app->make('money'); // fail

4.1前面的条件没成立的话,接下来,把参数parameters存入with数组,前面讲过了,parameters是实例化的时候需要的依赖,所以暂存于with数组。

1
$this->with[] = $parameters;

4.2.接下来通过函数getConcrete($abstract)获取concrete。

1
$concrete = $this->getConcrete($abstract);

我们看getConcrete源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Get the concrete type for a given abstract.
*
* @param string $abstract
* @return mixed $concrete
*/
protected function getConcrete($abstract)
{
if (! is_null($concrete = $this->getContextualConcrete($abstract)))
{
return $concrete;
}

// If we don't have a registered resolver or concrete for the type, we'll just
// assume each type is a concrete name and will attempt to resolve it as is
// since the container should be able to resolve concretes automatically.
if (isset($this->bindings[$abstract]))
{
return $this->bindings[$abstract]['concrete'];
}

return $abstract;
}

主要的思路是:

a. 看看上下文绑定数组中有没有$abstract对应的concrete值,如果有,太好了,最复杂的情况就是上下文绑定。直接返回就好了。连依赖都已经添加了。(参看上下文绑定存储结构和使用方法)

这里要特别注意,上下文绑定获取的concrete值可以是一个类路径,也可以是一个闭包(看看文档如何使用上下文绑定就知道了,可以传入类路径也可以是闭包。)。但是在后面的处理对两个情况是不一样的。

和上面情况雷同,其实这里getConcrete还是调用了getContextualConcrete,但buildstack中没有值,所以这个是虚的。暂时是没有值的。build解析依赖类的时候递归回来才有这个buildstack值。

b.如果没找到上下文绑定,就是一个普通绑定,就去bindings的数组中看看有没有$abstract对应的concrete值,从而确认是不是以前有绑定过。同样的$concrete可以是一个闭包,也可以是一个类路径。

c.都没有,说明没有绑定!!直接返回$abstract。

这里说明什么呢,我猜想我们是可以不用绑定bind函数,而直接make的,这样的话可以直接把$abstract当做$concrete来解析。

1
2
// 实测有效,直接返回Money::class 对象。
$boss= app()->make(Money::class);

这个方法处理的结果也有三种可能:

  • a.上下文绑定的concrete(这个其实没有)
  • b.binding 数组中的concrete
  • c.把$concrete===$abstract相等。

这里的c步骤到底做了什么,怎么处理的?我们往下看代码。第五步。

5.获取解析的对象了。

1
2
3
4
5
6
7
8
9
// 我们已经准备好实例化为绑定注册的具体类型的实例。 
// 这将实例化类型,并递归地解决其所有“嵌套”依赖项,直到所有问题都解决为止。
if ($this->isBuildable($concrete, $abstract))
{
$object = $this->build($concrete);
} else
{
$object = $this->make($concrete);
}

5.1 首先,我们要看下函数isBuildable函数是什么要求。如果$concrete===$abstract或者 concrete是一个闭包,好办返回true。

1
2
3
4
5
6
7
8
9
10
11
/**
* Determine if the given concrete is buildable.
*
* @param mixed $concrete
* @param string $abstract
* @return bool
*/
protected function isBuildable($concrete, $abstract)
{
return $concrete === $abstract || $concrete instanceof Closure;
}

5.2 如果是true,那么使用build函数处理这个object。

我们在这里简单说下build具体的会在下一章build源码中分析。

build的作用是这样的:

  • a.如果concrete是闭包,build执行闭包函数。
  • b.不是闭包,build函数会使用反射产生当前$concrete类的对象。和前面我们的猜想一样。既然$abstract===$concrete,那么直接解析,都不用绑定。

5.3 如果isBuildable返回的是false呢?就是$concrete的值是・类路径・的情况,调用make进入递归。如下give给的不是一个闭包是一个类路径。则进入make。

1
2
3
4
$container
->when(VideoController::class)
->needs(Filesystem::class)
->give(S3Filesystem::class);

make再去getConcrete函数,去上下文绑定数组和binding数组,查询这个时候这个・类路径下・(就是 abstract)有没有对应的闭包或类路径。但不管怎么样。最后下来要么闭包,要么相等,他都会进入build函数创建对象。

6.到此,我们得到了解析出来的object对象。

然后第六步我们要看看是否有扩展绑定entend的处理,参看0.2章节,执行:

1
2
3
4
5
// 如果我们为此类型定义了任何扩展程序,则需要遍历它们并将其应用于正在构建的对象。 
// 这允许扩展服务,例如更改配置或装饰对象。
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}

7.是否是单例分享的,如果是的话就存入instance,参看0.4章节

1
2
3
4
5
// 如果将请求的类型注册为单例,我们将要缓存“内存”中的实例,
// 这样我们以后就可以返回它,而无需在每次后续请求上创建对象的全新实例
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}

8.接着触发各个回调函数,参看0.3章节,执行回调,这个函数就是触发3个地方的回调函数。

1
$this->fireResolvingCallbacks($abstract, $object);

9.标记已经解析了。并且把参数从with中pop掉,没用了。这个with在build方法中使用了,在make方法中没有用到。

1
2
3
4
5
// 返回之前,我们还将解析标记设置为“true”,并弹出此构建的参数替代。 
// 完成这两件事之后,我们将准备好返回完整构造的类实例。
$this->resolved[$abstract] = true;

array_pop($this->with);

最后返回对象。

总结

make(解析)相对复杂。但是主要关注几个大步骤就能明白流程。

  • 1.首先获取最终的别名。
  • 2.设置是否是・上下文绑定・的标记。
  • 3.如果在shared的instances数组中找到了,同时又不是有上下文绑定需求的。直接返回对象。结束程序。
  • 4.否则,把实例化对象所依赖的参数parameters暂存with数组。
  • 5.通过getConcrete方法获取$concrete.注意这里的concrete还不是对象,是类路径或者是一个闭包函数。
  • 6.有了$concrete,如果是闭包,我们利用build函数生成对象。
  • 7.如果是类路径,我们要再递归,看看这个路径下是否还有$concrete的绑定。如果有再递归,像别名一样,找到真正那个。如果没有,使用build函数反射原理生成对象返回,with数组将在build反射中使用。
  • 8.完成对象生成,看看有没有extend扩展。
  • 9.看看是否需要shard,把对象存入instance中。
  • 10.触发各个回调函数。
  • 11.记录这个abstract已经解析过了。
  • 12.把with数组中parameters清空掉。
  • 13.返回对象。
0%