今天,我們要來分析 Container 的 build()
這裡有個有趣的小地方:build()
與 make()
第一個參數都可以傳類別名稱,但 build()
稱之為 $concrete
;make()
則是 $abstract
,這意味著,當建構的類別是實作(concrete)類別時,才能使用 build()
,是抽象(abstract)類別則會使用 make()
。
public function build($concrete) { if ($concrete instanceof Closure) { return $concrete($this, $this->getLastParameterOverride()); }
$reflector = new ReflectionClass($concrete);
if (! $reflector->isInstantiable()) { return $this->notInstantiable($concrete); }
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
if (is_null($constructor)) { array_pop($this->buildStack);
return new $concrete; }
$dependencies = $constructor->getParameters();
$instances = $this->resolveDependencies( $dependencies );
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances); }
|
這裡的流程有幾個重點:
- 使用 Closure 產生實例
- 使用反射(reflection)取得建構資訊
- 如果沒有建構子就直接產生實例
- 如果有建構子則使用
resolveDependencies()
解析依賴,最後再使用解析出來的依賴來產生實例
前三點與最後產生實例的方法都很好了解,因此我們重點放在 resolveDependencies()
的實作:
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; }
|
接著分別來看 resolvePrimitive()
與 resolveClass()
的原始碼:
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); }
protected function resolveClass(ReflectionParameter $parameter) { try { 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()
實作很簡單:
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);
$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) { if (! is_array($callback)) { return $default instanceof Closure ? $default() : $default; }
$method = static::normalizeMethod($callback);
if ($container->hasMethodBinding($method)) { return $container->callMethodBinding($method, $callback[0]); }
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 的分析,除了讚嘆它設計的奧妙之外,同時也理解它可以如何使用,更加能發揮它的價值。