Laravel 5.4 入門系列 13. 終篇: 小白也能看懂的 Laravel 核心概念講解

心智極客發表於2019-02-16

自動依賴注入

什麼是依賴注入,用大白話將通過型別提示的方式向函式傳遞引數。

例項 1

首先,定義一個類:

/routes/web.php
class Bar {}

假如我們在其他地方要使用到 Bar 提供的功能(服務),怎麼辦,直接傳入引數即可:

/routes/web.php
Route::get(`bar`, function(Bar $bar) {
    dd($bar);
});

訪問 /bar,顯示 $bar 的例項:

Bar {#272}

也就是說,我們不需要先對其進行例項!如果學過 PHP 的物件導向,都知道,正常做法是這樣:

class Bar {}
$bar = new Bar();
dd($bar);

例項 2

可以看一個稍微複雜的例子:

class Baz {}
class Bar 
{
    public $baz;

    public function __construct(Baz $baz)
    {
        $this->baz = $baz;
    }
    
}
$baz = new Baz();
$bar = new Bar($baz);
dd($bar);

為了在 Bar 中能夠使用 Baz 的功能,我們需要例項化一個 Baz,然後在例項化 Bar 的時候傳入 Baz 例項。

在 Laravel 中,不僅僅可以自動注入 Bar,也可以自動注入 Baz:

/routes/web.php
class Baz {}
class Bar 
{
    public $baz;

    public function __construct(Baz $baz)
    {
        $this->baz = $baz;
    }
    
}

Route::get(`bar`, function(Bar $bar) {
       dd($bar->baz);
});

顯示結果:

Baz {#276}

小結

通過上述兩個例子,可以看出,在 Laravel 中,我們要在類或者函式中使用其他類體用的服務,只需要通過型別提示的方式傳遞引數,而 Laravel 會自動幫我們去尋找響對應的依賴。

那麼,Laravel 是如何完成這項工作的呢?答案就是通過服務容器。

服務容器

什麼是服務容器

服務容器,很好理解,就是裝著各種服務例項的特殊類。可以通過「去餐館吃飯」來進行類比:

  • 吃飯 – 使用服務,即呼叫該服務的地方

  • 飯 – 服務

  • 盤子 – 裝飯的容器,即服務容器

  • 服務員 – 服務提供者,負責裝飯、上飯

這個過程在 Laravel 中如何實現呢?

定義 Rice 類:

/app/Rice.php
<?php

namespace App;

class Rice
{
    public function food()
    {
        return `香噴噴的白米飯`;
    }
}
  • 把飯裝盤子

在容器中定義了名為 rice 的變數(你也可以起其他名字,比如 rice_container),繫結了 Food 的例項:

app()->bind(`rice`, function (){
    return new AppRice();
});

也可以寫成:

app()->bind(`rice`,AppRice::class);

現在,吃飯了,通過 make 方法提供吃飯的服務:

Route::get(`eat`, function() {
       
       return app()->make(`rice`)->food(); 
       // 或者 return resolve(`rice`)->food();

});

make 方法傳入我們剛才定義的變數名即可呼叫該服務。

訪問 /eat,返回 香噴噴的白米飯

為了方便起見,我們在路由檔案中直接實現了該過程,相當於自給自足。但是服務通常由服務提供者來管理的。

因此,我們可以讓 AppServiceProvider 這個服務員來管理該服務:

/app/Providers/AppServiceProvider.php
namespace AppProviders;

public function register()
{
    $this->app->bind(`food_container`,Rice::class);
}

更為常見的是,我們自己建立一個服務員:

$ php artisan make:provider RiceServiceProvider

註冊:

/app/Providers/RiceServiceProvider.php
<?php

use AppRice;
public function register()
{
    $this->app->bind(`rice`,Rice::class);
}

這裡定義了 register() 方法,但是還需要呼叫該方法才能真正繫結服務到容器,因此,需要將其新增到 providers 陣列中:

/config/app.php
`providers` => [
   AppProvidersRiceServiceProvider::class,
],

這一步有何作用呢?Laravel 在啟動的時候會訪問該檔案,然後呼叫裡面的所有服務提供者的 register() 方法,這樣我們的服務就被繫結到容器中了。

小結

通過上述的例子,基本上可以理解服務容器和服務提供者的使用。當然了,我們更為常見的還是使用型別提示來傳遞引數:

use AppRice;

Route::get(`eat`, function(Rice $rice) {
       return $rice->food();

});

在本例中,使用自動依賴注入即可。不需要在用 bind 來手動繫結以及 make 來呼叫服務。那麼,為什麼還需要 bindmake 呢? make 比較好理解,我們有一些場合 Laravel 不能提供自動解析,那麼這時候手動使用 make 解析就可以了,而 bind 的學問就稍微大了點,後面將會詳細說明。

門面

門面是什麼,我們回到剛才的「吃飯」的例子:

Route::get(`eat`, function(Rice $rice) {
       return $rice->food();

});

在 Laravel,通常還可以這麼寫:

Route::get(`eat`, function() {
       return Rice::food();
});

或者

Route::get(`eat`, function() {
       return rice()->food();
});

那麼,Laravel 是如何實現的呢?答案是通過門面。

門面方法實現

先來實現 Rice::food(),只需要一步:

/app/RiceFacade.php
<?php 

namespace App;
use IlluminateSupportFacadesFacade;

class RiceFacade extends Facade
{
   
    protected static function getFacadeAccessor()
    {
        return `rice`;
    }
}

現在,RiceFacade 就代理了 Rice 類了,這就是門面的本質了。我們就可以直接使用:

Route::get(`eat`, function() {

    dd(AppRiceFacade::food());

});

因為 AppRiceFacade 比較冗長,我們可以用 php 提供的 class_alias 方法起個別名吧:

/app/Providers/RiceServiceProvider.php
public function register()
{  
   $this->app->bind(`rice`,AppRice::class);
   class_alias(AppRiceFacade::class, `Rice`);
}

這樣做的話,就實現了一開始的用法:

Route::get(`eat`, function() {
       return Rice::food();
});

看上去就好像直接呼叫了 Rice 類,實際上,呼叫的是 RiceFacade 類來代理,因此,個人覺得Facade 翻譯成假象比較合適。

最後,為了便於給代理類命名,Laravel 提供了統一命名別名的地方:

/config/app.php

`aliases` => [

    `Rice` => AppRiceFacade::class,

],

門面實現過程分析

首先:

Rice::food();

因為 Rice 是別名,所以實際上執行的是:

AppRiceFacade::food()

但是我們的 RiceFacade 類裡面並沒有定義靜態方法 food 啊?怎麼辦呢?直接丟擲異常嗎?不是,在 PHP 裡,如果訪問了不可訪問的靜態方法,會先呼叫 __callstatic,所以執行的是:

AppRiceFacade::__callStatic()

雖然我們在 RiceFacade 中沒有定義,但是它的父類 Facade 已經定義好了:

/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php
public static function __callStatic($method, $args)
{   
     
     // 例項化  Rice {#270}
    $instance = static::getFacadeRoot();
    
     // 例項化失敗,丟擲異常
    if (! $instance) {
        throw new RuntimeException(`A facade root has not been set.`);
    }
    
     // 呼叫該例項的方法
    return $instance->$method(...$args);
}

主要工作就是第一步例項化:

public static function getFacadeRoot()
{
    return static::resolveFacadeInstance(static::getFacadeAccessor());
    // 本例中:static::resolveFacadeInstance(`rice`)
}

進一步檢視 resolveFacadeInstance() 方法:

 protected static function resolveFacadeInstance($name)
    {   
          // rice 是字串,因此跳過該步驟
        if (is_object($name)) {
            return $name;
        }
         
         // 是否設定了 `rice` 例項
        if (isset(static::$resolvedInstance[$name])) {
            return static::$resolvedInstance[$name];
        }
         
        return static::$resolvedInstance[$name] = static::$app[$name];
    }

第一步比較好理解,如果我們之前在 RiceFacade 這樣寫:

protected static function getFacadeAccessor()
{

    return new AppRice;

}

那麼就直接返回 Rice 例項了,這也是一種實現方式。

主要難點在於最後這行:

return static::$resolvedInstance[$name] = static::$app[$name];

看上去像是在訪問 $app陣列,實際上是使用 陣列方式來訪問物件,PHP 提供了這種訪問方式介面,而 Laravel 實現了該介面。

也就是說,$app 屬性其實就是對 Laravel 容器的引用,因此這裡實際上就是訪問容器上名為 rice 的物件。而我們之前學習容器的時候,已經將 rice 繫結了 Rice 類:

public function register()
{  
   $this->app->bind(`rice`,AppRice::class);
   // class_alias(AppRiceFacade::class, `Rice`);
}

所以,其實就是返回該類的例項了。懂得了服務容器和服務提供者,理解門面也就不難了。

輔助方法實現

輔助方法的實現,更簡單了。不就是把 app->make(`rice`) 封裝起來嘛:

/vendor/laravel/framework/src/Illuminate/Foundation/helpers.php
if (! function_exists(`rice`)) {
  
    function rice()
    {   
        return app()->make(`rice`);
        // 等價於 return app(`rice`);
        // 等價於 return app()[`rice`];
    }
} 

然後我們就可以使用了:

Route::get(`eat`, function() {

    dd(rice()->food());
});

小結

Laravel 提供的三種訪問類的方式:

  • 依賴注入:通過型別提示的方式實現自動依賴注入

  • 門面:通過代理來訪問類

  • 輔助方法:通過方法的方式來訪問類

本質上,這三種方式都是藉助於服務容器和服務提供者來實現。那麼,服務容器本身有什麼好處呢?我們接下來著重介紹下。

IOC

不好的實現

我們來看另外一個例子(為了方便測試,該例子都寫在路由檔案中),假設有三種型別的插座:USB、雙孔、三孔插座,分別提供插入充電的服務:

class UsbsocketService
{
    public function insert($deviceName){
        return $deviceName." 正在插入 USB 充電";
    }
}

class DoubleSocketService
{
    public function insert($deviceName){
        return $deviceName." 正在插入雙孔插座充電";
    }
}

class ThreeSocketService
{
    public function insert($deviceName){
        return $deviceName." 正在插入三孔插座充電";
    }
}

裝置要使用插座的服務來充電:

class Device {

    protected $socketType; // 插座型別
    public function __construct()
    {
        $this->socketType = new UsbSocketService();
    }

    public function power($deviceName)
    {    
        return $this->socketType->insert($deviceName);
    }
}

現在有一臺手機要進行充電:

Route::get(`/charge`,function(){
       
   $device = new Device();
   return $device->power("手機");
    
});

因為 Laravel 提供了自動依賴注入功能,因此可以寫成:

Route::get(`/charge/{device}`,function(Device $device){
       
   return $device->power("手機");
    
});

訪問 /charge/phone,頁面顯示 phone 正在插入 USB 充電

假如,現在有一臺電腦要充電,用的是三孔插座,那麼我們就需要去修改 Device 類:

$this->socketType = new ThreeSocketService();

這真是糟糕的設計,裝置類對插座服務類產生了依賴。更換裝置型別時,經常就要去修改類的內部結構。

好的實現

為了解決上面的問題,可以參考「IOC」思路:即將依賴轉移到外部。來看看具體怎麼做。

首先定義插座型別介面:

interface SocketType {
    public function insert($deviceName);
}

讓每一種插座都實現該介面:

class UsbsocketService implements SocketType
{
    public function insert($deviceName){
        return $deviceName." 正在插入 USB 充電";
    }
}

class DoubleSocketService implements SocketType
{
    public function insert($deviceName){
        return $deviceName." 正在插入雙孔插座充電";
    }
}

class ThreeSocketService implements SocketType
{
    public function insert($deviceName){
        return $deviceName." 正在插入三孔插座充電";
    }
}

最後,裝置中傳入介面型別而非具體的類:

class Device {

    protected $socketType; // 插座型別
    public function __construct(SocketType $socketType) // 傳入介面
    {
        $this->socketType = $socketType;
    }

    public function power($deviceName)
    {    
        return $this->socketType->insert($deviceName);
    }
}

例項化的時候再決定使用哪種插座型別,這樣依賴就轉移到了外部:

Route::get(`/charge`,function(){
   
   $socketType = new ThreeSocketService();
   $device = new Device($socketType);
   echo $device->power("電腦");    
});

我們現在可以再不修改類結構的情況下,方便的更換插座來滿足不同裝置的充電需求:

Route::get(`/charge`,function(){
   
   $socketType = new DoubleSocketService();
   $device = new Device($socketType);
   echo $device->power("檯燈");    
});

自動依賴注入的失效

上面舉的例子,我們通過 Laravel 的自動依賴注入可以進一步簡化:

Route::get(`/charge`,function(Device $device){ 
       echo $device->power("電腦");
});

這裡的型別提示有兩個,一個是 Device $device,一個是 Device 類內部建構函式傳入的 SocketType $sockType。第一個沒有問題,之前也試過。但是第二個 SocketType 是介面,而 Laravel 會將其當成類試圖去匹配 SocketType 的類並將其例項化,因此訪問 /charge 時候就會報錯:

Target [SocketType] is not instantiable while building [Device].

錯誤原因很明顯,Laravel 沒法自動繫結介面。因此,我們就需要之前的 bind 方法來手動繫結介面啦:

app()->bind(`SocketType`,ThreeSocketService::class);
Route::get(`/charge`,function(Device $device){
       
       echo $device->power("電腦");
    
});

現在,如果要更換裝置,我們只需要改變繫結的值就可以了:

app()->bind(`SocketType`,DoubleSocketService::class);
Route::get(`/charge`,function(Device $device){
       
       echo $device->power("檯燈");
    
});

也就是說,我們將依賴轉移到了外部之後,進一步由第三方容器來管理,這就是 IOC。

契約

契約,不是什麼新奇的概念。其實就是上一個例子中,我們定義的介面:

interface SocketType {
    public function insert($deviceName);
}

通過契約,我們就可以保持鬆耦合了:

public function __construct(SocketType $socketType) // 傳入介面而非具體的插座型別
{
    $this->socketType = $socketType;
}

然後服務容器再根據需要去繫結哪種服務即可:

app()->bind(`SocketType`,UsbSocketService::class);
app()->bind(`SocketType`,DoubleSocketService::class);
app()->bind(`SocketType`,ThreeSocketService::class);

Laravel 5.4 入門系列告一段落,接下來準備學習 Vue ?


參考資料:

相關文章