Laravel 服務(wù)容器是管理類依賴的強(qiáng)力工具。依賴注入是比較專業(yè)的說法,真正意思是將類依賴透過構(gòu)造器或 「setter」 方法注入。
讓我們來看一個(gè)簡單的例子:
<?php namespace App\Handlers\Commands;use App\User;use App\Commands\PurchasePodcastCommand;use Illuminate\Contracts\Mail\Mailer;class PurchasePodcastHandler { /** * 一個(gè)發(fā)信功能的實(shí)現(xiàn) */ protected $mailer; /** * 創(chuàng)建一個(gè)新的實(shí)例 * * @param Mailer $mailer * @return void */ public function __construct(Mailer $mailer) { $this->mailer = $mailer; } /** * 購買一個(gè)播客節(jié)目 * * @param PurchasePodcastCommand $command * @return void */ public function handle(PurchasePodcastCommand $command) { // }}
在這個(gè)例子中,當(dāng)播客被購買時(shí), PurchasePodcast
命令處理器需要發(fā)送一封電子郵件。所以,我們將注入一個(gè)服務(wù)來提供這個(gè)能力。當(dāng)這個(gè)服務(wù)被注入以后,我們就可以輕易地切換到不同的實(shí)現(xiàn)。當(dāng)測試我們的應(yīng)用程序時(shí),我們同樣也可以輕易地「模擬」,或者創(chuàng)建一個(gè)虛擬的發(fā)信服務(wù)實(shí)現(xiàn),來幫助我們進(jìn)行測試。
如果要?jiǎng)?chuàng)建一個(gè)強(qiáng)大并且大型的應(yīng)用,或者對 Laravel 的內(nèi)核做貢獻(xiàn),首先必須對 Laravel 的服務(wù)容器進(jìn)行深入了解。
幾乎你所有服務(wù)容器將與已注冊的服務(wù)提供者綁定,這些例子都在情境(context)使用容器做說明,如果應(yīng)用程序其它地方需要容器實(shí)例,如工廠(factory),能以類型提示 Illuminate\Contracts\Container\Container
注入一個(gè)容器實(shí)例。另外,你可以使用 App
facade 訪問容器。
在一個(gè)服務(wù)提供者內(nèi)部,你總是可以通過 $this->app
實(shí)例變量來訪問到容器。
在服務(wù)提供者里,總是通過 $this->app
實(shí)例變量使用容器。
服務(wù)容器注冊依賴有幾種方式,包括閉包回調(diào)和綁定實(shí)例的接口。首先,我們來探討閉包回調(diào)的方式。被注冊至容器的閉包解析器包含一個(gè) key (通常用類名稱) 和一個(gè)有返回值的閉包:
$this->app->bind('FooBar', function($app){ return new FooBar($app['SomethingElse']);});
有時(shí)候,你可能希望綁定到容器的對象只會(huì)被解析一次,之后的調(diào)用都返回相同的實(shí)例:
$this->app->singleton('FooBar', function($app){ return new FooBar($app['SomethingElse']);});
你也可以使用 instance
方法,綁定一個(gè)已經(jīng)存在的實(shí)例到容器,接下來將總是返回該實(shí)例:
$fooBar = new FooBar(new SomethingElse);$this->app->instance('FooBar', $fooBar);
從容器解析出實(shí)例有幾種方式。
一、可以使用 make
方法:
$fooBar = $this->app->make('FooBar');
二、你可以像「訪問數(shù)組」一樣對容器進(jìn)行訪問,因?yàn)樗鼘?shí)現(xiàn)了PHP的 ArrayAccess
接口:
$fooBar = $this->app['FooBar'];
最后,也是最重要的一點(diǎn),你可以在構(gòu)造函數(shù)中簡單地「類型指定(type-hint)」你所需要的依賴,包括在控制器、事件監(jiān)聽器、隊(duì)列任務(wù),過濾器等等之中。容器將自動(dòng)注入你所需的所有依賴:
<?php namespace App\Http\Controllers;use Illuminate\Routing\Controller;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ù)容器有個(gè)非常強(qiáng)大特色,能夠綁定特定實(shí)例的接口。舉例,假設(shè)我們應(yīng)用程序要集成 Pusher 服務(wù)去收發(fā)即時(shí)事件,如果使用 Pusher 的 PHP SDK,可以在類注入一個(gè) Pusher 客戶端實(shí)例:
<?php namespace App\Handlers\Commands;use App\Commands\CreateOrder;use Pusher\Client as PusherClient;class CreateOrderHandler { /** * Pusher SDK 客戶端實(shí)例 */ protected $pusher; /** * 創(chuàng)建一個(gè)實(shí)例 * * @param PusherClient $pusher * @return void */ public function __construct(PusherClient $pusher) { $this->pusher = $pusher; } /** * 執(zhí)行命令 * * @param CreateOrder $command * @return void */ public function execute(CreateOrder $command) { // }}
在上面這個(gè)例子中,注入類的依賴到類中已經(jīng)能夠滿足需求;但同時(shí),我們也緊密耦合于 Pusher 的 SDK 。如果 Pusher 的 SDK 方法發(fā)生改變,或者我們要切換到別的事件服務(wù),那我們也需要同時(shí)修改 CreateOrderHandler
的代碼。
為了將 CreateOrderHandler
和事件推送的修改「隔離」,我們可以定義一個(gè) EventPusher
接口和一個(gè) PusherEventPusher
實(shí)現(xiàn):
<?php namespace App\Contracts;interface EventPusher { /** * Push a new event to all clients. * * @param string $event * @param array $data * @return void */ public function push($event, array $data);}
一旦 PusherEventPusher
實(shí)現(xiàn)這接口,就可以在服務(wù)容器像這樣注冊它:
$this->app->bind('App\Contracts\EventPusher', 'App\Services\PusherEventPusher');
當(dāng)有類需要 EventPusher
接口時(shí),會(huì)告訴容器應(yīng)該注入 PusherEventPusher
,現(xiàn)在就可以在構(gòu)造器中「類型指定」一個(gè) EventPusher
接口:
/** * Create a new order handler instance. * * @param EventPusher $pusher * @return void */ public function __construct(EventPusher $pusher) { $this->pusher = $pusher; }
有時(shí)候,你可能會(huì)有兩個(gè)類需要用到同一個(gè)接口,但是你希望為每個(gè)類注入不同的接口實(shí)現(xiàn)。例如當(dāng)我們的系統(tǒng)收到一個(gè)新的訂單時(shí),我們需要使用 PubNub 來代替 Pusher 發(fā)送消息。Laravel 提供了一個(gè)簡單便利的接口來定義以上的行為:
$this->app->when('App\Handlers\Commands\CreateOrderHandler') ->needs('App\Contracts\EventPusher') ->give('App\Services\PubNubEventPusher');
偶爾你可能需要解析綁定中的某個(gè)「類」。例如你正在建設(shè)一個(gè)匯總報(bào)表,它需要接收實(shí)現(xiàn)了 Report
接口的不同實(shí)現(xiàn)的數(shù)組。在注冊了 Report
的這些實(shí)現(xiàn)之后,你可以用 tag
方法來給他們賦予一個(gè)標(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'));});
Laravel 提供了幾個(gè)機(jī)會(huì)來使用服務(wù)容器以提高應(yīng)用程序的靈活性和可測試性。解析控制器是一個(gè)最主要的案例。所有的控制器都通過服務(wù)容器來進(jìn)行解析,意味著你可以在控制器的構(gòu)造函數(shù)中「類型指定」所需依賴,而且它們將被自動(dòng)注入。
<?php namespace App\Http\Controllers;use Illuminate\Routing\Controller;use App\Repositories\OrderRepository;class OrdersController extends Controller { /** * The order repository instance. */ protected $orders; /** * Create a controller instance. * * @param OrderRepository $orders * @return void */ public function __construct(OrderRepository $orders) { $this->orders = $orders; } /** * Show all of the orders. * * @return Response */ public function index() { $orders = $this->orders->all(); return view('orders', ['orders' => $orders]); }}
在這個(gè)例子中,OrderRepository
類將被自動(dòng)注入到控制器中。這意味著在進(jìn)行 單元測試 時(shí),我們可以綁定一個(gè)假的 OrderRepository
到容器中來代替我們對數(shù)據(jù)庫的真實(shí)操作,避免對真實(shí)數(shù)據(jù)庫的影響。
當(dāng)然,在上面提到過的,控制器并不是 Laravel 通過服務(wù)容器進(jìn)行解析的唯一類。你也可以在路由的閉包中、過濾器中、隊(duì)列任務(wù)中、事件監(jiān)聽器中來「類型指定」你所需要的依賴。對于在這些情境中如何使用服務(wù)容器,請參考相關(guān)文檔。
容器在解析每一個(gè)對象時(shí)就會(huì)觸發(fā)一個(gè)事件。你可以用 resolving
方法來監(jiān)聽此事件:
$this->app->resolving(function($object, $app){ // 當(dāng)容器解析任意類型的依賴時(shí)被調(diào)用});$this->app->resolving(function(FooBar $fooBar, $app){ // 當(dāng)容器解析 `FooBar` 類型的依賴時(shí)被調(diào)用});
被解析的對象將被傳入到閉包方法中。
更多建議: