這裡有個有趣的小地方:
build()
與make()
第一個參數都可以傳類別名稱,但build()
稱之為$concrete
;make()
則是$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);
}
這裡的流程有幾個重點:
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,官網的例子是這樣的:
$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()
都會使用這個方法來取得可能有被綁定過的實例。
最後來看這個靜態的類別,如果有寫過 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 的分析,除了讚嘆它設計的奧妙之外,同時也理解它可以如何使用,更加能發揮它的價值。