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