服務容器
介紹
Laravel 服務容器是管理類別依賴的強力工具。依賴注入是個異想天開的詞,真正意思是類別依賴透過建構子或 “setter” 方法注入。
來看個簡單範例:
<?php namespace App\Handlers\Commands;
use App\User;
use App\Commands\PurchasePodcastCommand;
use Illuminate\Contracts\Mail\Mailer;
class PurchasePodcastHandler {
/**
* The mailer implementation.
*/
protected $mailer;
/**
* Create a new instance.
*
* @param Mailer $mailer
* @return void
*/
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
/**
* Purchase a podcast.
*
* @param PurchasePodcastCommand $command
* @return void
*/
public function handle(PurchasePodcastCommand $command)
{
//
}
}
在這範例裡, 當播客被購買時, PurchasePodcast
命令處理器需要寄封 e-mails,因此,我們將 注入 能寄送 e-mails 的服務,由於服務被注入,我們能容易地切換成其它實例,當測試應用程式時,一樣能輕易地 “mock” 或建立假的發信者(mailer)實例。
在建置強大應用程式,以及為 Laravel 核心貢獻,須深入理解 Laravel 服務容器。
基本用法
綁定
幾乎你所有服務容器將與已註冊的服務提供者綁定,這些例子都在情境(context)使用容器做說明,如果應用程式其它地方需要容器實例,像是工廠(factory),能以型別提示 Illuminate\Contracts\Container\Container
注入一個容器實例。另外,你可以使用 App
facade 存取容器。
註冊基本解析器
在服務提供者裡,總是透過 $this->app
實例變數使用容器。
服務容器注冊依賴有幾種方式,包括閉包函式和綁定實例的介面。首先,探討閉包函式,具有鍵值(通常是類別名稱)和返值閉包的閉包解析器,被註冊至容器:
$this->app->bind('FooBar', function($app)
{
return new FooBar($app['SomethingElse']);
});
註冊共享
有時候,你可能希望綁定到容器的型別只會被解析一次,之後的呼叫都返回相同的實例:
$this->app->singleton('FooBar', function($app)
{
return new FooBar($app['SomethingElse']);
});
綁定已存在的實例到容器
你也可以使用 instance
方法,綁定一個已經存在的實例到容器,將總是返回特定的實例:
$fooBar = new FooBar(new SomethingElse);
$this->app->instance('FooBar', $fooBar);
解析
從容器解析有幾種方式。一、可以使用 make
方法:
$fooBar = $this->app->make('FooBar');
二、可以對實作 PHP ArrayAccess
介面的容器,使用 “陣列存取”:
$fooBar = $this->app['FooBar'];
最後,重點是你可以簡單地在類別建構子注入”型別提示”依賴,包含控制器、事件監聽者、工作隊列、篩選器等,容器將會自動注入依賴:
<?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 服務去收發即時事件,如果使用 Pusher 的 PHP SDK,可以在類別注入一個 Pusher 客戶端實例:
<?php namespace App\Handlers\Commands;
use App\Commands\CreateOrder;
use Pusher\Client as PusherClient;
class CreateOrderHandler {
/**
* The Pusher SDK client instance.
*/
protected $pusher;
/**
* Create a new order handler instance.
*
* @param PusherClient $pusher
* @return void
*/
public function __construct(PusherClient $pusher)
{
$this->pusher = $pusher;
}
/**
* Execute the given command.
*
* @param CreateOrder $command
* @return void
*/
public function execute(CreateOrder $command)
{
//
}
}
在這範例中,注入類別依賴是件好事,不過,與 Pusher SDK 產生緊密耦合,如果 Pusher SDK 方法異動,或是決定徹底改變成新的事件服務時,需要改寫 CreateOrderHandler
程式碼。
設計成介面
為了”隔離” CreateOrderHander
倚靠於事件推送變化,可以定義 EventPusher
介面和 PusherEventPusher
實例:
<?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
實作這介面,就可以在服務容器像這樣註冊它:
$this->app->bind('App\Contracts\EventPusher', 'App\Services\PusherEventPusher');
當有類別需要 EventPusher
實作時,會告訴容器應該注入 PusherEventPusher
,現在可以在建構子做型別提示 EventPusher
介面:
/**
* Create a new order handler instance.
*
* @param EventPusher $pusher
* @return void
*/
public function __construct(EventPusher $pusher)
{
$this->pusher = $pusher;
}
情境綁定
有時候,你可能有兩個類別使用到相同介面,但你希望每個類別能注入不同實例,例如當系統收到新訂單時,想透過 PubNub 來發送事件,而不是 Pusher。Laravel 提供一個簡單又流利介面來定義這行為:
$this->app->when('App\Handlers\Commands\CreateOrderHandler')
->needs('App\Contracts\EventPusher')
->give('App\Services\PubNubEventPusher');
標籤
你偶爾可能需要解析某一個分類下的所有綁定,例如你正在建置一個能接收各種 Report
介面實例之陣列的報表聚合器(report aggregator),註冊完 Report
實例後,可以使用 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 提供幾個使用服務容器,提高應用程式彈性和可測試性的機會,主要例子是解析控制器時,所有控制器都是透過服務容器解析,意思是你可在控制器建構子做型別提示依賴,它們將會自動注入。
<?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]);
}
}
在這範例中,OrderRespository
類別會自動被注入至控制器,這意味著,在單元測試時,”mock” OrderRepository
可以綁定至容器,給予資料庫層互動無痛的 stub 。
其他容器使用範例
當然,如上面所述,控制器不是唯一透過服務容器 Laravel 類別解析,你也可以在路由閉包、篩選器、工作隊列、事件聆聽等,做型別提示依賴,對於在這些情境使用服務容器的例子,請參考相關文件。
容器事件
註冊解析事件的監聽
每當容器解析一個物件時就會觸發事件,你可以使用 resolving
方法監聽這個事件:
$this->app->resolving(function($object, $app)
{
// Called when container resolves object of any type...
});
$this->app->resolving(function(FooBar $fooBar, $app)
{
// Called when container resolves objects of type "FooBar"...
});
被解析的物件會被傳到閉合函式。