1.8 - Laravel - 5.6 - build 解析機制

HarveyNorman發表於2020-03-18

build的作用就是生成物件,具體他是怎麼根據不同情況生成物件的,看原始碼。

原始碼:

//0 -> 引數
public function build($concrete)

{
    //1 
    if ($concrete instanceof Closure) {

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

    }

    //2
    $reflector = new ReflectionClass($concrete);

    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();

    $instances = $this->resolveDependencies(

        $dependencies

    );

    array_pop($this->buildStack);

    return $reflector->newInstanceArgs($instances);

}

0.還是從引數說起,傳入的例項concrete,可以是閉包函式,可以是一個類路徑。還是重新提一下,concrete並不是實體類物件,而是可以產生物件的類或者閉包。和實體物件集合instance有區別。


1.如果這個concrete是一個閉包函式,我們就直接執行這個閉包。

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

這裡的閉包的形式為:
$concrete($this, $this>getLastParameterOverride());

有兩個引數,但是不傳引數也是可以的。

1.1 第二個引數$this->getLastParameterOverride(),我們看下原始碼:make解析那章,解析的時候把parameters存入了with陣列,這個時候就是使用getLastParameterOverride獲取with陣列的最後一個值,也就是我們make時候剛剛放入的物件的parameters。

protected function getLastParameterOverride()
{
    return count($this->with) ? end($this->with) : [];
}

2.建立一個php的ReflectionClass物件,包含類的資訊(參考php文件),ReflectionClass針對類,ReflectionObject則是對物件。

所以這裡concrete是一個類路徑。正如前面提到的。

$reflector = new ReflectionClass($concrete);

2.1 如果這不是一個可以初始化的類,那麼notInstantiable方法會丟擲一個異常。

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

notInstantiable方法原始碼: 發現他無非就是組織一個message,然後丟擲了一個異常。

他返回的異常提示是 Targe … is not instantiable.


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);
}

3.這裡透過php反射機制,用concrete類產生物件。如何實現呢? 如下步驟

3.1 首先把concrete存入陣列buildStack中,等下用。為什麼一定要先存入一個陣列呢?下面會講。如果細看上一章,也許你知道。


$this->buildStack[] = $concrete;

3.2 然後利用前面得到的反射物件透過getConstructor方法得到他的建構函式,

如果建構函式是空的,就是預設沒有引數的建構函式,從buildStack取出concrete使用new建立這個物件返回。

$constructor = $reflector->getConstructor();

if (is_null($constructor)) {

    array_pop($this->buildStack);

    return new $concrete;

}

4.否則就是類裡有自定義建構函式,或者說可能建構函式存在依賴的情況。我們要先獲取依賴再去建立物件。

4.1透過建構函式我們獲取引數依賴。就是建構函式中的引數,如果沒有,返回一個空的陣列。


$dependencies = $constructor->getParameters();

在進行下面的分析前,先明確getParameters()會返回的陣列的形式,幫助後面的分析,下面為一個例子:

class dollar{

    public function __construct($import){}

        public function getAmount(){

        return 100;

    }
}

$reflect = new ReflectionClass('dollar');

$constructor = $reflect->getConstructor();

var_dump($constructor->getParameters());

最後他輸出的結構是:

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

4.2 有了上面的例子。使用resolveDependencies方法解析然後獲取這個依賴。(不想看具體實現可以跳過到4.3)

$instances = $this->resolveDependencies($dependencies);

看下整個原始碼,然後再一步一步看。

protected function resolveDependencies(array $dependencies)

{

    $results = [];

    foreach ($dependencies as $dependency) {

        if ($this->hasParameterOverride($dependency)) {

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

            continue;

        }

        $results[] = is_null($dependency->getClass()) ? 
        $this->resolvePrimitive($dependency) : 
        $this->resolveClass($dependency);
    }

    return $results;

}

4.2.1 首先遍歷這個依賴,然後使用方法$this->hasParameterOverride($dependency)判斷是否引數有覆蓋。什麼意思呢,看hasParameterOverride原始碼,如下:

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陣列中。


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

嚴謹的再看下getParameterOverride怎麼實現的:


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.5 resolvePrimitive方法原始碼分析)

II. 如果存在使用resolveClass方法,就是使用make函式解析這個依賴(具體看下面4.2.6 resolveClass方法原始碼)


$results[] = is_null($dependency->getClass())

? $this->resolvePrimitive($dependency)

: $this->resolveClass($dependency);

4.2.5 resolvePrimitive方法原始碼:


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方法,直接排除一個無法解析的異常。原始碼:


protected function unresolvablePrimitive(ReflectionParameter $parameter)

{

    $message = "Unresolvable dependency resolving [$parameter] in class {$parameter->getDeclaringClass()->getName()}";

    throw new BindingResolutionException($message);

}

4.2.6 resolveClass方法原始碼:


protected function resolveClass(ReflectionParameter $parameter)

{

    try {

        return $this->make($parameter->getClass()->name);

    }

    catch (BindingResolutionException $e) {

        if ($parameter->isOptional()) {

            return $parameter->getDefaultValue();

        }

        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


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 有了依賴了,使用反射透過依賴生成物件。

例項測試:

1. 直接使用build,沒有依賴的閉包函式


class ExampleTest extends TestCase{

    public function testClosure(){

    $boss= app()->build(function(){return new Money();});

    $output = $boss->getAmount();

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

    }

}

class Money{

    public function getAmount(){

        return 100;

    }

}

class Dollar extends Money{

    public function getAmount(){

        return 1;

    }

}

2. 使用make解析,make透過build建立物件,有依賴的閉包函式的測試


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]);

    }

}

class Money

{

    private $amount = 0;

    public function __construct($amount)
    {

        $this->amount = $amount;

    }

    public function getAmount(){

        return $this->amount;

    }

}

3. 使用make解析,make透過build物件,有依賴的類路徑測試。


public function testFunctionBuildWithDependenceOfClass()

{

    $obj = app()->make(Currency::class);

    $this->assertEquals($obj->getAmount(), 1);

}

Class Currency

{

    private $dollar;

    public function __construct(Dollar $dollar)

    {

        $this->dollar = $dollar;

    }

    public function getAmount()

    {

        return $this->dollar->getAmount();

    }

}

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;

    }

}

感想

我們讀通原始碼後,我們可以解決很多找不太到答案的問題,比如:


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

class Money

{

    private $amount = 0;

    public function __construct($amount)

    {

        $this->amount = $amount;

    }

    public function getAmount(){

        return $this->amount;

    }

}

嘗試解析這個這個類,發現總是提示找不到amount依賴,我們追蹤下去會發現,with陣列中的parameter是


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

由此我們知道我們傳入的parameter沒有key值,正確的做法是:


$obj = app()->make(Money::class, ['amount' => 1000]);
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章