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 方法會丟擲異常。