昨天有提到 Application 是 Laravel Service Container 的實作,它繼承了 Container,是負責管理元件如何產生的元件。
$container = new Container();
$container->singleton(MyClass::class, function () { return new MyClass('dep'); });
|
如此一來,當使用 make()
時,它就會觸發 callback,依照 callback 的寫法來產生對應的物件
$instance1 = $container->make(MyClass::class);
$instance2 = $container->make(MyClass::class);
|
事實上,它最好用的地方,正是自動處理依賴注入的功能:
class Dep { }
class MyClass { public function __construct(Dep $dep) { } }
$container = new Container();
$container->make(MyClass::class);
|
為什麼會這麼神奇呢?讓我們一起看看原始碼吧!
類別圖
首先,依賴很單純,它只依賴 Contracts 並實作 ArrayAccess:
PlantUML 原始碼如下:
@startuml interface Psr\Container\ContainerInterface { + {abstract} get($id) + {abstract} has($id) }
interface Contracts\Container\Container { + {abstract} bound($abstract) + {abstract} alias($abstract, $alias) + {abstract} tag($abstracts, $tags) + {abstract} tagged($tag) + {abstract} bind($abstract, $concrete = null, $shared = false) + {abstract} bindIf($abstract, $concrete = null, $shared = false) + {abstract} singleton($abstract, $concrete = null) + {abstract} extend($abstract, Closure $closure) + {abstract} instance($abstract, $instance) + {abstract} when($concrete) + {abstract} factory($abstract) + {abstract} make($abstract, array $parameters = []) + {abstract} call($callback, array $parameters = [], $defaultMethod = null) + {abstract} resolved($abstract) + {abstract} resolving($abstract, Closure $callback = null) + {abstract} afterResolving($abstract, Closure $callback = null) }
class Illuminate\Container\BoundMethod { + {static} call() }
Psr\Container\ContainerInterface <|-- Contracts\Container\Container Contracts\Container\Container <|.. Illuminate\Container\Container ArrayAccess <|.. Illuminate\Container\Container Illuminate\Container\Container --> Illuminate\Container\BoundMethod : static call Illuminate\Container\Container *-- Illuminate\Container\ContextualBindingBuilder @enduml
|
從類別圖可以了解:
- 核心角色為
Illuminate\Container\Container
(下稱 Container
)
Illuminate\Container\BoundMethod
(下稱 BoundMethod
)為類似 helper 的輔助角色
Illuminate\Container\ContextualBindingBuilder
(下稱 ContextualBindingBuilder
)也是輔助角色,協助產生 container 的設定。
singleton() 做了什麼事
從一開始的範例,我們知道 singleton()
是設定 callback 表示該物件如何建置,而 make()
則是產生。首先看 singleton()
的實作是什麼:
public function singleton($abstract, $concrete = null) { $this->bind($abstract, $concrete, true); }
|
這裡可以了解,它是 bind()
的另一種呼叫方法,因為 PHP 並不像 Java 有重載,所以常會使用這一類的寫法增加可用性與可閱讀性。如:
public function getData(array $query) { }
public function getDataById($id) { $this->getData([ 'id' => $id, ]); }
|
而 bind()
的原始碼如下,雖然已經有清楚的註解了,不過還是簡單用中文描述:
public function bind($abstract, $concrete = null, $shared = false) { $this->dropStaleInstances($abstract);
if (is_null($concrete)) { $concrete = $abstract; }
if (! $concrete instanceof Closure) { $concrete = $this->getClosure($abstract, $concrete); }
$this->bindings[$abstract] = compact('concrete', 'shared');
if ($this->resolved($abstract)) { $this->rebound($abstract); } }
|
在追程式碼的過程中,會同時注意屬性有哪些,因為不同方法之間的關聯,是由屬性連繫起來的。
其中,我們需要先了解預設的 Closure 是什麼:
protected function getClosure($abstract, $concrete) { return function ($container, $parameters = []) use ($abstract, $concrete) { if ($abstract == $concrete) { return $container->build($concrete); } return $container->make($concrete, $parameters); }; }
|
這裡的 $container
,指的就是 $this
。剛剛 bind()
裡面有提到:
當沒有給 concrete 的話,則會把 abstract 當 concrete 來處理
這裡原始碼可以發現,如果上述情況的話,它會使用 build()
建置實例;不是的話,則會使用一開始提到的 make()
建置。
這兩個之間的差異,只要繼續看 make()
就會了解。
make() 做了什麼事
make()
跟 singleton()
類似,也是在呼叫另一個方法 resolve()
,不過這個做法會比較像是 proxy pattern。
public function make($abstract, array $parameters = []) { return $this->resolve($abstract, $parameters); }
|
resolve()
可就複雜了:
protected function resolve($abstract, $parameters = []) { $abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null( $this->getContextualConcrete($abstract) );
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) { return $this->instances[$abstract]; }
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
if ($this->isBuildable($concrete, $abstract)) { $object = $this->build($concrete); } else { $object = $this->make($concrete); }
foreach ($this->getExtenders($abstract) as $extender) { $object = $extender($object, $this); }
if ($this->isShared($abstract) && ! $needsContextualBuild) { $this->instances[$abstract] = $object; }
$this->fireResolvingCallbacks($abstract, $object);
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object; }
|
整個過程可大略分成下面幾個重點:
make()
or build()
- Registry singleton pattern
- Extenders(或 decorator pattern)
- Fire callback(或 observer pattern)
後面三個有設計模式可以參考,都很好理解它們的目的甚至實作;建置如果使用 make()
的話,就會發生遞迴呼叫(recursive call),因此要先了解 isBuildable()
實作,先知道什麼情況會發生遞迴呼叫,什麼情況會終止。
protected function isBuildable($concrete, $abstract) { return $concrete === $abstract || $concrete instanceof Closure; }
|
實作非常簡單:如果 abstract 與 concrete 相同,或是 concrete 是 Closure 的話,會使用 build()
;反之,兩個不同,而且 concrete 不是 Closure 時,則會使用 make()
。
回顧上面 bind()
曾提到的:
當 concrete 不是 Closure 的話,會預期它是 class 名稱,Laravel 會把它包裝成預設的 Closure
換句話說,只要曾使用 bind()
定義過的類別,就一定會使用 build()
,舉幾個官網的例子:
$this->app->bind('HelpSpot\API', function ($app) { return new HelpSpot\API($app->make('HttpClient')); });
|
這個情況因為 concrete 是 Closure,所以會使用 build()
。再看另一個例子:
$this->app->bind( 'App\Contracts\EventPusher', 'App\Services\RedisEventPusher' );
|
因為 concrete 不是 Closure,它會包裝成預設的 Closure 存起來,所以最後也會使用 build()
。
如果沒有綁定過的類別拿來 make()
呢?比方說一開始舉的例子:
$container->make(MyClass::class);
|
這時就得了解 concrete 的來源:getConcrete()
的實作了:
protected function getConcrete($abstract) { if (! is_null($concrete = $this->getContextualConcrete($abstract))) { return $concrete; }
if (isset($this->bindings[$abstract])) { return $this->bindings[$abstract]['concrete']; }
return $abstract; }
|
看起來關鍵就是 getContextualConcrete()
做了什麼事了:
protected function getContextualConcrete($abstract) { if (! is_null($binding = $this->findInContextualBindings($abstract))) { return $binding; }
if (empty($this->abstractAliases[$abstract])) { return; }
foreach ($this->abstractAliases[$abstract] as $alias) { if (! is_null($binding = $this->findInContextualBindings($alias))) { return $binding; } } }
|
Contextual Binding 是用在同一個類別,在不同地方會使用到不同的實例,這裡再講下去會太複雜,就先跳過。
從以上追過原始碼的結果會發現,除非是使用 Contextual Binding,它才會在 resolve()
的時候使用 make()
,其他都會使用 build()
。
今天先到此結束,明天再繼續看 build()
做了什麼。