iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 4
0

今天,我們要來分析 Containerbuild()

這裡有個有趣的小地方:build()make() 第一個參數都可以傳類別名稱,但 build() 稱之為 $concretemake() 則是 $abstract,這意味著,當建構的類別是實作(concrete)類別時,才能使用 build(),是抽象(abstract)類別則會使用 make()

public function build($concrete)
{
    // 如果是 Closure ,就直接執行吧。 getLastParameterOverride() 是取得昨天提到的屬性 `with` 所存放的 parameters 
    if ($concrete instanceof Closure) {
        return $concrete($this, $this->getLastParameterOverride());
    }

    $reflector = new ReflectionClass($concrete);

    // 如果是無法直接建構的類別,就會丟例外
    if (! $reflector->isInstantiable()) {
        return $this->notInstantiable($concrete);
    }

    // 建置的過程中,可能會有其他依賴也要一同建置,這裡將會存放建置的 stack
    $this->buildStack[] = $concrete;

    $constructor = $reflector->getConstructor();

    // 沒有 `constructor` 意味著沒有依賴,直接 new 下去就對了
    if (is_null($constructor)) {
        array_pop($this->buildStack);

        return new $concrete;
    }

    // 有 `consturctor` 代表它的參數都是依賴
    $dependencies = $constructor->getParameters();

    // 解析依賴並產生對應的實例
    $instances = $this->resolveDependencies(
        $dependencies
    );

    // 建置完成會 pop stack 
    array_pop($this->buildStack);

    // 使用依賴產生這次建置所要的實例
    return $reflector->newInstanceArgs($instances);
}

這裡的流程有幾個重點:

  • 使用 Closure 產生實例
  • 使用反射(reflection)取得建構資訊
  • 如果沒有建構子就直接產生實例
  • 如果有建構子則使用 resolveDependencies() 解析依賴,最後再使用解析出來的依賴來產生實例

前三點與最後產生實例的方法都很好了解,因此我們重點放在 resolveDependencies() 的實作:

protected function resolveDependencies(array $dependencies)
{
    $results = [];

    foreach ($dependencies as $dependency) {
        // 如果是 make() 有給 parameters 的話,將會使用該 parameters
        if ($this->hasParameterOverride($dependency)) {
            $results[] = $this->getParameterOverride($dependency);
            continue;
        }

        // 如果取不到類別名稱,代表它是 primitive 類型的變數, 會使用 resolvePrimitive() 解析;取得到,就使用 resolveClass() 解析
        $results[] = is_null($dependency->getClass())
                        ? $this->resolvePrimitive($dependency)
                        : $this->resolveClass($dependency);
    }

    return $results;
}

接著分別來看 resolvePrimitive()resolveClass() 的原始碼:

protected function resolvePrimitive(ReflectionParameter $parameter)
{
    // 如果有設定 contextual binding ,則回傳該內容 
    if (! is_null($concrete = $this->getContextualConcrete('$'.$parameter->name))) {
        return $concrete instanceof Closure ? $concrete($this) : $concrete;
    }

    // 有預設值則使用預設值
    if ($parameter->isDefaultValueAvailable()) {
        return $parameter->getDefaultValue();
    }

    // 都不是的話,則無法解析
    $this->unresolvablePrimitive($parameter);
}

protected function resolveClass(ReflectionParameter $parameter)
{
    try {
        // 直接使用 make() 來產生該實例。如果一開始是從 make() 進來 build() 的,這裡將會發生遞迴呼叫(recursive call)
        return $this->make($parameter->getClass()->name);
    }
    catch (BindingResolutionException $e) {
        // 如果發生例外的話,則使用預設值試看看,不行的話也只能丟例外了
        if ($parameter->isOptional()) {
            return $parameter->getDefaultValue();
        }

        throw $e;
    }
}

resolvePrimitive() 的流程很單純,基本上就是看有沒有預設值,除非有設定 contextual binding。resolveClass() 更為簡單,知道類別名稱後,直接再拿來 make() 即可。即使發生遞迴呼叫,它一定會有中止的時候,因為建構子的依賴鏈,是不可能無窮無盡的。

兩個類別的建構子,是可以寫出循環依賴的,但本來就無法使用,所以不能 make() 也是正常的。

到此,已經可以理解 make() 是如何把複雜依賴關係的類別建置出來。

Contextual Binding

現在再回頭來看 Contextual Binding,官網的例子是這樣的:

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('local');
          });

$this->app->when([VideoController::class, UploadController::class])
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('s3');
          });

這裡有使用 fluent pattern ,讓表達更加接近自然語言,如:「當 PhotoController 需要 Filesystem 時,就給 local storage」

when() 的實作很單純:

build()when() 的參數也是 $concrete ,所以這裡要傳的是實作的 class。

return new ContextualBindingBuilder($this, $this->getAlias($concrete));

只回傳 ContextualBindingBuilder 實例,建構子和 needs() 單純只是把傳入值保存下來,就不提了。given() 則會呼叫 Container 真的在處理 Contextual Binding 的方法-- addContextualBinding()

public function give($implementation)
{
    $this->container->addContextualBinding(
        $this->concrete, $this->needs, $implementation
    );
}

addContextualBinding() 實作很單純:

$this->contextual[$concrete][$this->getAlias($abstract)] = $implementation;

而昨天還有提到 findInContextualBindings(),它的實作也很單純:

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

findInContextualBindings() 的意思正是找尋看看,現在正在 build() 的類別,有沒有哪個依賴有被綁定過,有被綁定的話就回傳這個綁定內容。昨天提到的 resolve() 與今天提到的 resolvePrimitive() 都會使用這個方法來取得可能有被綁定過的實例。

BoundMethod

最後來看這個靜態的類別,如果有寫過 Laravel Controller 的話,相信看完下面的分析後,會突然理解一些原理。

Container call() 可以呼叫 Closure 同時,並解析它的傳入值並產生相關依賴,讓該 Closure 可以正常被執行。

裡面呼叫的 BoundMethod::call() 實作很簡單:

// 假如 $callback 是特殊模式的字串,或是 $defaultMethod 不是 null ,則呼叫 callClass()
if (static::isCallableWithAtSign($callback) || $defaultMethod) {
    return static::callClass($container, $callback, $parameters, $defaultMethod);
}

// 呼叫方法
return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
    return call_user_func_array(
        $callback, static::getMethodDependencies($container, $callback, $parameters)
    );
});

什麼是特殊模式的字串?看 callClass() 即可知道

// 字串裡要有 `@`
$segments = explode('@', $target);

// 如果切出來是兩半的話,$segments[1] 就是 method
$method = count($segments) == 2
                ? $segments[1] : $defaultMethod;

// 如果解析不出來,就例外吧
if (is_null($method)) {
    throw new InvalidArgumentException('Method not provided.');
}

// 有類別,有方法,就再從頭再來一次
return static::call(
    $container, [$container->make($segments[0]), $method], $parameters
);

這個規則就是在定義 Routing 的時候會寫的,如官網範例:

Route::get('/user', 'UserController@index');

類似 'UserController@index' 這個字串。

那符合這個字串會做什麼事呢?繼續往下看 callBoundMethod()

protected static function callBoundMethod($container, $callback, $default)
{
    // 如果 callback 不是 Array,比方說 Closure 的話,就回傳預設值,不過預設是固定給 Closure ,所以會拿來跑出結果後再回傳 
    if (! is_array($callback)) {
        return $default instanceof Closure ? $default() : $default;
    }

    // 這裡會把 Array 型式的 callable 轉換原 class@method 型式,這也正好就是 5.7 的新功能
    $method = static::normalizeMethod($callback);

    // 回頭看 Container 有沒有做 method binding ,有的話就 call。
    if ($container->hasMethodBinding($method)) {
        return $container->callMethodBinding($method, $callback[0]);
    }

    // 執行 Closure
    return $default instanceof Closure ? $default() : $default;
}

Closure 做的事如下:

function () use ($container, $callback, $parameters) {
    return call_user_func_array(
        $callback, static::getMethodDependencies($container, $callback, $parameters)
    );
}

它會拿 callback 來執行,並把 dependencies 全都解析完後帶入。解析的方法跟 make() 類似,但較為簡單,所以這邊就不再分析了。

今日總結

看完 Container 的分析,除了讚嘆它設計的奧妙之外,同時也理解它可以如何使用,更加能發揮它的價值。


上一篇
分析 Container(1)
下一篇
分析 Application
系列文
Laravel 原始碼分析46

尚未有邦友留言

立即登入留言