laravel5.5框架解析[2]——容器與依賴注入

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是否存在于容器, $idstring類型

這是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)建了一個容器,在容器中綁定了ChargeAbleIPhone, 然后就通過容器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)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末赶袄,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子氮块,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,348評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件玖瘸,死亡現(xiàn)場離奇詭異驾窟,居然都是意外死亡,警方通過查閱死者的電腦和手機该溯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評論 2 385
  • 文/潘曉璐 我一進店門岛抄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人狈茉,你說我怎么就攤上這事夫椭。” “怎么了氯庆?”我有些...
    開封第一講書人閱讀 156,936評論 0 347
  • 文/不壞的土叔 我叫張陵蹭秋,是天一觀的道長。 經(jīng)常有香客問我堤撵,道長仁讨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,427評論 1 283
  • 正文 為了忘掉前任粒督,我火速辦了婚禮陪竿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘屠橄。我一直安慰自己族跛,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,467評論 6 385
  • 文/花漫 我一把揭開白布锐墙。 她就那樣靜靜地躺著礁哄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪溪北。 梳的紋絲不亂的頭發(fā)上桐绒,一...
    開封第一講書人閱讀 49,785評論 1 290
  • 那天,我揣著相機與錄音之拨,去河邊找鬼茉继。 笑死,一個胖子當(dāng)著我的面吹牛蚀乔,可吹牛的內(nèi)容都是我干的烁竭。 我是一名探鬼主播,決...
    沈念sama閱讀 38,931評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼吉挣,長吁一口氣:“原來是場噩夢啊……” “哼派撕!你這毒婦竟也來了婉弹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,696評論 0 266
  • 序言:老撾萬榮一對情侶失蹤终吼,失蹤者是張志新(化名)和其女友劉穎镀赌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體际跪,經(jīng)...
    沈念sama閱讀 44,141評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡商佛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,483評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了垫卤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片威彰。...
    茶點故事閱讀 38,625評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖穴肘,靈堂內(nèi)的尸體忽然破棺而出歇盼,到底是詐尸還是另有隱情,我是刑警寧澤评抚,帶...
    沈念sama閱讀 34,291評論 4 329
  • 正文 年R本政府宣布豹缀,位于F島的核電站,受9級特大地震影響慨代,放射性物質(zhì)發(fā)生泄漏邢笙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,892評論 3 312
  • 文/蒙蒙 一侍匙、第九天 我趴在偏房一處隱蔽的房頂上張望氮惯。 院中可真熱鬧,春花似錦想暗、人聲如沸妇汗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽杨箭。三九已至,卻和暖如春储狭,著一層夾襖步出監(jiān)牢的瞬間互婿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工辽狈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留慈参,地道東北人。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓刮萌,卻偏偏與公主長得像懂牧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子尊勿,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,492評論 2 348

推薦閱讀更多精彩內(nèi)容