# 服務容器
## 簡介
Laravel 服務容器是一個用于管理類的依賴和執行依賴注入的強大工具。依賴注入這個花哨名詞實質上是指:類的依賴通過構造函數,或者某些情況下通過 "setter" 方法 "注入" 到類中。
來看一個簡單的例子:
~~~php
<?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`需要從數據源獲取 users。因此,我們要**注入**一個能夠獲取 users 的服務。在當前上下文中,`UserController`很可能是使用[Eloquent]從數據庫中獲取 user 信息。然而,由于 repository 是被注入的,所以我們可以輕易地將其切換為另一個的實現。這種注入方式的便利之處還體現在當我們為應用編寫測試時,我們還可以輕松地 "模擬" 或創建`UserRepository`的虛擬實現。
想要構建強大的大型應用,至關重要的一件事是:要深刻地理解 Laravel 服務容器。當然,為 Laravel 的核心代碼做出貢獻也一樣。
## 綁定
### 綁定基礎
幾乎所有的服務容器綁定都是在[服務提供器], 所以文檔中大多數例子都是使用了在服務提供器中綁定的容器。
> {tip} 如果類沒有依賴任何接口,就沒有必要將類綁定到容器中。容器不需要指定如何構建這些對象,因為它可以使用反射自動解析這些對象。
#### 簡單綁定
在服務提供器中,你總是可以通過`$this->app`屬性訪問容器。我們可以通過容器的`bind`方法注冊綁定,`bind`方法的第一個參數為要綁定的類/接口名,第二個參數是一個返回類實例的`Closure`:
~~~php
$this->app->bind('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});
~~~
注意,我們接受容器本身作為解析器的參數。然后,我們可以使用容器來解析正在構建的對象的子依賴。
#### 綁定一個單例
`singleton`方法將類或接口綁定到只解析一次的容器中。一旦單例綁定被解析,相同的對象實例會在隨后的調用中返回到容器中:
~~~php
$this->app->singleton('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});
~~~
#### 綁定實例
你也可以使用`instance`方法將現有對象實例綁定到容器中。給定的實例會始終在隨后的調用中返回到容器中:
~~~php
$api = new HelpSpot\API(new HttpClient);
$this->app->instance('HelpSpot\API', $api);
~~~
#### 綁定基本值
當你有一個類不僅需要接受一個注入類,還需要注入一個基本值(比如整數)。你可以使用上下文綁定來輕松注入你的類需要的任何值:
~~~php
$this->app->when('App\Http\Controllers\UserController')
->needs('$variableName')
->give($value);
~~~
### 綁定接口到實現
服務容器有一個很強大的功能,就是支持綁定接口到給定的實現。例如,如果我們有個`EventPusher`接口 和一個`RedisEventPusher`實現。一旦我們寫完了`EventPusher`接口的`RedisEventPusher`實現,我們就可以在服務容器中注冊它,像這樣:
~~~php
$this->app->bind(
'App\Contracts\EventPusher',
'App\Services\RedisEventPusher'
);
~~~
這么做相當于告訴容器:當一個類需要實現`EventPusher`時,應該注入`RedisEventPusher`。現在我們就可以在構造函數或者任何其他通過服務容器注入依賴項的地方使用類型提示注入`EventPusher`接口:
~~~php
use App\Contracts\EventPusher;
/**
* 創建新的類實例
*
* @param EventPusher $pusher
* @return void
*/
public function __construct(EventPusher $pusher)
{
$this->pusher = $pusher;
}
~~~
### 上下文綁定
有時你可能有兩個類使用了相同的接口,但你希望各自注入不同的實現。例如,有兩個控制器可能依賴了不同的`Illuminate\Contracts\Filesystem\Filesystem`[契約]實現。Laravel 提供了一個簡單的,優雅的接口來定義這個行為:
~~~php
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`方法為它們分配標簽:
~~~php
$this->app->bind('SpeedReport', function () {
//
});
$this->app->bind('MemoryReport', function () {
//
});
$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');
~~~
一旦服務被標記,你就可以使用`tagged`方法輕松將它們全部解析:
~~~php
$this->app->bind('ReportAggregator', function ($app) {
return new ReportAggregator($app->tagged('reports'));
});
~~~
### 擴展綁定
`extend`方法可以修改已解析的服務。例如,當一個服務被解析后,你可以添加額外的代碼去修飾或配置這個服務。`extend`方法接受一個閉包,該閉包唯一參數就是這個服務,并返回修改過的服務:
~~~php
$this->app->extend(Service::class, function($service) {
return new DecoratedService($service);
});
~~~
## 解析實例
#### `make`方法
你可以使用`make`方法從容器中解析出類實例。`make`方法接受一個你想要解析的類名或接口名:
~~~php
$api = $this->app->make('HelpSpot\API');
~~~
如果你的代碼處于無法訪問`$app`變量的位置,則可用全局輔助函數`resolve`來解析:
~~~php
$api = resolve('HelpSpot\API');
~~~
如果你的類依賴不能通過容器來解析,你可以通過將它們作為關聯數組傳遞到`makeWith`方法來注入它們:
~~~php
$api = $this->app->makeWith('HelpSpot\API', ['id' => 1]);
~~~
#### 自動注入
另外,并且更重要的是,你可以簡單地使用"類型提示"的方式在類的構造函數中注入那些需要容器解析的依賴項,包括[控制器],[事件監聽器],[隊列任務],[中間件]等。實際上,這才是大多數對象應該被容器解析的方式。
比如,你可以在控制器的構造函數中添加一個 repository 的類型提示,然后這個 repository 將會被自動解析并注入類中:
~~~php
<?php
namespace App\Http\Controllers;
use App\Users\Repository as UserRepository;
class UserController extends Controller
{
/**
* user repository 實例
*/
protected $users;
/**
* 創建一個新的控制器實例
*
* @param UserRepository $users
* @return void
*/
public function __construct(UserRepository $users)
{
$this->users = $users;
}
/**
* 顯示給定ID的user
*
* @param int $id
* @return Response
*/
public function show($id)
{
//
}
}
~~~
## 容器事件
服務容器每次解析對象時會觸發一個事件,你可以使用`resolving`方法監聽這個事件:
~~~php
$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"...
});
~~~
如你所見,被解析的對象將被傳入回調函數,這使得你能夠在對象被傳給調用者之前給它設置額外的屬性。
## PSR-11
Laravel 的服務容器實現了[PSR-11]接口。 因此,你可以使用 PSR-11容器『接口類型提示』來獲取 Laravel 容器的實例:
~~~php
use Psr\Container\ContainerInterface;
Route::get('/', function (ContainerInterface $container) {
$service = $container->get('Service');
//
});
~~~
> {note} 如果標識符還沒有被顯式綁定到容器,那么調用`get`方法將會拋出異常。