服務容器

介紹

Laravel的服務容器是很強大的工具,用來管理類別的依賴並執行依賴注入。新手聽到依賴注入這個名詞通常都會一臉矇逼,聽不懂這是在講甚麼小朋友,不代表你的理解力差

其實依賴注入的意思就如同字面所說,類別所依賴的對象,比如物件參數是透過建構子或者是設定子這幾種方式來傳入而非自己產生

就讓我們來看個簡單的例子:

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Repositories\UserRepository;
use App\Models\User;

class UserController extends Controller
{
    /**
     * 用戶儲存庫實作.
     *
     * @var 用戶儲存庫
     */
    protected $users;

    /**
     * 建立一個新的控制器實例.
     *
     * @param  用戶儲存庫  $users
     * @return void
     */
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

    /**
     * 顯示某給定用戶的資料.
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        $user = $this->users->find($id);

        return view('user.profile', ['user' => $user]);
    }
}

在這個例子當中, UserController 需要從資料來源來獲取用戶資料。所以,我們需要注入一個服務,讓該服務傳回用戶資料。
在這個情境裡頭,這個 UserRepository用戶儲存庫就如同使用Eloquent來進行資料查詢後傳回資料。但不同點在於,因為這個儲存庫是被注入的,所以就很容易地將之置換成其他的實作,比方說改成透過API來獲得等等。也就是說我們也可以很輕鬆地去模擬或者創建一個 UserRepository 儲存庫的假實現來操作

深入理解服務容器,對於構建一個強大的、大型的應用,以及對 Laravel 核心本身都是極有幫助的

零設定解析技術

假如某類別沒有依賴關係或只有依賴其他具體的類別(沒有介面的意思),該容器就沒有必要在被建構時去考慮如何解析該類別。比方,你可能會在路由檔 routes/web.php 裏頭加入以下程式碼:

<?php

class Service
{
    //
}

Route::get('/', function (Service $service) {
    die(get_class($service));
});

在這個例子當中,當訪問應用的根路由('/')將會自動的解析 Service 類別並注入到你的路由處理器。這種改變是顛覆式的,這代表你可以發展你的應用並利用依賴注入而不需要擔心你的設定檔案變的超級肥大

幸運的,大多數你寫的類別會在建立Laravel應用時自動地透過容器來獲得依賴,包含了控制器本身. 事件偵聽器. 中介層,還有更多。除此之外,你可在 排程工作的handle() 裏頭利用類型提示依賴。一旦你嚐到自動且零設定依賴注入的好處,你將再也離不開它

何時要使用容器

感謝有零設定解析技術,你將經常在路由. 控制器. 事件偵聽器和任何沒有手動透過容器去進行互動的地方去用型別提示依賴

舉例來說,你可能在路由設定檔去型別提示 Illuminate\Http\Request 物件,這讓你能夠輕鬆地取得當前請求。儘管我們不需要去和容器打交道才能寫這段程式碼,它們依然在背後去處理這些依賴注入:

use Illuminate\Http\Request;

Route::get('/', function (Request $request) {
    // ...
});

在大多數狀況下,感謝有自動依賴注入和 facades,你能夠不需要手動綁定或透過容器去解析任何東西

那麼,究竟在甚麼時候你會需要手動的去與容器互動呢? 有兩種狀況

第一,假如你寫了一個類別,它有實作一個介面,而且你希望在路由或者是類別建構子去做類型提示,你就必須要告訴容器該怎麼去解析那個容器

第二,假如你寫了一個Laravel套件,希望將之分享給其他開發者,你就必須要綁定你套件的服務到容器裡頭

綁定

綁定基礎

幾乎所有的服務容器綁定都會在服務供應器中註冊,下面的多數範例將演示如何在服務供應器中使用容器

技巧:

如果某個容器不依賴於任何介面就沒必要在這個容器裡去綁定類別。容器不需要指定如何建構這些對象,因為它可以使用反射來自動解析這些對象

在服務供應器中,你總是可以通過 $this->app 屬性來訪問容器,我們可以通過容器的 bind 方法註冊綁定,bind 方法的第一個參數為要綁定的類別 / 介面名稱,第二個參數是一個返回類別實例的 Closure:

use App\Services\Transistor;
use App\Services\PodcastParser;

$this->app->bind(Transistor::class, function ($app) {
    return new Transistor($app->make(PodcastParser::class));
});

注意,我們接受容器本身作為解析器的參數。然後,我們可以使用容器來解析正在建構對象的子依賴

如前所述,你一般將會在服務供應器裡頭去與容器交互;因此,如果你想要在服務供應器之外去與容器互動;你就需要透過 App 這個 facade:

use App\Services\Transistor;
use Illuminate\Support\Facades\App;

App::bind(Transistor::class, function ($app) {
    // ...
});

記得只要它們沒有依賴任何界面,就不需要在容器內去綁定類別。容器不需要去被告知該如何建立這些對象,因為它能夠透過反射的技巧自動地去解析這些對象

綁定單例(Singleton)

singleton 方法將類別或介面綁定到只解析一次的容器中。一旦單例綁定被解析,相同的對象實例會在隨後的呼叫中回傳到容器中:

use App\Services\Transistor;
use App\Services\PodcastParser;

$this->app->singleton(Transistor::class, function ($app) {
    return new Transistor($app->make(PodcastParser::class));
});

綁定實例(Instance)

你也可以使用 instance 方法將現有對象實例綁定到容器中。所提供的實例會始終在隨後的呼叫回傳到容器中:

use App\Services\Transistor;
use App\Services\PodcastParser;

$service = new Transistor(new PodcastParser);

$this->app->instance(Transistor::class, $service);

綁定介面到實作

服務容器有一個很強大的功能,就是支持綁定介面到給定的實作。例如,如果我們有個 EventPusher 介面 和一個 RedisEventPusher 實作。一旦我們寫完了 EventPusher 介面的 RedisEventPusher 實作,我們就可以在服務容器中註冊它,像這樣:

use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;

$this->app->bind(EventPusher::class, RedisEventPusher::class);

這麼做相當於告訴容器:當一個類別需要實作 EventPusher 時,應該注入 RedisEventPusher。現在我們就可以在建構子函數或者任何其他通過服務容器注入依賴項的地方使用類型提示注入 EventPusher 介面。記得,控制器.事件偵聽器.中介層以及在Laravel應用裡的各種其他類型的類別總是透過容器來解析:

use App\Contracts\EventPusher;

/**
 * Create a new class instance.
 *
 * @param  \App\Contracts\EventPusher  $pusher
 * @return void
 */
public function __construct(EventPusher $pusher)
{
    $this->pusher = $pusher;
}

情境綁定

有時你可能有兩個類別使用了相同的介面,但你希望各自注入不同的實作。例如,有兩個控制器可能依賴了 Illuminate\Contracts\Filesystem\Filesystem 契約。Laravel 提供了一個簡單的且優雅的介面來定義這個行為:

use App\Http\Controllers\PhotoController;
use App\Http\Controllers\UploadController;
use App\Http\Controllers\VideoController;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;

$this->app->when(PhotoController::class)
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('local');
          });

$this->app->when([VideoController::class, UploadController::class])
          ->needs(Filesystem::class)
          ->give(function () {
              return Storage::disk('s3');
          });

綁定基礎值

當有一個類別不僅需要接受一些注入類別,還需要注入一個基礎值(比如整數)。你可以使用情境綁定來輕鬆注入你的類別所需要的任何值:

$this->app->when('App\Http\Controllers\UserController')
          ->needs('$variableName')
          ->give($value);

有時一個類別可能依賴於一系列的標記實例。使用 giveTagged 方法,您可以輕鬆地使用該標籤注入所有容器綁定:

$this->app->when(ReportAggregator::class)
    ->needs('$reports')
    ->giveTagged('reports');

綁定型別參數

有時您可能有一個使用可變參數建構子參數接收類型化對象陣列的類別(聽起來有點拗口,我同意,看下面的例子可能比較好理解):

<?php

use App\Models\Filter;
use App\Services\Logger;

class Firewall
{
    /**
     * logger 實例.
     *
     * @var \App\Services\Logger
     */
    protected $logger;

    /**
     * 過濾器 instances.
     *
     * @var array
     */
    protected $filters;

    /**
     * 建立一個新的類別實例.
     *
     * @param  \App\Services\Logger  $logger
     * @param  array  $filters
     * @return void
     */
    public function __construct(Logger $logger, Filter ...$filters)
    {
        $this->logger = $logger;
        $this->filters = $filters;
    }
}

使用情境綁定,您可以通過給 give 方法提供一個 Closure 來解決此依賴關係,該 Closure 返回已解析的 Filter 實例的陣列:

$this->app->when(Firewall::class)
          ->needs(Filter::class)
          ->give(function ($app) {
                return [
                    $app->make(NullFilter::class),
                    $app->make(ProfanityFilter::class),
                    $app->make(TooLongFilter::class),
                ];
          });

為了方便起見,您還可以只提供一個類別名稱陣列,以便在 Firewall類別 需要 Filter 實例時由容器解析:

$this->app->when(Firewall::class)
          ->needs(Filter::class)
          ->give([
              NullFilter::class,
              ProfanityFilter::class,
              TooLongFilter::class,
          ]);

可變依賴標記

有時,一個類別可能具有可變的依賴關係,該依賴關係被類型提示為給定的類別(Report $reports)。使用 needs 和 giveTagged 方法,您可以輕鬆地為給定依賴項注入帶有該標籤的所有容器綁定:

$this->app->when(ReportAggregator::class)
    ->needs(Report::class)
    ->giveTagged('reports');

標籤(Tagging)

有時候,你可能需要解析某個「分類」下的所有綁定。比如,你可能正在建構一個報表的分析器,它接收一個包含不同 Report 介面實現的陣列。註冊 Report 實作之後,你可以使用 tag 方法給他們分配一個標籤:

$this->app->bind(CpuReport::class, function () {
    //
});

$this->app->bind(MemoryReport::class, function () {
    //
});

$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');

一旦這些服務被打上標籤之後,你就能夠輕鬆地透過容器的 tagged 方法來解析這些它們:

$this->app->bind(ReportAnalyzer::class, function ($app) {
    return new ReportAnalyzer($app->tagged('reports'));
});

繼承綁定

extend 方法可以修改已解析的服務。比如,當一個服務被解析後,你可以添加額外的程式碼來修飾或者配置它。extend 方法接受一個 closure,該closure唯一的參數就是這個服務, 並返回修改過的服務。Closure接受被解析的服務以及容器實例:

$this->app->extend(Service::class, function ($service, $app) {
    return new DecoratedService($service);
});

解析

Make方法

你可以使用 make 方法從容器中解析出類別實例。make 方法接收你想要解析的類別或介面的名字:

use App\Services\Transistor;

$api = $this->app->make(Transistor::class);

假如有一些你的類別依賴無法透過容器加以解析,你可以透過將他們作為相關陣列傳進 makeWith 方法。舉例來說,我們能手動傳入 HelpSpot\API 服務所需要的 $id 建構子參數:

use App\Services\Transistor;

$api = $this->app->makeWith(Transistor::class, ['id' => 1]);

如果你的代碼處於無法訪問 $app 變量的位置,則可以用 App facade 來從容器去解析類別實例:

use App\Services\Transistor;
use Illuminate\Support\Facades\App;

$api = App::make(Transistor::class);

假如你希望能將Laravel容器實例注入到被容器解析的類別裡頭,你能夠在你的類別建構子型別提示 Illuminate\Container\Container 類別:

use Illuminate\Container\Container;

/**
 * 建立一個新的類別實例.
 *
 * @param  \Illuminate\Container\Container
 * @return void
 */
public function __construct(Container $container)
{
    $this->container = $container;
}

自動注入

另外,並且更重要的是,你可以簡單地使用「型別提示」 的方式在類別的建構子中注入那些需要容器解析的依賴類別,包括 控制器、事件監聽器、中介層 等 。此外你也可以在序列任務的 handle 方法中使用「型別提示」注入依賴。實際上,這才是大多數對象應該被容器解析的方式。

例如,你可以在控制器的建構子中添加一個 repository 的型別提示,然後這個 repository 將會被自動解析並注入類別中:

<?php

namespace App\Http\Controllers;

use App\Repositories\UserRepository;

class UserController extends Controller
{
    /**
     * 用戶儲存庫實例.
     *
     * @var \App\Repositories\UserRepository
     */
    protected $users;

    /**
     * 建立一個新的控制器實例.
     *
     * @param  \App\Repositories\UserRepository  $users
     * @return void
     */
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

    /**
     * 利用所給的ID來呈現使用者資料.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        //
    }
}

容器事件

服務容器每次解析對象會觸發一個事件,你可以使用 resolving 方法監聽這個事件:

use App\Services\Transistor;

$this->app->resolving(Transistor::class, function ($api, $app) {
    // 當容器解析型別為 "Transistor::class" 的物件時被呼叫...
});

$this->app->resolving(function ($object, $app) {
    // 當容器解析任何型別的物件時被呼叫...
});

正如你所看到的,被解析的對象將會被傳入回呼函數,這使得你能夠在對象被傳給調用者之前給它設置額外的屬性

PSR-11

Laravel 的服務容器實現了 PSR-11 介面。因此,你可以使用 PSR-11 容器『介面型別提示』來獲取一個 Laravel 容器的實例:

use App\Services\Transistor;
use Psr\Container\ContainerInterface;

Route::get('/', function (ContainerInterface $container) {
    $service = $container->get(Transistor::class);

    //
});

如果無法解析給定的辨識符,則將會引發異常。未綁定辨識符時,會拋出 Psr\Container\NotFoundExceptionInterface 異常。如果辨識符已綁定但無法解析,會拋出 Psr\Container\ContainerExceptionInterface 異常

Select a repo