Laravel 提供一種名為服務容器(Service Container)的工具,用來管理類與類之間的依賴與依賴注入。
但在介紹服務容器之前,先來介紹一下什麼是依賴?什麼是依賴注入?
什麼是依賴?
如果一個A類中的方法需要使用到B類,那麼我們可以說A類依賴於B類,A 類使用B類的地方越多,就代表依賴越強。
如下所例,John
中多個方法都會需要使用 Iphone
,因此 John
對 Iphone
有著很強的依賴性。(但這不是個好設計,程式設計會盡量避免強依賴)
<?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
需要進行更改,可能還有其他類如 Cindy
、Tony
、Allen
... 等,這些類也有在使用 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
介面,Iphone
與 Nokia
均需要實作此介面,在建立 John
實例的時候,只需要傳入一個實作 Mobile
介面的實例即可,無需修改 John
內部的任何方法。
John 並不關心手機使用的是否為 iPhone (Iphone
)或是 Nokia(Nokia
),只要是手機就好(Mobile
)。
因此 John 對於 iPhone 的依賴便不存在,因為 John 可以自行決定要使用哪一種手機,只要是手機,John 都可以拿來使用。
這樣的設計原則又稱為控制反轉(Inversion of Control,縮寫為 IoC),依賴注入是實現控制反轉的一種方式。
服務容器
在 Laravel 中,只要將參數的類型聲明(Type declarations)設定好,執行請求時,服務容器就根據類型聲明,自動幫我們新建參數類的實例。
舉個例子,新建一個 PaymentService
,並在 ProductController
的 show()
方法中使用。
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()
並不會發生錯誤,因為服務容器已幫我們建立好 PaymentService
與 PaymentRepository
的實例。
class PostController extends Controller
{
public function show(PaymentService $paymentService)
{
// create payment
echo $paymentService->createPayment();
}
}
可以在使用 Laravel 的 helper app()
查看目前有服務容器已綁定哪些類別。
dd(app());
在 Application 的屬性 resolved 中,可以看到服務容器已綁定哪些類別,最下方有我們剛剛新建的 PaymentRepository
與 PaymentService
。
如果在依賴中放入無法實例化的類型,例如介面或是原始型別(Primitive type,例如 int 與 string),就會發生錯誤,例如下面的例子。
class PaymentService
{
protected string $secretKey;
// 因為 Laravel 無法知道 $secretKey 的值,所以無法實例 PaymentService 而出現錯誤
public function __construct(string $secretKey)
{
$this->secretKey = $secretKey;
}
// ...
}
這時候我們可以在 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();
}
並嘗試在 PostController
的 show()
方法中使用。
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;
}
});