サービスコンテナ
イントロダクション
Laravelのサービスコンテナは、クラス間の依存を管理する強力な管理ツールです。依存注入というおかしな言葉は主に「コンストラクターか、ある場合にはセッターメソッドを利用し、あるクラスをそれらに依存しているクラスへ外部から注入する」という意味で使われます。
シンプルな例を見てみましょう。
<?php
namespace App\Http\Controllers;
use App\User;
use App\Repositories\UserRepository;
use App\Http\Controllers\Controller;
class UserController extends Controller
{
/**
* ユーザリポジトリの実装
*
* @var UserRepository
*/
protected $users;
/**
* 新しいコントローラインスタンスの生成
*
* @param UserRepository $users
* @return void
*/
public function __construct(UserRepository $users)
{
$this->users = $users;
}
/**
* 指定ユーザのプロフィール表示
*
* @param int $id
* @return Response
*/
public function show($id)
{
$user = $this->users->find($id);
return view('user.profile', ['user' => $user]);
}
}
この例中で、UserController
はデータソースからユーザを取得する必要があります。そのため、ユーザを取得できるサービスを注入しています。このようなコンテキストでは、データベースからユーザ情報を取得するために、ほとんどの場合でEloquentが使われるでしょう。しかしながら、リポジトリは外部から注入されているため、他の実装へ簡単に交換できます。さらに、「モック」することも簡単ですし、アプリケーションのテストでUserRepository
のダミー実装を作成することもできます。
Laravelのサービスコンテナを深く理解することは、パワフルで大きなアプリケーションを構築するのと同時に、Laravelコア自身に貢献するために重要なことです。
結合
結合 Basics
ほとんどのサービスコンテナの結合は、サービスプロバイダの中で行われています。そのため、ほとんどの例はコンテナ中で使用される状況をデモンストレートしています。
Tip!! インターフェイスに依存していないのであれば、コンテナでクラスを結合する必要はありません。コンテナにオブジェクトを結合する方法を指示する必要はなく、リフレクションを使用してそのオブジェクトを自動的に依存解決します。
シンプルな結合
サービスプロバイダの中からは、いつでも$this->app
プロパティにより、コンテナへアクセスできます。bind
メソッドへ登録したいクラス名かインターフェイス名と、クラスのインスタンスを返す「クロージャ」を引数として渡せば、結合を登録できます。
$this->app->bind('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});
コンテナ自身をリゾルバ―の引数として受け取っていることに注目してください。これで構築しているオブジェクトの依存を解決するためにも、コンテナを利用できます。
結合 A Singleton
singleton
メソッドは、クラスやインターフェイスが一度だけ依存解決されるようにコンテナに登録します。一度シングルトン結合が解決されると、以降にコンテナへこの結合が呼び出されるたび、同じオブジェクトインスタンスが返されます。
$this->app->singleton('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});
結合 Instances
既に存在するオブジェクトのインスタンスをinstance
メソッドを用いて、コンテナに結合することもできます。指定されたインスタンスが、以降のコンテナで呼び出されるたびに返されます。
$api = new HelpSpot\API(new HttpClient);
$this->app->instance('HelpSpot\Api', $api);
結合 Primitives
クラスにより依存注入されたインスタンスを受け取ることもあるでしょうし、さらに整数のようなプリミティブな値を挿入する必要も起きるでしょう。必要なクラスのコンテキストによる結合を使用すれば簡単です。
$this->app->when('App\Http\Controllers\UserController')
->needs('$variableName')
->give($value);
結合 Interfaces To Implementations
サービスコンテナーのとても強力な機能は、インターフェイスを指定された実装に結合できることです。たとえばEventPusher
インターフェイスとRedisEventPusher
実装があるとしましょう。このインターフェイスのRedisEventPusher
を実装し終えたら、サービスコンテナに登録できます。
$this->app->bind(
'App\Contracts\EventPusher',
'App\Services\RedisEventPusher'
);
これはつまり、EventPusher
の実装クラスが必要な時、コンテナはRedisEventPusher
を注入するということです。ではコンストラクタか、もしくはサービスコンテナが依存を注入できる場所で、EventPusher
インターフェイスをタイプヒントしてみましょう。
use App\Contracts\EventPusher;
/**
* 新しいインスタンスの生成
*
* @param EventPusher $pusher
* @return void
*/
public function __construct(EventPusher $pusher)
{
$this->pusher = $pusher;
}
コンテキストによる結合
時には、同じインターフェイスを使用した2つのクラスがあり、クラスごとに異なった実装を注入しなくてはならない場合もあるでしょう。たとえば、2つのコントローラが異なったIlluminate\Contracts\Filesystem\Filesystem
契約の実装に依存している場合です。Laravelでは、このような場合の定義をシンプルで、読み書きしやすくなっています。
use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\PhotoController;
use App\Http\Controllers\VideoController;
use Illuminate\Contracts\Filesystem\Filesystem;
$this->app->when(PhotoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('local');
});
$this->app->when(VideoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('s3');
});
タグ付け
ある明確な「カテゴリー」の結合を全部解決する必要がある場合も存在すると思います。例えば、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'));
});
依存解決
make
メソッド
コンテナによりクラスインスタンスを依存解決する場合は、make
メソッドを使います。make
メソッドには、依存解決したいクラスかインターフェイスの名前を引数として渡します。
$api = $this->app->make('HelpSpot\API');
もし、$app
変数へアクセスできない場所で依存解決したい場合は、グローバルなresolve
ヘルパが使えます。
$api = resolve('HelpSpot\API');
自動注入
一番重要な依存解決方法は、コンテナによる依存解決が行われるクラスのコンストラクタで、シンプルに依存を「タイプヒント」で記述するやりかたです。これはコントローラ、イベントリスナ、キュージョブ、ミドルウェアなどで利用できます。実践でコンテナによりオブジェクトの解決が行われるのは、これが一番多くなります。
たとえば、コントローラのコンストラクタで、アプリケーションにより定義されたリポジトリをタイプ品としてみましょう。このリポジトリは、自動的に依存解決され、クラスへ注入されます。
<?php
namespace App\Http\Controllers;
use App\Users\Repository as UserRepository;
class UserController extends Controller
{
/**
* Userリポジトリインスタンス
*/
protected $users;
/**
* 新しいコントローラインスタンスの生成
*
* @param UserRepository $users
* @return void
*/
public function __construct(UserRepository $users)
{
$this->users = $users;
}
/**
* 指定されたIDのユーザ表示
*
* @param int $id
* @return Response
*/
public function show($id)
{
//
}
}
コンテナイベント
コンテナはオブジェクトの依存解決時に毎回イベントを発行します。このイベントは、resolving
を使用して購読できます。
$this->app->resolving(function ($object, $app) {
// どんなタイプのオブジェクトをコンテナが解決した場合でも呼び出される
});
$this->app->resolving(HelpSpot\API::class, function ($api, $app) {
// "HelpSpot\API"クラスのオブジェクトをコンテナが解決した場合に呼び出される
});
ご覧の通り、依存解決対象のオブジェクトがコールバックに渡され、最終的に取得元へ渡される前に追加でオブジェクトのプロパティをセットすることができます。