分析 Container(2)

今天,我們要來分析 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 的分析,除了讚嘆它設計的奧妙之外,同時也理解它可以如何使用,更加能發揮它的價值。