【Laravel】 サービスコンテナを使って自動で依存性を解決しよう

サービスコンテナは、依存性の注入を行うためのツールです。コントローラやサービスが依存するオブジェクトを検出して、適切なインスタンスを外部から注入できます。つまり依存性の注入を行います。

サービスコンテナの例

この説明文だけではおそらくさっぱりわからないので、例を見てみましょう。

app/Http/Controllers/HelloController.php
<?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内ではNoteRepositorynewしていません。コンストラクタで受け取っていますが、どうやっているのでしょうか。Laravelはタイプヒントを見て自動的に必要なクラスをnewし、コンストラクタや、場合によってはセッターに”注入”してくれます。

いわゆる依存性の注入です。依存性というと分かりづらいですが、オブジェクトが依存するオブジェクト (コンストラクタの引数にあるなど) を受け取る行為のことです。

上の例では、HelloControllerNoteRepositoryの存在に依存しています。そこで、どのようなオブジェクトにするかを外部に任せ、外部から依存するオブジェクトを渡してあげるという行為をする場合があります。このようなパターンを依存性の注入と呼びます。

依存性を自動解決

クラスに依存関係がなかったり、指定したクラスそのものを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ファサードでも使用できます。

app/Services/HogeService.php
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);
    }
}
app/Providers/AppServiceProvider.php
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(...); でもどこでも良い
    }
}
app/Http/Controllers/HelloController.php
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()
    {
        // ...
    }

ここで、指定するクラスにはインターフェースなどを用いることもできます。

app/Services/IFooService.php
namespace App\Services;

interface IFooService {
    function outputLog();
}
app/Services/HogeService.php
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);
    }
}
app/Providers/AppServiceProvider.php
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;
        });
    }
}
app/Http/Controllers/HelloController.php
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を用います。次の例は、コントローラとコンポーネントのクラスで同じクラスを指定しているため、シングルトンで使い回せている例です。

app/Services/HogeService.php
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);
    }
}
app/Providers/AppServiceProvider.php
namespace App\Providers;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        app()->singleton(HogeService::class);
        // app()->singleton(HogeService::class, HogeService::class); と同じ
    }
}
app/Http/Controllers/HelloController.php
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() { /*...*/ }
app/View/Components/Alert.php
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つの対応するインスタンス作成を行っていました。これでは同じクラスのインスタンスを返すことしかできません。

そこで、whenneedsgiveの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;
            });
    }
}
各Service
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()->makeApp::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;
    }
}

この例では、HigeServiceNoteDomainModelに依存しています。通常であればHigeServicenewする時にNoteDomainModelnewしたものを引数に入れる必要がありますが、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

makemakeWithは、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つに含まれています。

オブジェクト指向や疎結合などに関する知識が無いと中々メリットが分かりづらいですが、把握できれば大きく前進できる内容です。

laravel service container thumb

役に立ったらシェアしよう!