Masayan tech blog.

  1. ブログ記事一覧>
  2. 【Laravel】依存性の注入(Dependency injection)とサービスコンテナについて説明してみる

【Laravel】依存性の注入(Dependency injection)とサービスコンテナについて説明してみる

公開日

環境

  • windows10
  • PHP 8.x
  • VsCode
  • Laravel8.x

依存性の注入(DI)とは

あるクラス(A)が依存している別のクラス(B)のオブジェクトを外部から渡すことです。(Aの内部でBをnewしない)

DIのメリット

DIの主なメリットは以下が挙げられます。

  • クラスの生成を1箇所に集約できる
  • クラスの構造を隠蔽できる
  • モックを使ってテストがしやすくなる

以降は、サービスコンテナの概要と、Laravelで行われる基本的なDIパターンについて紹介したいと思います。

サービスコンテナ

サービスコンテナとは

いろいろなサービスを管理できる領域のことです。Laravelでは、このサービスコンテナを利用して、依存性注入を簡単に実装することが可能です。

サービスコンテナにサービスを追加する

bindメソッドを使うことで、サービスコンテナにサービスを入れることができます。

// bind('サービス名', 内容)
app()->bind('myservice', function () {
  return 1 + 2;
});

サービスコンテナからサービスを取り出す

サービスコンテナに入れたサービスは、makeメソッドを利用して取り出すことができます。

$myService = app()->make('myservice');
ddd($myService); // 3

依存関係の注入(DI)

さて、ここからが本題です。Laravelで行われる基本的なDIパターンについて3つ(+@)紹介したいと思います。

  1. コントローラーのメソッドインジェクション
  2. コントローラーのコンストラクタインジェクション
  3. インターフェースのインジェクション

コントローラーのメソッドインジェクション

なぜControllerではRequestクラスが引数で受け取れるのか

Laravelでリソースコントローラーを作成すると、store()やupdate()の引数にはRequest $requestが自動で付与されていると思います。

public function store(Request $request) 
{
}

これらのメソッドはユーザーからのリクエストを受け取ってデータを保存したり、更新したりすることが目的なので、現在のユーザーからのリクエストにすぐにアクセスすることができる必要がある、ということが想像できます。

これについては、Laravel公式ドキュメントにも下記の記載がある通り、コントローラ、イベントリスナ、キュージョブ、ミドルウェア、コマンド、ルートなどLaravelのスキームに組み込まれたクラスはサービスコンテナへの登録なしに、現在のリクエストにすぐにアクセスすることができるようになっているため、このようなタイプヒントと呼ばれる記述が可能になるわけです。

ありがたいことに依存解決の設定がいらないため、ルート、コントローラ、イベントリスナ、その他どこでも、コンテナを手動で操作しなくても、依存関係を頻繁にタイプヒントするでしょう。たとえば、現在のリクエストに簡単にアクセスできるように、ルート定義でIlluminate\Http\Requestオブジェクトをタイプヒントできます。このコードを書くため、コンテナーを操作する必要はありません。コンテナーはこうした依存関係の注入をバックグラウンドで管理しています。

公式ドキュメントより一部抜粋

以降の説明のため、app\Http\Controllers\InjectController.phpを用意しておきます。

<?php

namespace App\Http\Controllers;

use App\Sample\Part2\CalcInterface;
use App\Sample\Part3\Greet;

class InjectController extends Controller
{
    private Greet $greet;

    public function __construct(Greet $greet)
    {
        $this->greet = $greet;
    }

    // コントローラーのメソッドインジェクション
    public function injectByMethod(Greet $greet)
    {
        ddd($greet->find("Ch")); // ニーハオ
    }

    // コントローラーのコンストラクタインジェクション
    public function injectByConstructer()
    {
        ddd($this->greet->find("En")); // Hello
    }

    // コントローラーのインジェクション(インターフェース)
    public function injectToInterfaceByConstructer(CalcInterface $calc)
    {
        ddd($calc->exec(1, 10)); // 11
    }
}

具体例

url(/)にアクセスすると、web.phpにてInjectControllerのinjectByMethodが呼び出されます。injectByMethodでは、Greetクラスのオブジェクトを引数として受け取り、Greetクラスのfindメソッドを使用して得られた戻り値を出力しています。

web.php

<?php

use App\Http\Controllers\InjectController;
use Illuminate\Support\Facades\Route;

// 1.コントローラーのメソッドインジェクション
Route::get("/", [InjectController::class, "injectByMethod"]);

app\Http\Controllers\InjectController.php

<?php

namespace App\Http\Controllers;

use App\Sample\Part3\Greet;

// コントローラーのメソッドインジェクション     
public function injectByMethod(Greet $greet)     
{  
  // 不要 $greet = new Greet();        
  ddd($greet->find("Ch")); // ニーハオ
}

app\sample\part3\Greet.php

<?php

// コントローラーのメソッドインジェクション
namespace App\Sample\Part3;

class Greet
{
    private array $greetArray = [
        "En" => "Hello",
        "Ja" => "こんにちは",
        "Ch" => "ニーハオ",
        "Ko" => "アンニョンハセヨ",
        "Vi" => "シン・チャオ",
    ];

    public function find(string $type): array
    {
        return array_filter(
            $this->greetArray,
            function ($key) use ($type) {
                return $key === $type;
            },
            // 未指定なら配列の値をフィルタリング対象にできる
            // ARRAY_FILTER_USE_BOTH を指定する配列のキーと値をフィルタリング対象にできる
            ARRAY_FILTER_USE_KEY // 配列のキーをフィルタリング対象にできる
        );
    }
}

前述のタイプヒントにより、コントローラー内のinjectByMethodの引数としてGreetクラスのオブジェクトを受け取ることができるため、メソッド内でnewしてインスタンスを作成する必要がないということです。

コントローラーのコンストラクタインジェクション

url(/)にアクセスすると、web.phpにてInjectControllerのinjectByConstructerが呼び出されます。injectByConstructerでは、__constructにより、privateプロパティである$greetに対してGreetクラスのオブジェクトが注入されます。そして、injectByConstructerでは、このプロパティを使用してオブジェクトをnewすることなく、greetクラスのfindメソッドを呼び出すことができているわけです。

web.php

<?php

use App\Http\Controllers\InjectController;
use Illuminate\Support\Facades\Route;

// 2.コントローラーのコンストラクタインジェクション
Route::get("/", [InjectController::class, "injectByConstructer"]);

app\Http\Controllers\InjectController.php

<?php

namespace App\Http\Controllers;

use App\Sample\Part3\Greet;

class InjectController extends Controller
{
    private Greet $greet;

    public function __construct(Greet $greet)
    {
        $this->greet = $greet;
    }

    // コントローラーのコンストラクタインジェクション
    public function injectByConstructer()
    {
        ddd($this->greet->find("En")); // Hello
    }
}

コントローラーのインジェクション(インターフェース)

url(/)にアクセスすると、web.phpにてInjectControllerのinjectToInterfaceByConstructerが呼び出されます。injectToInterfaceByConstructerでは、CalcInterfaceをタイプヒントとして受け取り、execメソッドを実行して結果を出力しています。

web.php

<?php

use App\Http\Controllers\InjectController;
use Illuminate\Support\Facades\Route;

// 3.コントローラーのインジェクション(インターフェース)
// app\Providers\AppServiceProvider.phpに実装クラスのオブジェクトをbindしていることが前提
Route::get("/", [InjectController::class, "injectToInterfaceByConstructer"]);

app\Http\Controllers\InjectController.php

<?php

namespace App\Http\Controllers;

use App\Sample\Part2\CalcInterface;

class InjectController extends Controller
{
    // コントローラーのインジェクション(インターフェース)
    public function injectToInterfaceByConstructer(CalcInterface $calc)
    {
        ddd($calc->exec(1, 10)); // 11
    }
}

app\sample\part2\CalcInterface.php

// インターフェース
<?php

namespace App\Sample\Part2;

interface CalcInterface
{
    public function exec(int $x, int $y): int;
}

app\sample\part2\Addition.php

// 実装クラス
<?php

namespace App\Sample\Part2;

class Addition implements CalcInterface
{
    public function exec(int $x, int $y): int
    {
        return $x + $y;
    }
}

このパターンでは、Interfaceの実装クラスは自動的に解決できないため、どのクラスのオブジェクトを外部から注入したいのかについて、あらかじめサービスコンテナにbindしておく必要があります。

app\Providers\AppServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Sample\Part2\Addition;
use App\Sample\Part2\CalcInterface;


class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(CalcInterface::class, function ($app) {
            return $app->make(Addition::class);
        });
    }
}

公式ドキュメントの記載にもあるように、リフレクションクラスという仕組みを利用することで、インターフェースの実装クラスではないクラスは実体が1つなので、自動的に解決してくれます。

クラスがどのインターフェイスにも依存しない場合、クラスをコンテナーにバインドする必要はありません。コンテナは、リフレクションを使用してこれらのオブジェクトを自動的に解決できるため、これらのオブジェクトの作成方法を指示する必要はありません

サービスコンテナをサービスプロバイダ外から利用する方法

Laravelではresloveを使用して、サービスプロバイダの外からでも依存関係を解決することができます。

Mainクラスは、インスタンス生成時にconstructでCalcクラスのオブジェクトを受け取る必要があるため、Calcクラスに依存しています。

ところが、resolveメソッドに、対象クラスの完全修飾名を含む文字列を渡してあげることで、newしてインスタンスを生成することなくMainクラスのオブジェクトを生成することができます。

// resolveにより、サービスコンテナを使用して指定したクラスまたはインターフェイス名をインスタンスへ依存解決
$main = new Main(resolve(Calc::class)); // HogeClass::classを使用すると、Hogeクラスの完全修飾名を含む文字列を取得できる
$main->exec(12);

app\sample\Main.php

<?php

namespace App\Sample;

use App\Sample\Calc;

class Main
{
    private $calc;

    public function __construct(Calc $calc)
    {
        $this->calc = $calc;
    }

    public function exec(int $x, int $y): void
    {
        print_r($this->calc->multiplication($x, $y));
    }
}

app\sample\Calc.php

<?php

namespace App\Sample;

class Calc
{
    public function multiplication(int $x, int $y)
    {
        return $x * $y;
    }
}

以上です。

Laravel依存性の注入(Dependency injection)とサービスコンテナは、フレームワークを用いて効率的に開発する際に理解しておくべき重要な概念になりますので、ぜひ参考にしてみてください。

まとめ

いかがでしたでしょうか。本記事では、LaravelフレームワークでのDI(依存性の注入)の方法について紹介しています。フレームワークが用意してくれているDIの仕組みを活用することで、クラスの生成を1箇所に集約できたり、クラスの構造を隠蔽できたり、モックを使ってテストがしやすくなる等、メリットしかありませんので、ぜひ活用してみてください。