1. 程式人生 > >Laravel 5.5 底層原理:服務容器

Laravel 5.5 底層原理:服務容器

簡介

Laravel 服務容器是用於管理類的依賴和執行依賴注入的工具。

依賴注入的本質是通過建構函式或者某些情況下通過 setter 方法,將類的依賴注入到類中。

來看一個簡單的例子:

<?php

namespace App\Http\Controllers;

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

class UserController extends Controller
{
    /**
     * 使用者儲存庫的實現。
     *
     * @var UserRepository
     */
    protected $users;

    /**
     * 建立新的控制器例項。
     *
     * @param  UserRepository  $users
     * @return void
     */
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }

    /**
     * 顯示指定使用者的 profile。
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        $user = $this->users->find($id);

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

在本例中,UserController 需要從資料來源獲取使用者,所以,我們注入了一個可以獲取使用者的服務 UserRepository,其扮演的角色類似使用 Eloquent 從資料庫獲取使用者資訊。注入 UserRepository 後,我們可以在其基礎上封裝其他實現,也可以模擬或者建立一個假的 UserRepository 實現用於測試。

深入理解 Laravel 服務容器對於構建功能強大的大型 Laravel 應用而言至關重要,對於貢獻程式碼到 Laravel 核心也很有幫助。

繫結

幾乎所有的服務容器繫結都是在服務提供者中完成。因此本文件的演示例子用到的容器都是在服務提供者中繫結。

注:如果一個類沒有基於任何介面那麼就沒有必要將其繫結到容器。容器並不需要被告知如何構建物件,因為它會使用 PHP 的反射服務自動解析出具體的物件。

簡單繫結

在服務提供者中,可以通過 $this->app 屬性訪問容器。我們可以通過 bind 方法進行繫結。

bind 方法需要兩個引數,第一個引數是我們想要註冊的類名或介面名稱,第二個引數是返回類的例項的閉包。

$this->app->bind('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});

注意到我們將容器本身作為解析器的一個引數,然後我們可以使用該容器來解析我們正在構建的物件的子依賴。

繫結一個單例

singleton 方法繫結一個只會解析一次的類或介面到容器,然後接下來對容器的呼叫將會返回同一個物件例項。

$this->app->singleton('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});

繫結例項

你還可以使用 instance 方法繫結一個已存在的物件例項到容器,隨後呼叫容器將總是返回給定的例項:

$api = new HelpSpot\API(new HttpClient);

$this->app->instance('HelpSpot\API', $api);

繫結初始資料

當你有一個類不僅需要接受一個注入類,還需要注入一個基本值(比如整數)。你可以使用上下文繫結來輕鬆注入你的類需要的任何值:

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

繫結介面到實現

服務容器有一個強大的功能,就是將介面繫結到給定實現。

假設有一個 EventPusher 介面及其實現類 RedisEventPusher ,編寫完該介面的 RedisEventPusher 實現後,就可以將其註冊到服務容器:

$this->app->bind(
    'App\Contracts\EventPusher',
    'App\Services\RedisEventPusher'
);

這段程式碼告訴容器當一個類需要 EventPusher 的實現時將會注入 RedisEventPusher。

現在,我們可以在建構函式或者任何其它通過服務容器注入依賴項的地方,使用型別提示,來依賴注入 EventPusher 介面。

use App\Contracts\EventPusher;

/**
 * 建立一個新的類例項
 *
 * @param  EventPusher  $pusher
 * @return void
 */
public function __construct(EventPusher $pusher)
{
    $this->pusher = $pusher;
}

上下文繫結

有時候,你可能有兩個類使用了相同的介面,但你希望每個類都能注入不同的實現。

例如,兩個控制器可能需要依賴不同的 Illuminate\Contracts\Filesystem\Filesystem 契約 實現。 Laravel 提供了一個簡單、優雅的介面來定義這個行為:

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

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

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

標籤

有時候,你可能需要解析某個「分類」下的所有繫結。

例如,你正在構建一個報表的聚合器,它接收多個不同的 Report 介面實現。在註冊完 Report 實現之後,可以通過 tag 方法給它們分配一個標籤。

$this->app->bind('SpeedReport', function () {
    //
});

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

$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');

這些服務被打上標籤後,可以通過 tagged 方法來輕鬆解析它們:

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

擴充套件繫結

extend 方法允許對解析服務進行修改。

例如,當服務被解析後,可以執行額外程式碼裝飾或配置該服務。extend 方法接收一個閉包來返回修改後的服務:

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

解析

有很多方式可以從容器中解析出我們想要的物件。

make 方法

可以使用 make 方法將容器中的類例項解析出來。

make 方法接收要解析的類或介面的名稱:

$api = $this->app->make('HelpSpot\API');

resolve 方法

如果你的程式碼處於不能訪問 $app 變數的位置,你可以使用全域性的輔助函式 resolve 進行解析:

$api = resolve('HelpSpot\API');

makeWith 方法

如果某些類的依賴項不能通過容器去解析,那你可以通過將它們作為關聯陣列傳遞到 makeWith 方法來注入它們。

$api = $this->app->makeWith('HelpSpot\API', ['id' => 1]);

自動注入

自動注入是最常用的解析方式。

在具體實踐中,這是大多數物件從容器中解析的方式。

可以簡單地使用「型別提示」的方式,在由容器解析的類的建構函式中新增依賴項。

控制器、事件監聽器、佇列任務、中介軟體等都是通過這種方式。

<?php

namespace App\Http\Controllers;

use App\Users\Repository as UserRepository;

class UserController extends Controller
{
    /**
     * 使用者儲存庫例項。
     */
    protected $users;

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

    /**
     * 顯示指定 ID 的使用者資訊。
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        //
    }
}

容器事件

服務容器在每一次解析物件時都會觸發一個事件,可以使用 resolving 方法監聽該事件:

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

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

如你所見,被解析的物件將會傳遞給回撥函式,從而允許你在物件被傳遞給消費者之前為其設定額外屬性。

PSR-11

Laravel 的服務容器實現了 PSR-11 介面。

因此,你可以通過型別提示 PSR-11 容器介面來獲取 Laravel 容器的例項:

use Psr\Container\ContainerInterface;

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

    //
});

注:如果繫結到容器的唯一標識有衝突,呼叫 get 方法會丟擲異常。