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

HarveyNorman發表於2020-03-12

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 協議》,轉載必須註明作者和本文連結

相關文章