簡單介紹 Laravel 的服務容器

Laravel 提供一種名為服務容器(Service Container)的工具,用來管理類與類之間的依賴與依賴注入。

但在介紹服務容器之前,先來介紹一下什麼是依賴?什麼是依賴注入?

什麼是依賴?


如果一個A類中的方法需要使用到B類,那麼我們可以說A類依賴於B類,A 類使用B類的地方越多,就代表依賴越強。

如下所例,John 中多個方法都會需要使用 Iphone,因此 JohnIphone 有著很強的依賴性。(但這不是個好設計,程式設計會盡量避免強依賴)

<?php

class Iphone
{
    public function video()
    {
        echo 'play video';
    }

    public function music()
    {
        echo 'play music';
    }
}

class John
{
    public function watchVideo()
    {
        $iphone = new Iphone();
        $iphone->video();
    }

    public function ListenToMusic()
    {
        $iphone = new Iphone();
        $iphone->music();
    }
}

什麼是依賴注入?


假設今天需要更改設計,John 需要將手機 Iphone 換成 Nokia

// before
$iphone = new Iphone();

// after
$nokia = new Nokia();

雖然看起來修改的地方不多,似乎不會是個大工程。

但如果不是只有 John 需要進行更改,可能還有其他類如 CindyTonyAllen ... 等,這些類也有在使用 Iphone,而且每一個類中可能都多達 10 多種方法,每個方法都需要 Iphone,那麼修改起來就會是個惡夢。

除了可能有多個類都依賴於 Iphone 之外,假設之後又要將手機換成 Pixel,那麼換成 Nokia 所做的修改,就必須再做一次,真正的惡夢。

為了避免這樣的情況,我們可以使用依賴注入的方式,來降低類之間的依賴(解耦)。

<?php

interface Mobile
{

}

// ...

class Nokia implements Mobile
{
    public function video()
    {
        echo 'play video';
    }

    public function music()
    {
        echo 'play music';
    }
}

class John
{
    public function __construct(
        public Mobile $mobile
    )
    {
    }

    public function watchVideo()
    {
        $this->mobile->video();
    }

    public function ListenToMusic()
    {
        $this->mobile->music();
    }
}

// $iphone = new Iphone();
// $john = new John($iphone);

// 改為使用 Nokia 
$nokia = new Nokia();
$john = new John($nokia);

// 假設之後又需要更改成 Pixel
// 只需要再新增一個實作 mobile 介面的 pixel 並傳入 john 即可
// 無需修改 John 內部的方法
$pixel = new Pixel();
$john = new John($pixel);

我們新增了一個 Mobile 介面,IphoneNokia 均需要實作此介面,在建立 John 實例的時候,只需要傳入一個實作 Mobile 介面的實例即可,無需修改 John 內部的任何方法。

John 並不關心手機使用的是否為 iPhone (Iphone)或是 Nokia(Nokia),只要是手機就好(Mobile)。

因此 John 對於 iPhone 的依賴便不存在,因為 John 可以自行決定要使用哪一種手機,只要是手機,John 都可以拿來使用。

這樣的設計原則又稱為控制反轉(Inversion of Control,縮寫為 IoC),依賴注入是實現控制反轉的一種方式。

服務容器


在 Laravel 中,只要將參數的類型聲明(Type declarations)設定好,執行請求時,服務容器就根據類型聲明,自動幫我們新建參數類的實例。

舉個例子,新建一個 PaymentService,並在 ProductControllershow() 方法中使用。

namespace App\Services;

class PaymentService
{
    public function process()
    {
        return 'Payment processed';
    }
}

不需要在 __construct 中新建 PaymentService 的實例,或是使用 new 關鍵字,就可以直接使用 PaymentService 實例的 process() 方法。

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Services\PaymentService;

class ProductController extends Controller
{
    public function show(PaymentService $paymentService)
    {
        // Payment processed
        echo $paymentService->process();
    }
}

如果我們 PaymentService 需要依賴其他類別 PaymentRepository,Laravel 的服務容器也會自動幫我們建立依賴類的實例。

新增一個 PaymentRepository

namespace App\Repositories;

class PaymentRepository
{
    public function create()
    {
        return 'create payment';
    }
}

PaymentService 中新增一個 createPayment() 方法,該方法需要使用到 PaymentRepository

use App\Repositories\PaymentRepository;

class PaymentService
{
    protected PaymentRepository $paymentRepository;

    public function __construct(PaymentRepository $paymentRepository)
    {
        $this->paymentRepository = $paymentRepository;

    }

    // 這裡新增一個 createPayment() 方法,其中會使用 PaymentRepository
    public function createPayment()
    {
        return $this->paymentRepository->create();
    }

    // ...
}

PostController 中,直接使用 createPayment() 並不會發生錯誤,因為服務容器已幫我們建立好 PaymentServicePaymentRepository 的實例。

class PostController extends Controller
{
    public function show(PaymentService $paymentService)
    {
        // create payment
        echo $paymentService->createPayment();
    }
}

可以在使用 Laravel 的 helper app() 查看目前有服務容器已綁定哪些類別。

dd(app());
2022_03_08_22_25_30_6227675a8cda7.png

在 Application  的屬性 resolved 中,可以看到服務容器已綁定哪些類別,最下方有我們剛剛新建的 PaymentRepositoryPaymentService

如果在依賴中放入無法實例化的類型,例如介面或是原始型別(Primitive type,例如 int 與 string),就會發生錯誤,例如下面的例子。

class PaymentService
{
    protected string $secretKey;

    // 因為 Laravel 無法知道 $secretKey 的值,所以無法實例 PaymentService 而出現錯誤
    public function __construct(string $secretKey)
    {
        $this->secretKey = $secretKey;
    }

    // ...
}
2022_03_08_22_27_23_622767cbc4a59.png

這時候我們可以在 AppServiceProvider 中告訴 Laravel,PaymentService 可以怎麼實例化。

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(PaymentService::class, function () {
            // $secretKey 的值設定為 'hello world'
            return new PaymentService('hello world!');
        });
    }

    // ...
}

新增一個 Payment 的介面:PaymentServiceContract

interface PaymentServiceContract
{
    public function process();
}

並嘗試在 PostControllershow() 方法中使用。

class PostController extends Controller
{
    // 介面無法實例化,所以會發生錯誤
    public function show(PaymentServiceContract $paymentService)
    {
        // create payment
        echo $paymentService->createPayment();
    }
}

因為介面無法實例化,所以 Laravel 無法判斷要注入依賴的類是什麼,這時一樣可以在 AppServiceProvider 設定,告訴服務容器 PaymentServiceContract 介面需要實例化哪個類。

$this->app->bind(PaymentServiceContract::class, PaymentService::class);

這裡可以使用 callback,在 callback 中加入條件判斷來綁定不一樣的類別。

$this->app->bind(PaymentServiceContract::class, function () {
    if (request()->bind === 'hello') {
        return PaymentService::class;
    }
});

參考資料


sharkHead
written by
sharkHead

後端工程師, PHP 基金會每月 5 鎂小額贊助人 稍微擅長 PHP、Python 與 Google Search,偶爾寫寫 TypeScript 對於逗號後面必須加空格有著絕對的堅持