Service Container 是 Laravel 框架中相當重點的一個功能,主要是用來節省撰寫程式碼時手動建立類別實例的步驟。
說明 Service Container 的功能前先來看一個例子。
/app/Http/Controllers/TodoController.php
public function store(Request $request)
{
$data = $request->all();
$user = Auth::user();
$user->todos()->create([
'name' => $data['name']
]);
}
這是我們在 Controller 中建立的 store 方法,負責收到請求時建立資料,他有一個參數 $request ,從用法上來看 $request 明顯是某個 class 的實例,問題是這個實例是什麼時候建立的?
在 Laravel 的 Service Container 中,或者說 Laravel App 當中,當我們將函式的參數型別定義為某個 class 或是 interface 時,Laravel App 會自動解析該類別的建立方法( __construct ) 並嘗試建立實例傳入函式當中,讓我們可以省略掉宣告類別實例的步驟。
舉例來說如果沒有 Service Container 的話上面的程式碼就變成
public function store()
{
$request = new Request(...requirements);
$data = $request->all();
//...
}
我們必須先建好創建 Request 實例所需的參數,才能建成 Request 實例,如果這接參數又有各自依賴的參數,就必須一一建立後才能開始使用 Request 的功能。
如果做一次還好,但每個 Controller 底下的方法都要建一次 Request 就吃不消了。
而 Service Container 的好處就在於當我們宣告要使用這個類別時,會解析類別後嘗試自動去建立該類別的實例,如果類別不須需要參數就直接建立。如果需要參數,就繼續嘗試建立參數的實例,一層層自動建立直到最後。而最後的結果就是已經完全準備好供我們直接使用的實例。
當然偶而還是有些類別無法簡單的建立實例,需要特別宣告參數,這時候就得在 Service Container 中特別定義該類別的建立方法。
在 Service Container 中當遇到無法簡單建立實例的類別時,就需要額外定義 Service Provider 指導該如何建立該類別的實例。
Service Provider可以藉由指令建立,預設放在 app/Providers 目錄下。
php artisan make:provider RiakServiceProvider
借用官方文件的範例說明。
<?php
namespace App\Providers;
use App\Services\Riak\Connection;
use Illuminate\Support\ServiceProvider;
class RiakServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->singleton(Connection::class, function ($app) {
return new Connection(config('riak'));
});
}
}
ServiceProvider 會有一個 register 函式,也就是在 Service Container 中註冊該類別的實例建立方式的方法。
register 中呼叫的 $this->app 指的就是 Laravel App 本身的實例,並且用 singleton 宣告註冊的方式。
像這邊就是註冊 Connection::class 的建立方法,當有參數宣告為 Connection 類別時,就提供 new Connection(config('riak')) 所產生的實例作為該參數的值。
註冊的方式還不只一種,底下分別介紹。
bind 是基礎的註冊方法,這種方式每次建立的實例都是全新的,彼此不同。
use App\Services\Transistor;
use App\Services\PodcastParser;
$this->app->bind(Transistor::class, function ($app) {
return new Transistor($app->make(PodcastParser::class));
});
singleton 相對於 bind ,當 singleton 的實例第一次被建立後,之後就不會再重新建立,而是重複提供第一次建立的實例,所以如果實例被編輯過的話下次呼叫時會保留編輯的內容。
use App\Services\Transistor;
use App\Services\PodcastParser;
$this->app->singleton(Transistor::class, function ($app) {
return new Transistor($app->make(PodcastParser::class));
});
scoped 相似於 singleton ,建立之後就保持相同的實例,不過 scoped 保持的範圍只有單次的 request 或是 job 的處理週期中,當開始下一次的 request 處理時就重新建立實例。
instance 也很像 singleton ,只差別在 singleton 會等到第一次被呼叫後才固定實例, instance 則是直接指派一個已經建好的實例。
use App\Services\Transistor;
use App\Services\PodcastParser;
$service = new Transistor(new PodcastParser);
$this->app->instance(Transistor::class, $service);
不只能在類別被呼叫的時候指派實例,也能在介面被呼叫時指派特定的實作來產生實例
EventPusher 是介面, RedisEventPusher 則是 EventPusher 的實作類別。
use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;
$this->app->bind(EventPusher::class, RedisEventPusher::class);
這樣當呼叫 EventPusher 作為參數時,正常來說他是介面是不能產生實例的,但因為我們在 Service Container 中指派了對應的實作類別 RedisEventPusher, Laravel 就能直接用 RedisEventPusher 來產生實例。
use App\Contracts\EventPusher;
public function __construct(EventPusher $pusher)
{
$this->pusher = $pusher;
}
Service Provider 建立好之後記得要加入 config/app.php 中的 'providers' 陣列底下,這樣 App 被啟動時 Service 才會被註冊。
除了一手一手建立 Service Provider 外,如果要綁定的類別不是需要參數才能建立的類型,也可以用 $bindings 或 $singletons 批量註冊。
<?php
namespace App\Providers;
use App\Contracts\DowntimeNotifier;
use App\Contracts\ServerProvider;
use App\Services\DigitalOceanServerProvider;
use App\Services\PingdomDowntimeNotifier;
use App\Services\ServerToolsProvider;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* All of the container bindings that should be registered.
*
* @var array
*/
public $bindings = [
ServerProvider::class => DigitalOceanServerProvider::class,
];
/**
* All of the container singletons that should be registered.
*
* @var array
*/
public $singletons = [
DowntimeNotifier::class => PingdomDowntimeNotifier::class,
ServerProvider::class => ServerToolsProvider::class,
];
}
除了在函式的參數宣告時借用 Service Container 的力量建立實例,也可以直接用 App 的 make() 方法建立。
public function store()
{
$request = $this->app->make(Request::class);
$data = $request->all();
//...
}
如果建立類別的部分參數無法簡單的被 Service Container 解析,也可用 makeWith 手動傳入
use App\Services\Transistor;
$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);