簡介
服務容器就是一個普通的容器,用來封裝類的實例,然后在需要的時候再取出來。用更專業的術語來說是服務容器實現了控制反轉(Inversion of Control,縮寫為IoC),
意思是正常情況下類A需要一個類B的時候,我們需要自己去new類B,意味著我們必須知道類B的更多細節,比如構造函數,隨著項目的復雜性增大,這種依賴是毀滅性的。控制反轉的意思就是,將類A主動獲取類B的過程顛倒過來變成被動,類A只需要聲明它需要什么,然后由容器提供。
樣做的好處是,類A不依賴于類B的實現,這樣在一定程度上解決了耦合問題。
在Laravel的服務容器中,為了實現控制反轉,可以有以下兩種:
- 依賴注入(Dependency Injection)。
- 綁定。
依賴注入
依賴注入是一種類型提示,舉官網的例子
<?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;
}
/**
* Show the profile for the given user.
*
* @param int $id
* @return Response
*/
public function show($id)
{
$user = $this->users->find($id);
return view('user.profile', ['user' => $user]);
}
}
在本例中,UserController 需要從數據源獲取用戶,所以,我們注入了一個可以獲取用戶的服務 UserRepository,其扮演的角色類似使用 Eloquent 從數據庫獲取用戶信息。
這里UserController
需要一個UserRepository
實例,我們只需在構造方法中聲明我們需要的類型,容器在實例化UserController
時會自動生成UserRepository
的實例(或者實現類,因為UserRepository
可以為接口),而不用主動去獲取UserRepository
的實例,這樣也就避免了了解UserRepository
的更多細節,也不用解決UserRepository
所產生的依賴,我們所做的僅僅是聲明我們所需要的類型,所有的依賴問題都交給容器去解決。
綁定
幾乎所有的服務容器綁定都是在服務提供者中完成。
注:如果一個類沒有基于任何接口那么就沒有必要將其綁定到容器。容器并不需要被告知如何構建對象,因為它會使用 PHP 的反射服務自動解析出具體的對象。
綁定操作一般在 服務提供者 ServiceProviders
中的register
方法中,最基本的綁定是容器的bind
方法,它接受一個類的別名或者全名和一個閉包來獲取實例:
簡單的綁定
在一個服務提供者中,可以通過 $this->app 變量訪問容器,然后使用 bind 方法注冊一個綁定,該方法需要兩個參數,第一個參數是我們想要注冊的類名或接口名稱,第二個參數是返回類的實例的閉包:
$this->app->bind('blogConfig', function ($app) {
return new MapRepository();
});
$this->app->bind('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});
綁定一個單例
singleton
方法和bind
寫法沒什么區別,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);
上文中提到的request實例就是通過這種方法綁定到容器的:
$this->app->instance('request', $request);
綁定原始值
你可能有一個接收注入類的類,同時需要注入一個原生的數值比如整型,可以結合上下文輕松注入這個類需要的任何值:
$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\VideoController;
use App\Http\Controllers\PhotoControllers;
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'));
});
解析
也就是獲取綁定的實例:
獲取方法
1. app('HelpSpot\API');
2. app()->make('HelpSpot\API');
3. app()['HelpSpot\API'];
4. resolve('HelpSpot\API');
以上四種方法均會返回獲得HelpSpot\API
的實例。
區別是在一次請求的生命周期中:
bind方法的閉包會在每一次調用以上四種方法時執行,
singleton方法的閉包只會執行一次。
在使用中,如果每一個類要獲的不同的實例,或者需要“個性化”的實例時,這時我們需要用bind方法以免這次的使用對下次的使用造成影響;
如果實例化一個類比較耗時或者類的方法不依賴該生成的上下文,那么我們可以使用singleton方法綁定。singleton方法綁定的好處就是,如果在一次請求中我們多次使用某個類,那么只生成該類的一個實例將節省時間和空間。
自動注入
最后,也是最常用的,你可以簡單的通過在類的構造函數中對依賴進行類型提示來從容器中解析對象,控制器、事件監聽器、隊列任務、中間件等都是通過這種方式。在實踐中,這是大多數對象從容器中解析的方式。
容器會自動為其解析類注入依賴,例如,你可以在控制器的構造函數中為應用定義的倉庫進行類型提示,該倉庫會自動解析并注入該類:
<?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) {
// Called when container resolves object of any type...
});
$this->app->resolving(HelpSpot\API::class, function ($api, $app) {
// Called when container resolves objects of type "HelpSpot\API"...
});
正如你所看到的,被解析的對象將會傳遞給回調函數,從而允許你在對象被傳遞給消費者之前為其設置額外屬性。