鍍金池/ 教程/ PHP/ 服務(wù)容器
Laravel Cashier
Eloquent ORM
HTTP 響應(yīng)
發(fā)行說(shuō)明
擴(kuò)展包開(kāi)發(fā)
HTTP 控制器
事件
擴(kuò)展框架
Contracts
開(kāi)發(fā)
配置
表單驗(yàn)證
錯(cuò)誤與日志
Hashing
貢獻(xiàn)指南
郵件
Session
遷移與數(shù)據(jù)填充
查詢構(gòu)造器
Redis
升級(jí)向?qū)?/span>
概覽
緩存
服務(wù)提供者
Envoy 任務(wù)執(zhí)行器
隊(duì)列
單元測(cè)試
服務(wù)容器
文件系統(tǒng) / 云存儲(chǔ)
認(rèn)證
請(qǐng)求的生命周期
加密
模板
視圖 (View)
Laravel Homestead
Laravel 安裝指南
介紹
Command Bus
分頁(yè)
輔助方法
應(yīng)用程序結(jié)構(gòu)
HTTP 路由
HTTP 請(qǐng)求
基本用法
本地化
HTTP 中間件
結(jié)構(gòu)生成器
Facades
Laravel Elixir

服務(wù)容器

介紹

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)容器。

注冊(cè)基本解析器

在一個(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']);
});

注冊(cè)一個(gè)單例

有時(shí)候,你可能希望綁定到容器的對(duì)象只會(huì)被解析一次,之后的調(diào)用都返回相同的實(shí)例:

$this->app->singleton('FooBar', function($app)
{
    return new FooBar($app['SomethingElse']);
});

綁定一個(gè)已經(jīng)存在的實(shí)例

你也可以使用 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)聽(tīng)器、隊(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)
    {
        //
    }

}

將接口綁定到實(shí)現(xiàn)

注入具體依賴

服務(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');

標(biāo)簽

偶爾你可能需要解析綁定中的某個(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'));
});

實(shí)際應(yīng)用

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ù)的影響。

使用容器的其他幾個(gè)例子

當(dāng)然,在上面提到過(guò)的,控制器并不是 Laravel 通過(guò)服務(wù)容器進(jìn)行解析的唯一類。你也可以在路由的閉包中、過(guò)濾器中、隊(duì)列任務(wù)中、事件監(jiān)聽(tīng)器中來(lái)「類型指定」你所需要的依賴。對(duì)于在這些情境中如何使用服務(wù)容器,請(qǐng)參考相關(guān)文檔。

容器事件

注冊(cè)一個(gè)解析事件監(jiān)聽(tīng)器

容器在解析每一個(gè)對(duì)象時(shí)就會(huì)觸發(fā)一個(gè)事件。你可以用 resolving 方法來(lái)監(jiān)聽(tīng)此事件:

$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ì)象將被傳入到閉包方法中。