Laravel 服務(wù)容器是管理類依賴的強(qiáng)力工具。依賴注入是比較專業(yè)的說(shuō)法,真正意思是將類依賴透過(guò)構(gòu)造器或 「setter」 方法注入。
讓我們來(lái)看一個(gè)簡(jiǎn)單的例子:
<?php namespace App\Handlers\Commands;
use App\User;
use App\Commands\PurchasePodcast;
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òu)買一個(gè)播客節(jié)目
*
* @param PurchasePodcastCommand $command
* @return void
*/
public function handle(PurchasePodcastCommand $command)
{
//
}
}
在這個(gè)例子中,當(dāng)播客被購(gòu)買時(shí), PurchasePodcast
命令處理器需要發(fā)送一封電子郵件。所以,我們將注入一個(gè)服務(wù)來(lái)提供這個(gè)能力。當(dāng)這個(gè)服務(wù)被注入以后,我們就可以輕易地切換到不同的實(shí)現(xiàn)。當(dāng)測(cè)試我們的應(yīng)用程序時(shí),我們同樣也可以輕易地「模擬」,或者創(chuàng)建一個(gè)虛擬的發(fā)信服務(wù)實(shí)現(xiàn),來(lái)幫助我們進(jìn)行測(cè)試。
如果要?jiǎng)?chuàng)建一個(gè)強(qiáng)大并且大型的應(yīng)用,或者對(duì) Laravel 的內(nèi)核做貢獻(xiàn),首先必須對(duì) Laravel 的服務(wù)容器進(jìn)行深入了解。
幾乎你所有服務(wù)容器將與已注冊(cè)的服務(wù)提供者綁定,這些例子都在情境(context)使用容器做說(shuō)明,如果應(yīng)用程序其它地方需要容器實(shí)例,如工廠(factory),能以類型提示 Illuminate\Contracts\Container\Container 注入一個(gè)容器實(shí)例。另外,你可以使用 App facade 訪問(wèn)容器。
在一個(gè)服務(wù)提供者內(nèi)部,你總是可以通過(guò) $this->app
實(shí)例變量來(lái)訪問(wèn)到容器。
在服務(wù)提供者里,總是通過(guò) $this->app
實(shí)例變量使用容器。
服務(wù)容器注冊(cè)依賴有幾種方式,包括閉包回調(diào)和綁定實(shí)例的接口。首先,我們來(lái)探討閉包回調(diào)的方式。被注冊(cè)至容器的閉包解析器包含一個(gè) key (通常用類名稱) 和一個(gè)有返回值的閉包:
$this->app->bind('FooBar', function($app)
{
return new FooBar($app['SomethingElse']);
});
有時(shí)候,你可能希望綁定到容器的對(duì)象只會(huì)被解析一次,之后的調(diào)用都返回相同的實(shí)例:
$this->app->singleton('FooBar', function($app)
{
return new FooBar($app['SomethingElse']);
});
你也可以使用 instance
方法,綁定一個(gè)已經(jīng)存在的實(shí)例到容器,接下來(lái)將總是返回該實(shí)例:
$fooBar = new FooBar(new SomethingElse);
$this->app->instance('FooBar', $fooBar);
從容器解析出實(shí)例有幾種方式。
一、可以使用 make
方法:
$fooBar = $this->app->make('FooBar');
二、你可以像「訪問(wèn)數(shù)組」一樣對(duì)容器進(jìn)行訪問(wèn),因?yàn)樗鼘?shí)現(xiàn)了PHP的 ArrayAccess
接口:
$fooBar = $this->app['FooBar'];
最后,也是最重要的一點(diǎn),你可以在構(gòu)造函數(shù)中簡(jiǎn)單地「類型指定(type-hint)」你所需要的依賴,包括在控制器、事件監(jiān)聽器、隊(duì)列任務(wù),過(guò)濾器等等之中。容器將自動(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ù)容器像這樣注冊(cè)它:
$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 來(lái)代替 Pusher 發(fā)送消息。Laravel 提供了一個(gè)簡(jiǎn)單便利的接口來(lái)定義以上的行為:
$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ù)組。在注冊(cè)了 Report
的這些實(shí)現(xiàn)之后,你可以用 tag
方法來(lái)給他們賦予一個(gè)標(biāo)簽:
$this->app->bind('SpeedReport', function()
{
//
});
$this->app->bind('MemoryReport', function()
{
//
});
$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');
一旦服務(wù)打上標(biāo)簽,可以通過(guò) tagged
方法輕易地解析它們:
$this->app->bind('ReportAggregator', function($app)
{
return new ReportAggregator($app->tagged('reports'));
});
Laravel 提供了幾個(gè)機(jī)會(huì)來(lái)使用服務(wù)容器以提高應(yīng)用程序的靈活性和可測(cè)試性。解析控制器是一個(gè)最主要的案例。所有的控制器都通過(guò)服務(wù)容器來(lái)進(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)行 單元測(cè)試 時(shí),我們可以綁定一個(gè)假的 OrderRepository
到容器中來(lái)代替我們對(duì)數(shù)據(jù)庫(kù)的真實(shí)操作,避免對(duì)真實(shí)數(shù)據(jù)庫(kù)的影響。
當(dāng)然,在上面提到過(guò)的,控制器并不是 Laravel 通過(guò)服務(wù)容器進(jìn)行解析的唯一類。你也可以在路由的閉包中、過(guò)濾器中、隊(duì)列任務(wù)中、事件監(jiān)聽器中來(lái)「類型指定」你所需要的依賴。對(duì)于在這些情境中如何使用服務(wù)容器,請(qǐng)參考相關(guān)文檔。
容器在解析每一個(gè)對(duì)象時(shí)就會(huì)觸發(fā)一個(gè)事件。你可以用 resolving
方法來(lái)監(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)用
});
被解析的對(duì)象將被傳入到閉包方法中。