1.7 - Laravel - 5.6 - Container make 解析机制

resolve解析是容器中最复杂的部分,有很多小细节只有通读整个流程反复试验后才能领略作者的设计的目的。

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

这里需要明确的是,make解析的时候会调用build函数实例化对象,就是说理论上如果绑定的是一个字符串,laravel默认这是一个可以实例化对象的类路径。
那我们如果想要绑定一个纯粹的字符串或者数字,我们可以使用闭包函数。让闭包返回我们需要的类型。具体看下面的源码

把resolve和make放在一起是因为其实上在Container类中,make就是resolve的一个包装。
我们看看make方法:很简单直接调用了resolve方法,类似的还有makeWith方法,有兴趣的可以看看。

public function make($abstract, array $parameters = [])
{
    return $this->resolve($abstract, $parameters);
}

先整体看下resolve函数源码:

protected function resolve($abstract, $parameters = [])
    {
        $abstract = $this->getAlias($abstract);

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

        // If an instance of the type is currently being managed as a singleton we'll
        // just return an existing instance instead of instantiating new instances
        // so the developer can keep using the same objects instance every time.
        if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
            return $this->instances[$abstract];
        }

        $this->with[] = $parameters;

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

        // We're ready to instantiate an instance of the concrete type registered for
        // the binding. This will instantiate the types, as well as resolve any of
        // its "nested" dependencies recursively until all have gotten resolved.
        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }

        // If we defined any extenders for this type, we'll need to spin through them
        // and apply them to the object being built. This allows for the extension
        // of services, such as changing configuration or decorating the object.
        foreach ($this->getExtenders($abstract) as $extender) {
            $object = $extender($object, $this);
        }

        // If the requested type is registered as a singleton we'll want to cache off
        // the instances in "memory" so we can return it later without creating an
        // entirely new instance of an object on each subsequent request for it.
        if ($this->isShared($abstract) && ! $needsContextualBuild) {
            $this->instances[$abstract] = $object;
        }

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

        // Before returning, we will also set the resolved flag to "true" and pop off
        // the parameter overrides for this build. After those two things are done
        // we will be ready to return back the fully constructed class instance.
        $this->resolved[$abstract] = true;

        array_pop($this->with);

        return $object;
    }

还是从参数说起
0.1. 参数$abstruct,获取在容器中的服务的名字,或者叫id //不多说都知道
0.2. 参数$parameters, 有些实例对象实例化的时候会需要参数,这个$parametters就是我们传入的参数。
举例:看代码,上一章我们知道,bind只是绑定一个闭包,啥也不干,所以不用传入参数,因为压根没有实例化对象。但是当我们这里要make解析的时候,即实例化Boss.class的时候,我们要把这个Object类型的对象传进去。Boss.class才能实例化。

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

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

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

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

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

2.设置一个变量$needsContextualBuild来做标记,标记当前这个解析的实例需不需要上下文绑定。在上下文绑定那章我们也说了,上下文绑定其实就是依赖绑定,就是判断当前的make的实例需不需要依赖。满足下面两个条件中的任意一个就需要:
a. 传入的参数不为空。很好理解,你都传入参数了,这个参数上面刚刚讲了就是为了当前实例化的时候传入作为依赖的。
b. 通过函数getContextualConcrete,获取到了当前解析的这个类,是否已经有了上下文绑定的依赖。(就是事先已经使用上下文绑定过了),这个其实虚 的没有任何作用,往下细看

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

2.1.那让我们看看getContextualConcrete($abstract)方法如何获取事先绑定的上下文依赖的:
2.1.1.首先判断是否在上下文绑定的数组中存在abstruct的实例concrete,如果有就返回。//直接从数组中找。
2.1.2.如果没有,看看这个$abstractAliases数组里面有没有$abstruct别名,这个数组前面”别名”章节我们提过,和$aliases数据保存相反格式,保存abstruct和alias关系的数组。注意,后面的数组value值才是别名,键值‘app’是abstruct
格式如下:

$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.如果这个数组不是空的,遍历所有abstruct的别名,这个别名在binding数组中是否存在。

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

getContextualConcrete代码入下:

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.3.1 重点来了,我们去看看findInContextualBindings源码:

protected function findInContextualBindings($abstract)
{
    if (isset($this->contextual[end($this->buildStack)][$abstract])) {
        return $this->contextual[end($this->buildStack)][$abstract];
    }
}

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

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


3.回到主线resolve函数,如果在数组instances中已经存才这个abstruct的对象了并且不需要上下文绑定,直接调用这个instances中的值返回。我们前面章节知道instances数组是保存可以shared的实体对象。既然有了,并且没有依赖,就直接返回。
这里有个问题,如果有依赖,instances中的值为什么不能直接返回,因为依赖可能会变化,仔细想想是不是。你前面使用instance传入的有依赖的对象的参数,和这次我们要求的对象传入的依赖参数,可能是不同的。比如 以前存储的new A('1'),这次需要的new A('2'),一个对象参数不同。

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

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

//使用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数组。

$this->with[] = $parameters;

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

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

我们看getConcrete源代码:

protected function getConcrete($abstract)
{
    if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
        return $concrete;
    }

    if (isset($this->bindings[$abstract])) {
        return $this->bindings[$abstract]['concrete'];
    }

    return $abstract;
}

主要的思路是:
a.看看上下文绑定数组中有没有$abstruct对应的concrete值,如果有,太好了,最复杂的情况就是上下文绑定。直接返回就好了。连依赖都已经添加了。(参看山下文绑定存储结构和使用方法)
这里要特别注意,上下文绑定获取的concrete值可以是一个类路径,也可以是一个闭包(看看文档如何使用上下文绑定就知道了,可以传入类路径也可以是闭包。)。但是在后面的处理对两个情况是不一样的

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

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

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

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

这个方法处理的结果也有三种可能:
a.上下文绑定的concrete (这个其实没有)
b.binding数组中的concrete
c.把 $concrete === $abstruct 相等。
这里的c步骤到底做了什么,怎么处理的?我们往下看代码。第五步。


5.获取解析的对象了。

if ($this->isBuildable($concrete, $abstract)) {
    $object = $this->build($concrete);
} else {
    $object = $this->make($concrete);
}

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

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类的对象。和前面我们的猜想一样。既然$abstruct===$concrete,那么直接解析,都不用绑定。

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

$container
    ->when(VideoController::class)
    ->needs(Filesystem::class)
    ->give(S3Filesystem::class);

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


6.到此,我们得到了解析出来的object对象。
然后第六步我们要看看是否有扩展绑定entend的处理,参看0.2章节,执行

foreach ($this->getExtenders($abstract) as $extender) {
    $object = $extender($object, $this);
}

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

if ($this->isShared($abstract) && ! $needsContextualBuild) {
    $this->instances[$abstract] = $object;
}

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

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

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

$this->resolved[$abstract] = true;
array_pop($this->with);

最后返回对象。


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

  1. 首先获取最终的别名。
  2. 设置是否是·上下文绑定·的标记
  3. 如果在shared的instances数组中找到了,同时又不是有上下文绑定需求的。直接返回对象。结束程序。
  4. 否则,把实例化对象所依赖的参数parameters暂存with数组
  5. 1 通过getConcrete方法获取$concrete.注意这里的concrete还不是对象,是类路径或者是一个闭包函数
  6. 有了$concrete,如果是闭包,我们利用build函数生成对象。
  7. 1 如果是类路径,我们要再递归,看看这个路径下是否还有$concrete的绑定。如果有再递归,像别名一样,找到真正那个。如果没有,使用build函数反射原理生成对象返回,with数组将在build反射中使用。
  8. 完成对象生成,看看有没有extend扩展
  9. 看看是否需要shard,把对象存入instance中
  10. 触发各个回调函数
  11. 记录这个abstruct已经解析过了。
  12. 1 把with数组中parameters清空掉。
  13. 返回对象
本作品采用《CC 协议》,转载必须注明作者和本文链接
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!