laravel5.5框架解析系列文章屬于對laravel5.5框架源碼分析,如有需要,建議按順序閱讀該系列文章, 不定期更新,歡迎關(guān)注
已經(jīng)有很多文章寫laravel的ioc了, 這篇文章淺談一下其實現(xiàn)原理
laravel容器的作用:
- 共享對象(準(zhǔn)確來說是變量都可以共享, 對象共享用的多)
- 依賴注入, 自動實例化.
PSR(php標(biāo)準(zhǔn)規(guī)范)定義的容器接口:
interface ContainerInterface
{
public function get($id);
public function has($id);
}
該接口只有2個方法: get
方法通過id取出值, has
方法判斷id是否存在于容器, $id
為 string
類型
這是laravel拓展的容器接口:
interface Container extends ContainerInterface
{
// 抽象類型(接口,抽象類)是否被綁定過
public function bound($abstract);
// 給抽象類型取一個別名, 用于依賴關(guān)系處理
public function alias($abstract, $alias);
// 給一組抽象類型打上一組標(biāo)簽.這一組抽象類里的每一個抽象類都會打上tags里的所有標(biāo)簽. 打上標(biāo)簽后,可以通過tag取出抽象類
public function tag($abstracts, $tags);
// 取出被打上tag標(biāo)簽的所有抽象類型
public function tagged($tag);
/**
* 綁定一個接口到實現(xiàn)(實現(xiàn)類的類名, 或用閉包返回一個實例)
*
* @param string|array $abstract 接口名稱
* @param \Closure|string|null $concrete 實現(xiàn)類
* @param bool $shared 是否共享, 為true時每次會返回相同對象, 就是單例模式了
* @return void
*/
public function bind($abstract, $concrete = null, $shared = false);
// 沒有被綁定過才綁定
public function bindIf($abstract, $concrete = null, $shared = false);
// 綁定單例, 即bind方法的share參數(shù)為ture
public function singleton($abstract, $concrete = null);
//綁定一個回調(diào)到抽象類, 當(dāng)類被實例化時會調(diào)用這個回調(diào)并傳入實例, 你可以在回調(diào)中動態(tài)的添加屬性到實例, 即實現(xiàn)拓展了一個抽象類型
public function extend($abstract, Closure $closure);
// 也是綁定接口到實現(xiàn),只是該實現(xiàn)是一個已經(jīng)存在的實例
public function instance($abstract, $instance);
/**
* 設(shè)定條件, 因為不同的地方,你可能需要同一個接口的不同實現(xiàn)類, 通過when設(shè)定條件, 像這樣使用
* $contianer->when(A::class)->needs(InterfaceB::class)->give(C::class);
* $contianer->when(D::class)->needs(InterfaceB::class)->give(E::class);
*
* @param string $concrete 條件類名
* @return \Illuminate\Contracts\Container\ContextualBindingBuilder
*/
public function when($concrete);
// 返回一個閉包, 調(diào)用這個閉包就能從容器得到$abstract的實現(xiàn)實例
public function factory($abstract);
// 從容器獲取實現(xiàn)類實例, 可以傳參給實現(xiàn)類構(gòu)造函數(shù), 因為有些參數(shù)容器無法處理, 比如某實現(xiàn)類構(gòu)造方法需要一個int 參數(shù), 你必須告訴容器int參數(shù)值具體是多少
public function make($abstract, array $parameters = []);
// 調(diào)用指定方法(像這樣 ClassA::class . '@methodA'),或閉包 . 容器會make出類實例, 然后解析方法需要的依賴,注入調(diào)用方法,最后返回結(jié)果
public function call($callback, array $parameters = [], $defaultMethod = null);
// 判斷抽象類是否已經(jīng)resolved, 從一個抽象類型創(chuàng)建一個實現(xiàn)類實例的過程叫resolve
public function resolved($abstract);
// resolve 之前的回調(diào), 回調(diào)接收2個參數(shù): 這里的$abstract 和具體的實例
public function resolving($abstract, Closure $callback = null);
// resolve之后的回調(diào)
public function afterResolving($abstract, Closure $callback = null);
}
可以看到, laravel為容器增加了自動實例化的功能.以及調(diào)用類方法注入?yún)?shù)的功能
實現(xiàn)原理
其實要實現(xiàn)psr 的容器很簡單, 你甚至可以稍微封裝一下數(shù)組就能夠?qū)崿F(xiàn). psr 容器實際上就是個key=>value的map數(shù)組.這里重點講一下laravel如何實現(xiàn)依賴注入
使用案例
看如下代碼
// 可充電設(shè)備
interface ChargedAble
{
public function getCharged(int $power);
}
// iPhone...
class IPhone implements ChargedAble
{
public function getCharged(int $power)
{
echo "the iPhone is being charged with $power volt power\n";
}
}
// 充電寶
class PowerBank
{
protected $volt = 5;
protected $device;
public function __construct(ChargedAble $device)
{
$this->device = $device;
}
public function charge()
{
$this->device->getCharged($this->volt);
}
}
$container = new Container();
$container->bind(ChargedAble::class, IPhone::class);
$powerBank = $container->make(PowerBank::class);
$powerBank->charge();
// $this is a "phpunit test case"
$this->expectOutputString("the iPhone is being charged with 5 volt power\n");
這是一個充電寶給手機充電的例子, 當(dāng)然邏輯有點問題, 手機應(yīng)該是在charge的時候傳給充電寶, 而不是充電寶一出生就注定了一生所愛, but it's not the point. 能說明問題就行了
make()干了啥
上面案例中首先創(chuàng)建了一個容器,在容器中綁定了ChargeAble
和IPhone
, 然后就通過容器make了一臺充電寶, 能夠正常使用. 充電寶所得到的IPhone是怎么來的呢? Illuminate\Container
源碼, make方法就是直接調(diào)用了這個resolve
protected function resolve($abstract, $parameters = [])
{
// 如果這個抽象類是一個別名(通過上面的alias方法定義的別名, 所謂定義別名就是用k->v數(shù)組存起來),
// 那么這里會被映射到被取別名的抽象類
$abstract = $this->getAlias($abstract);
// 這里判斷是不是之前設(shè)定過條件綁定, 這個結(jié)果用來決定是返回緩存實例還是新間一個實例,
// 所以還`||`上了是否有參數(shù), 有參數(shù)傳進來當(dāng)然不能返回之前的緩存了, 得用參數(shù)構(gòu)造新的
$needsContextualBuild = ! empty($parameters) || ! is_null(
// 這里用到了一個biuld棧, 后面會有提及
$this->getContextualConcrete($abstract)
);
// 有緩存就直接返回緩存
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
// with數(shù)組是一個棧, 參數(shù)入棧是為了解決多層依賴的問題, 如a依賴b,b依賴c, 可能會遞歸調(diào)用make方法
$this->with[] = $parameters;
// 獲取實現(xiàn)類名稱. 其實綁定的時候把關(guān)系存數(shù)組了, 直接從數(shù)組里取,
// 如果沒綁定, 那認(rèn)為你想創(chuàng)建的就是$abstract, 直接返回
$concrete = $this->getConcrete($abstract);
// isBuildable 判斷$concrete是不是可實例化
if ($this->isBuildable($concrete, $abstract)) {
創(chuàng)建實例, 具體在下邊, 一會再看.
$object = $this->build($concrete);
} else {
// 不可實例化, 說明這個$concrete還是個抽象類型, 遞歸調(diào)用make
$object = $this->make($concrete);
}
// 調(diào)用上面說過的拓展回調(diào)
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// 把結(jié)果加到緩存里邊
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
// 調(diào)用resolving回調(diào)(是不是發(fā)現(xiàn)有個resolved 回調(diào)沒被調(diào)用, 其實在fireResolvingCallbacks方法里調(diào)用完resolving 就調(diào)用了resolved)
$this->fireResolvingCallbacks($abstract, $object);
// 標(biāo)記一下這個類被resolve過了, 上面容器接口里的resolved方法就是靠這個標(biāo)記來返回某個類是否被resolve過
$this->resolved[$abstract] = true;
// 參數(shù)出棧
array_pop($this->with);
// 返回結(jié)果
return $object;
}
// 創(chuàng)建一個類實例
public function build($concrete)
{
// 如果是個閉包, 那好辦,直接通過調(diào)用閉包就完成了創(chuàng)建
if ($concrete instanceof Closure) {
// getLastParameterOverride, 這個方法會從上面resolve方法提到的參數(shù)棧里獲取所需參數(shù)
return $concrete($this, $this->getLastParameterOverride());
}
// 反射, 不會的同學(xué)參考手冊哦: http://php.net/manual/en/class.reflectionclass.php
$reflector = new ReflectionClass($concrete);
// 判斷是否能夠?qū)嵗? if (! $reflector->isInstantiable()) {
// 不能實例化的話, 這里其實是直接拋異常
return $this->notInstantiable($concrete);
}
// 實現(xiàn)類入 build 棧, 在處理條件綁定的時候會用到
$this->buildStack[] = $concrete;
// 構(gòu)造方法的反射
$constructor = $reflector->getConstructor();
// 沒有構(gòu)造方法, 就不需要處理依賴了, 直接 new 即可
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
// 構(gòu)造方法參數(shù)反射實例數(shù)組, 即需要被注入的依賴
$dependencies = $constructor->getParameters();
// 獲取依賴, 具體步驟再下邊
$instances = $this->resolveDependencies(
$dependencies
);
// 出 build 棧
array_pop($this->buildStack);
// 注入依賴, 創(chuàng)建實例, 并返回結(jié)果
return $reflector->newInstanceArgs($instances);
}
// 把依賴都resolve出來, $dependencies 是參數(shù)反射數(shù)組
protected function resolveDependencies(array $dependencies)
{
// 結(jié)果數(shù)組
$results = [];
// 遍歷依賴數(shù)組, resolve出各個依賴參數(shù)
foreach ($dependencies as $dependency) {
// 在這個方法:resolve($abstract, $parameters = [])里
// 不是可以指定創(chuàng)建實例所需參數(shù)嗎, 如果依賴已經(jīng)被傳過來了, 那就直接放到結(jié)果數(shù)組里
if ($this->hasParameterOverride($dependency)) {
// 這里獲取傳過來的依賴,就利用了上面的參數(shù)棧
$results[] = $this->getParameterOverride($dependency);
continue;
}
// 如果依賴是一個class, 那就resolve 這個 class,
// 也就是繼續(xù)調(diào)用make把這個class實例取出來,
// 只是多了一步, 如果make報錯了, 就看這個參數(shù)是不是有默認(rèn)值, 有的話就返回默認(rèn)值
// 如果依賴不是class, 而是基本類型呢?
// 那會嘗試是不是有過條件綁定, 即$container->when($classname)->need($dep)->give($sth);
// 如果有綁定過$need為這個依賴參數(shù)的變量名, 那就會返回$sth,
// 如果沒有的話,看該依賴參數(shù)是不是有默認(rèn)值, 如果還沒有, 那沒辦法,只能拋異常了
$results[] = is_null($dependency->getClass())
? $this->resolvePrimitive($dependency)
: $this->resolveClass($dependency);
}
// 返回結(jié)果數(shù)組
return $results;
}
就這樣 $container->make(PowerBank::class)
就得到了充電寶實例. 整個流程應(yīng)該算比較清晰的.
關(guān)于參數(shù)棧和build棧, 這2個棧是用來保存當(dāng)前正在resolve的參數(shù)和類, 因為resolve是遞歸的,先進后出. 棧也是遞歸當(dāng)中常用的數(shù)據(jù)結(jié)構(gòu)