【Laravel】 サービスコンテナを使って自動で依存性を解決しよう
サービスコンテナは、依存性の注入を行うためのツールです。コントローラやサービスが依存するオブジェクトを検出して、適切なインスタンスを外部から注入できます。つまり依存性の注入を行います。
目次
サービスコンテナの例
この説明文だけではおそらくさっぱりわからないので、例を見てみましょう。
<?php namespace App\Http\Controllers; use App\Repositories\NoteRepository; class HelloController extends Controller { /** * NoteRepositoryの実装 * * @var NoteRepository */ private $notes; public function __construct(NoteRepository $notes) { $this->notes = $notes; } public function index() { $note = $this->notes->getNote(1); return view('index', compact('note')); } }
これを見ると、HelloController
内ではNoteRepository
をnew
していません。コンストラクタで受け取っていますが、どうやっているのでしょうか。Laravelはタイプヒントを見て自動的に必要なクラスをnew
し、コンストラクタや、場合によってはセッターに”注入”してくれます。
いわゆる依存性の注入です。依存性というと分かりづらいですが、オブジェクトが依存するオブジェクト (コンストラクタの引数にあるなど) を受け取る行為のことです。
上の例では、HelloController
はNoteRepository
の存在に依存しています。そこで、どのようなオブジェクトにするかを外部に任せ、外部から依存するオブジェクトを渡してあげるという行為をする場合があります。このようなパターンを依存性の注入と呼びます。
依存性を自動解決
クラスに依存関係がなかったり、指定したクラスそのものをnew
するだけで良い場合は、何もする必要はありません。タイプヒントを仮引数につけることで、上の例のように自動てインスタンスをコンストラクタに入れてくれます。
また、この依存は何個でも指定できます。
use App\Repositories\NoteRepository; class HelloController extends Controller { private $notes; // NoteRepositoryとUserRepositoryはinterfaceでもabstractでもない // この2つのインスタンスは、外部で自動的で作成され、渡してくれる public function __construct(NoteRepository $notes, UserRepository $user) { $this->notes = $notes; // ... } }
class HelloController extends Controller { // 引数がないので何も注入しない public function __construct() { } }
この注入は任意のクラスで行なえます。上の例ではリポジトリパターンを想定してXxxRepository
というクラス名ですが、サービス層を用意してYyyService
というクラスを入れることなどもできます。
また、これらの依存性は、依存が連鎖していても全て解決してくれます。
依存性を手動解決
上の例ではコンストラクタの仮引数にタイプヒントを付けることで自動で解決しました。しかし、interfaceを指定する、事前になにか値を入れておきたいなどがある場合は手動で指定できます。
つまり、このインスタンスがほしいというのをコンストラクタにかけば、事前に手動で設定した通りにインスタンスを作り、値を入れたりした上でインスタンスを渡してくれるということです。
これを結合を用いて設定します。
bind
app()->bind($abstract, $concrete)
を用いて、結合を設定します。第1引数に利用する側が依存するクラスを、第2引数に実際に作成するインスタンスの作り方を指定します。
サービスプロバイダ内であれば$this->app->bind(/* ... */)
でも可能です。また、App
ファサードでも使用できます。
namespace App\Services; use Illuminate\Support\Facades\Log; class HogeService { private $value; public function setValue($value) { $this->value = $value; } public function outputLog() { Log::debug($this->value); } }
namespace App\Providers; class AppServiceProvider extends ServiceProvider { public function register() { // ここで結合を設定 // HogeService (第1引数) を要求されたときに何を返すかを、第2引数の関数で設定する app()->bind(HogeService::class, function ($app) { $hoge = new HogeService; $hoge->setValue(500); return $hoge; }); // ServiceProvider内なので、$this->app->bind(...) でも良い // use Illuminate\Support\Facades\App; // App::bind(...); でもどこでも良い } }
namespace App\Http\Controllers; use App\Services\HogeService; class HelloController extends Controller { public function __construct(HogeService $hoge) { $hoge->outputLog(); // local.DEBUG: 500 } public function index() { // ... }
ここで、指定するクラスにはインターフェースなどを用いることもできます。
namespace App\Services; interface IFooService { function outputLog(); }
namespace App\Services; use Illuminate\Support\Facades\Log; class HogeService implements IFooService { private $value; public function setValue($value) { $this->value = $value; } public function outputLog() { Log::debug($this->value); } }
namespace App\Providers; use App\Services\IFooService; use App\Services\HogeService; class AppServiceProvider extends ServiceProvider { public function register() { app()->bind(IFooService::class, function ($app) { $hoge = new HogeService; $hoge->setValue(500); return $hoge; }); } }
namespace App\Http\Controllers; use App\Services\IFooService; class HelloController extends Controller { public function __construct(IFooService $hoge) { // ... }
また、指定したクラスをnew
したいだけの場合、関数ではなくクラス名::class
でも動作します。
app()->bind(IFooService::class, HogeService::class);
シングルトンを結合
シングルトンでは、アクセスされることでLaravelが動いてからWebページを返すまでの一連の動作で、ただ1つだけインスタンスを作成し、作成後は使い回す際に使用します。
シングルトンにしたい場合、bind
の代わりにsingleton
を用います。次の例は、コントローラとコンポーネントのクラスで同じクラスを指定しているため、シングルトンで使い回せている例です。
namespace App\Services; use Illuminate\Support\Facades\Log; // このクラスをシングルトンで扱う class HogeService { private $value = 20; public function addValue($value) { $this->value += $value; } public function outputLog() { Log::debug($this->value); } }
namespace App\Providers; class AppServiceProvider extends ServiceProvider { public function register() { app()->singleton(HogeService::class); // app()->singleton(HogeService::class, HogeService::class); と同じ } }
namespace App\Http\Controllers; use App\Services\HogeService; class HelloController extends Controller { public function __construct(HogeService $hoge) { $hoge->addValue(5); $hoge->outputLog(); // local.DEBUG: 25 } public function index() { /*...*/ }
namespace App\View\Components; use App\Services\HogeService; class Alert extends Component { public function __construct(HogeService $hoge, $noteId) { $hoge->addValue(100); $hoge->outputLog(); // local.DEBUG: 125 // このコンストラクタ引数では、viewのコンポーネントから渡される属性も並べている // $note = Note::find($noteId); } public function render() { /* ... */ } }
上の例ではシングルトンを利用しているため、コントローラで5
を加算した状態のままコンポーネントのクラスにも引き継いでいることが分かります。1つのインスタンスを使いまわしているためです。
つまり、1つのインスタンスを全体で共有できるということになります。
インスタンスを結合
また、既に作成済みのインスタンスを使用して結合することでもインスタンスを共有できます。bind
の代わりにinstance
を用います。
$hoge = new HogeService; app()->instance(HogeService::class, $hoge);
インターフェースを使って、対象に応じて実装を変える
今までの例では、1つの依存されるクラスに対して、ただ1つの対応するインスタンス作成を行っていました。これでは同じクラスのインスタンスを返すことしかできません。
そこで、when
、needs
、give
の3つを使って、注入対象のクラスに応じて作成するオブジェクトを変更することができます。
namespace App\Providers; use App\Http\Controllers\HelloController; use App\Services\FugaService; use App\Services\HogeService; use App\Services\IFooService; use App\View\Components\Alert; use App\View\Components\Hoge\Fuga; use Illuminate\Support\Facades\App; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function register() { // 注入対象がHelloControllerで、IFooServiceに依存する部分は、HogeServiceのインスタンスを渡す app()->when(HelloController::class) ->needs(IFooService::class) ->give(function () { return new HogeService; }); /** * 注入対象が App\View\Components\Alert か App\View\Components\Hoge\Fuga の場合でIFooServiceに依存する部分は * FugaServiceのインスタンスを渡す */ app()->when([Alert::class, Fuga::class]) ->needs(IFooService::class) ->give(function () { return new FugaService; }); } }
namespace App\Services; use Illuminate\Support\Facades\Log; // IFooService.php interface IFooService { function outputLog(); } // HogeService.php class HogeService implements IFooService { // ... } // FugaService.php class FugaService implements IFooService { // ... }
サービスコンテナを使って依存解決
これまでの例では、Laravel側がクラスを作るものに使うように、関連付けをしてきました。しかし、開発者がその依存解決の機能を使ってインスタンスを作ることもできます。
make
そのためには、new
するのではなくapp()->make
やApp::make
を使用します。
class HelloController extends Controller { public function __construct() { // 例示のため、ここでHigeService作成 $hige = app()->make(HigeService::class); $hige->outputLog(); } }
namespace App\Services; use App\DomainModels\NoteDomainModel; use Illuminate\Support\Facades\Log; class HigeService { /** * @var NoteDomainModel */ private $note; public function __construct(NoteDomainModel $note) { $this->note = $note; } public function outputLog() { Log::debug('Hige' . $this->note->getNoteContent(1)); } }
namespace App\DomainModels; use App\Models\Note; class NoteDomainModel { public function getNoteContent($id) { $note = Note::find($id); return $note->content; } }
この例では、HigeService
がNoteDomainModel
に依存しています。通常であればHigeService
をnew
する時にNoteDomainModel
をnew
したものを引数に入れる必要がありますが、Laravelのサービスコンテナを用いることで、その必要はなくなりました。
今まで通り自動的に解決するか、bind
等で定義したとおりにインスタンスが作成され、注入してくれます。
bind
関数内でもmake
は使用できます。
$this->app->bind(IFooService::class, function ($app) { return $app->make(HogeService::class); });
makeWith
また、makeWith
を使用すると、第2引数に変数名と値のペアの連想配列を入れると、その他のコンストラクタ引数として渡せます。
class HigeService { /** * @var NoteDomainModel */ private $note; public function __construct(NoteDomainModel $note, $v, $v2) { $this->note = $note; Log::debug($v . $v2); } }
$hige = app()->makeWith(HigeService::class, ['v' => 100, 'v2' => 200]);
makeWith
で引数を渡すと、暗黙の結合をした場合はコンストラクタに自動的に渡されますが、結合を明示した場合は関数の第2引数で受け取ることができます。
$hige = app()->makeWith(HigeService::class, ['v' => 100, 'v2' => 200]);
app()->bind(HigeService::class, function (Application $app, array $context) { Log::debug($context['v']); // local.DEBUG: 100 Log::debug($context['v2']); // local.DEBUG: 200 return new HigeService( $app->make(NoteDomainModel::class), $context['v'], $context['v2'] ); });
app
make
とmakeWith
は、app
関数でも通ります。
// 自動で解決できない引数がある場合は、makeWithのように渡す $hige = app(HigeService::class, ['v' => 100, 'v2' => 200]); // 自動で解決できる引数のみの場合 $hoge = app(HogeService::class);
namespace App\Services; class HogeService { public function __construct() { // ... } }
namespace App\Services; use App\DomainModels\NoteDomainModel; use Illuminate\Support\Facades\Log; class HigeService { private $note; // NoteDomainModelは自動で解決できる public function __construct(NoteDomainModel $note, $v, $v2) { $this->note = $note; Log::debug($v . $v2); } }
app()->singleton(/* ... */)
をしているものを対象にインスタンスを取得する場合で、対象に解決できないコンストラクタ引数がある場合、毎回コンストラクタに引数を渡す必要があります。
// シングルトンで結合 (HigeServiceを要求されたら、HigeServiceをmakeする) app()->singleton(HigeService::class);
// OK $hige = app(HigeService::class, ['v' => 100, 'v2' => 200]); // Unresolvable dependency resolving [Parameter #1 [ <required> $v ]] in class App\Services\HigeService $hige = app(HigeService::class);
まとめ
サービスコンテナは、Laravelの核といえる機能の1つです。公式ドキュメントでも4つの構成概念の1つに含まれています。
オブジェクト指向や疎結合などに関する知識が無いと中々メリットが分かりづらいですが、把握できれば大きく前進できる内容です。
