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