如何理解 Laravel 和 ThinkPHP 5 中的服務容器與注入?

從文檔提及

不少人一開始看到官方的文檔,不管是 Laravel 仍是 ThinkPHP ,看完都是一頭霧水,不求甚解。甚至都是直接跳過去,不看,反正我也不同用獲得這麼高端的東西,若是在短期內有這個念頭很正常,尤爲是習慣了 ThinkPHP 3 的使用者,相對引入的理念比較前沿,若是你在長時間內都不去考慮去理解,那就要看你本身的職業規劃了。
接下來就來一塊兒看一下,細細追品。php

從 Laravel 開始

從 Laravel 的文檔中看到有 bindsingleton 以及 instance ,這三個經常使用方法,接下來就一一解答。laravel

實際應用

假設咱們有這樣一個場景,當咱們用戶在進行註冊時,咱們須要向用戶手機發送一條短信驗證碼,而後當用戶收到驗證碼後在註冊表單提交時還須要驗證驗證碼是否正確。git

這個需求看起來很是容易實現,對吧?github

當咱們拿到短信平臺的開發文檔後,咱們只須要寫出兩個方法。sendcheck 分別用來發送驗證碼和校驗驗證碼,下面就在不用容器的狀況下來寫一下僞代碼。web

  • MeiSms.php
<?php


namespace App\Tools;


class MeiSms
{
    public function send($phone)
    {
        $code = mt_rand(1e3, 1e4 - 1);
        // TODO ... 經過接口發送
        // 存放驗證碼到 Redis
        $cacheManager = cache();
        // 設定 5 分鐘失效
        $cacheManager->set('sms:' . $phone, $code, 5 * 60);
        return true;
    }

    public function check($phone, $code)
    {
        $cacheManager = cache();
        return $cacheManager->get('sms:' . $phone) === $code;
    }
}

很容易,不是嗎?thinkphp

而後在控制器中 new 一個 MeiSms 的實例,直接調用 sendcheck 就能夠分別發送和檢查驗證碼了。bootstrap

可是,若是運營忽然反饋說,以前給的短信平臺不可靠,發送短信不穩定,用戶常常收不到。數組

這時候咱們就須要換一個接口,常見的方式就是 咱們再寫一個對象, 而後 又可能這個代碼是別人寫的讓你來接收,你以爲 send 或者 check 這個方法名不夠規範,而後你就給改了,而後順帶把原來的註冊那邊一併改了,而後代碼就突突上線,跑起來了。瀏覽器

而後沒過多久,運營又以爲這個平臺的短信太貴了,另外又找到了一家既便宜又穩定的一家,而後你又重複了上面的事情,此次,方法這些你都以爲很完美,不用改動。app

只是須要寫一寫方法體,而後在調用的地方改一些new 時的類名。

固然,這只是一個小例子,開發過程當中咱們可能還會遇到比這複雜的多的改動,又或者,運營又想讓你換回以前的版本?emm。

在這裏,若是有了解過簡單工廠模式的朋友,可能會想到我可使用簡單工廠模式來搞定這個啊。

function factory($name)
{
    $modules = [
        'sms' => new MeiSms(),
    ];
    if (!isset($modules[$name])) {
        throw new \Exception('對象不存在。');
    }
    return $modules[$name];
}

在須要的地方直接調用 factory('sms') 這樣就能拿到一個 發送短信的對象,當需求改了後,我直接改造一下工廠就行了,不是也簡單了不少。

可是,到這裏你會發現一些問題,工廠生產出來的對象沒有類型提示,並且咱們在工廠內沒辦法限制類必需要實現哪些方法(固然你能夠把工廠搞的更復雜,加上接口校驗),可是到頭來你會發現,這裏咱們最初要作的事兒愈來愈遠,並且,愈來愈複雜,不是嗎?並且,工廠也不是那麼的易用。

服務容器

這裏就要看回咱們的 服務容器 ,首先咱們先看看控制器的文檔中關於依賴注入部分的說明,這也是不少人最開始瞭解到 依賴注入 的地方。

  • 構造函數注入
Laravel 服務容器 解析全部的控制器。所以,你能夠在控制器的構造函數中使用類型提示可能須要的依賴項。依賴聲明會被自動解析並注入到控制器實例
  • 方法注入
處理構造函數注入,你還能夠在控制器方法中輸入類型提示依賴項。方法注入最多見的用例是在控制器方法中注入 Illuminate\Http\Request 的實例

當咱們每次建立一個控制器方法,都會主動填寫第一個參數,即 Request $request 你是否有注意過那個 Request 的參數呢?是否是很神奇呢?

爲何我什麼都沒有作,我就可使用它, 並且,着並不限於 Laravel 內置的對象,咱們本身寫的對象也是能夠的,並且在使用 IDE 開發時 咱們還能夠方便的使用類型提示,這些工做,就是 服務容器 幫咱們作的。

當容器解析到這個方法時,當方法存在,就會用反射,來解析這個方法中須要的參數以及參數的類型。
ReflectionFunctionAbstract::getParameters,而後在容器中查找咱們是否 bind 有這個類型,若是沒有,就繼續使用容器去建立這個類(由於這個類的構造方法中可能還會依賴其餘的類)直到所依賴的類實例化完成,而且,把實例存到容器。

到這裏你是否是以爲服務容器沒有卵用?就是幫咱們遞歸實例化類而已。那你就太年輕了,既然上面說到了咱們要用 服務容器 來解決 簡單工廠 所解決的問題,難倒我還會騙你不成?哈哈

這裏就要開始說第一個了 bind

bind 方法

首先 咱們來看一下 Laravel 中 bind 方法的實現。

/**
     * Register a binding with the container.
     *
     * @param  string  $abstract
     * @param  \Closure|string|null  $concrete
     * @param  bool  $shared
     * @return void
     */
    public function bind($abstract, $concrete = null, $shared = false)
    {
        // If no concrete type was given, we will simply set the concrete type to the
        // abstract type. After that, the concrete type to be registered as shared
        // without being forced to state their classes in both of the parameters.
        $this->dropStaleInstances($abstract);

        if (is_null($concrete)) {
            $concrete = $abstract;
        }

        // If the factory is not a Closure, it means it is just a class name which is
        // bound into this container to the abstract type and we will just wrap it
        // up inside its own Closure to give us more convenience when extending.
        if (! $concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->bindings[$abstract] = compact('concrete', 'shared');

        // If the abstract type was already resolved in this container we'll fire the
        // rebound listener so that any objects which have already gotten resolved
        // can have their copy of the object updated via the listener callbacks.
        if ($this->resolved($abstract)) {
            $this->rebound($abstract);
        }
    }
    /**
     * Drop all of the stale instances and aliases.
     *
     * @param  string  $abstract
     * @return void
     */
    protected function dropStaleInstances($abstract)
    {
        unset($this->instances[$abstract], $this->aliases[$abstract]);
    }

    /**
     * Get the Closure to be used when building a type.
     *
     * @param  string  $abstract
     * @param  string  $concrete
     * @return \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);
        };
    }

在一開始就調用了 $this->dropStaleInstances($abstract); 追蹤源碼咱們看到,他直接刪除了 第一個參數對應的已經存在的實例和別名。

而後接着往下,當 $concrete 不是一個 Closure (匿名方法) 時,他會去作一些包裝處理成一個匿名方法,最後存入了 bindings 這個屬性,鍵爲 $abstract,值是一個數組,其中 concrete 是包裝後的方法,而後調用了容器的 make 。。
當運行到這個位置

if ($this->resolved($abstract)) {
    $this->rebound($abstract);
}

會先去判斷這個是否已經解析過了,進行更新已經存放在容器中的副本。

這就是 bind 所幹的事兒,描述簡單點兒,就是給一個類、類實例、匿名方法提供了一個別名綁定到了容器中去。

當咱們使用 resolve 傳入剛剛的別名時就能解析拿到咱們以前綁定的實例。

趕忙來試試?

咱們先打開 bootstrap/app.php,能夠看到一開始,就建立了一個 Application 的實例,咱們就試着在 $app 被 return 前面給綁定一下。

  • bootstrap/app.php:54
$app->bind('hello', \App\Tools\MeiSms::class);
return $app;
  • routes/web.php:23
Route::any('hello', function ()
{
    $resolve = resolve('hello');
    var_dump(get_class($resolve));
});

打開瀏覽器看一下

clipboard.png

不錯吧,可是到了這裏,咱們只是作到了和簡單工廠差很少的事情,接下來咱們改造一下咱們的短信類。

首先,咱們約定一個接口,短信驗證必需要發送短信和驗證短信驗證碼兩個方法,分別爲 send($phone)check($phone,$code) 方法。

  • Sms.php
<?php


namespace App\Contracts\Interfaces;


interface Sms
{
    /**
     * @param string $phone 手機號
     * @return bool 是否發送成功
     */
    public function send(string $phone): bool;

    /**
     * @param string $phone 手機號
     * @param string $code 用戶填寫的驗證碼
     * @return bool 是否驗證經過
     */
    public function check(string $phone, string $code): bool;
}

而後,咱們用把以前的 MeiSms 類實現實現這個接口。

  • MeiSms.php
<?php


namespace App\Tools;


use App\Contracts\Interfaces\Sms;

class MeiSms implements Sms
{
    public function send(string $phone): bool
    {
        $code = mt_rand(1e3, 1e4 - 1);
        // TODO ... 經過接口發送
        // 存放驗證碼到 Redis
        $cacheManager = cache();
        // 設定 60 分鐘失效
        $cacheManager->set('sms:' . $phone, $code, 5 * 60);
        return true;
    }

    public function check(string $phone, string $code): bool
    {
        $cacheManager = cache();
        return $cacheManager->get('sms:' . $phone) === $code;
    }
}

如今,咱們再去 bootstrap/app.php 中註冊,此次就跟之前的有點兒不同了。

$app->bind(\App\Contracts\Interfaces\Sms::class, \App\Tools\MeiSms::class);
return $app;

能夠看到,咱們的第一個參數傳遞的時 Sms 的接口,要綁定上去的時 MeiSms 類,接着咱們改造一下路由。

  • web.php
Route::any('hello', function (\App\Contracts\Interfaces\Sms $sms)
{
    var_dump(get_class($sms));
});
  • 結果

clipboard.png

你是否是拿剛剛的截圖騙我?上面明明限定的是 \App\Contracts\Interfaces\Sms 怎麼 打印出來的是 \App\Tools\MeiSms ,代碼竟然沒有報錯?

別驚訝,首先 \App\Tools\MeiSms 已經實現了 \App\Contracts\Interfaces\Sms 接口,因此在接口限定類型這是合法。

而由於這個方法調用時經過,容器進行調用的,容器會調用內部的 make 方法 進行一系列的依賴注入處理,當獲取到方法須要一個 \App\Contracts\Interfaces\Sms 類型的參數時,容器將類名字符串到已經綁定中去查找,由於咱們已經再前面註冊過,因此就至關於實現了給類一個別名,最終仍是由 \App\Tools\MeiSms 來執行結果給咱們。

好處

那麼回到議題,咱們在考慮咱們以前遇到問題,看上去已經解決了,那相比以前的方法有什麼好處呢,一個個來說。

  • 更好的規範
由於咱們在路由那裏限定了接口限定,因此咱們不用再擔憂調用 send 或者 check 不存在了
不用再擔憂由於 send 或者 check 方法 返回值參數不知道怎麼判斷結果了(由於咱們已經限定了只能返回 bool 值)
  • 再也不改動原來的業務代碼,更少的 bug
是的,沒錯。咱們再也不須要去改動現有的業務代碼,只須要把新加入的類實現接口後綁定到容器便可,其餘的都沒有發生改變。
  • 更好的測試

補缺

固然,說到這裏,你可能以爲我少說了什麼東西。

singleton 方法

其實從源碼很容易看到,singleton 仍是調用了 bind 方法,只是 shared 參數 不同,表示綁定了一個單例對象。

/**
     * Register a shared binding in the container.
     *
     * @param  string  $abstract
     * @param  \Closure|string|null  $concrete
     * @return void
     */
    public function singleton($abstract, $concrete = null)
    {
        $this->bind($abstract, $concrete, true);
    }

instance 方法。

這個和 bind 幾乎同樣,只是 bind 能夠綁定一個匿名方法或者直接類名(內部會處理)。
而 instance 正如其名字,用來綁定一個實例到容器裏面去。

結束

在小範圍看來,服務容器工廠模式有不少類似的地方,可是服務容器會讓你接觸到 PHP 的另外一個知識塊 反射,這個強大的 API 。

其實我也沒想通,我爲啥要用 Laravel 來舉例寫這篇文章。
由於看了一下 ThinkPHP 的實現,相對要好讀 容易一些。
其實一開始我是準備把兩個框架都說一下,可是感受都又差很少,就挑了 Laravel ,雖然其中要複雜些,甚至不少點都沒有照顧到,可是我仍是把這個文章寫出來了,不是嗎?

也但願,這個文章對你在瞭解服務容器方面有所幫助。固然,我更加推薦你去 Laravel 或者 ThinkPHP 的源碼,由於這樣可以更加加深本身對其的理解。

參考資料

相關文章
相關標籤/搜索