はらへり日記

腹に弾丸

Laravelでセッターインジェクションする

この記事は

Laravelアドベントカレンダー8日目の記事です。

qiita.com

前提知識

この記事ではDIパターンを実現する1つの手段であるセッターインジェクションをLaravelで実現する方法を紹介します。

なのでDIパターンやDIコンテナを知らない方は先にこれらの記事を読んでいただくと理解が進むと思います。

Inversion of Control コンテナと Dependency Injection パターン

さくっと知りたい方は私が今年発表したスライドの55枚目までを流し読みしていただければと思います。

LaravelにおけるDependency Injection

LaravelはHTTPリクエストが来たタイミングでDIコンテナが起動します。

それがアプリケーションコードのほぼ全ての依存を解決するので、開発者はインスタンスの生成方法を意識せずにコーディングすることが可能となっています。

例えば以下のようなコントローラークラスがあるとしましょう。

<?php

namespace App\Http\Controllers;

use App\Services\SampleService;

/**
 * Class SampleController
 */
class SampleController 
{
    /**
     * @param SampleService  $sampleService
     *
     * @return \Illuminate\Http\Response
     */
    public function main(SampleService $sampleService)
    {
        $data = $sampleService->getData();
        
        return view('main', compact('data'));
    }
}

なんてことはない、mainという名前のViewを必要なデータを入れてレンダリングして返すコントローラーメソッドです。

このコントローラーをもしLaravelを使わずに使おうと思うとおそらくこんな感じのコードを書かなければなりません。

(コードはイメージ)

<?php

namesapce App\Http;

use App\Http\Controllers;
use App\Services\SampleService;
use App\Repositories\SampleRepository;

/**
 * Class OriginalRoute
 */
class OriginalRoute
{
    /**
     * @return array
     */
    public function route()
    {
        return [
            '/main' => function (\Illuminate\Http\Request $request) {
                $controller = new SampleController;
                $response = $controller->main(
                    new SampleService(new SampleRepository)
                );
                return $response;
            },
        ];
    }
}

注目してほしいのは連想配列/mainに指定しているClouser部分です。

実装者はSampleControllerインスタンスを実装し、mainメソッドが依存しているインスタンスを自分の手で生成する必要があります。

小規模なアプリケーションであればこのような方法で実装するのは問題にはなりづらいかもしれません。

しかしクラス数が増えたら?インスタンスの生成方法が変わったら?実装の差し替えが起こったら?

そういったことを考えるとこのような方法では将来的につらいことになる可能性が高いです。

しかしLaravelだとこんなことはしなくても大丈夫です。

auto wiring

先ほど述べたようにLaravelではHTTPリクエストが来た際にDIコンテナが立ち上がります。

そのDIコンテナがアプリケーションコード中でDIパターンによって明示的に指定されている依存関係を全てよしなに解決します。

詳しい仕組みはコードを読むとよいと思いますが、LaravelのDIコンテナはReflection等を活用して自動で必要な依存関係を調べるauto wiringという仕組みで動いています。

詳しくは下記リンクを読むと理解が進むかと思います。

Aura.Di/auto.md at 3.x · auraphp/Aura.Di · GitHub

Auto Wiring - Container

これによって何が嬉しいかというと、開発者は例えばSampleController::main()メソッドが必要としているSampleServiceインスタンスを生成する必要が無いということです。

普段は意識することは少ないかもしれませんが、これを覚えておくと設定なしにInterfaceやスカラー型をタイプヒントしてもDIコンテナからインジェクションしてくれない理由がわかると思います。

セッターインジェクション

セッターインジェクションとはDIパターンを実現するための1つの手段です。

Laravelでよく使われる手段としてコンストラクタインジェクションがあります。

<?php

namespace App\Services;

use App\Repositories\SampleRepository;

/**
 * Class SampleService
 */
class SampleService
{
    /** @var SampleRepository */
    protected $sample;

    /**
     * ConstructorでタイプヒントしておくとLaravelのauto wiringにより
     * インスタンスが注入される
     *
     * @param SampleRepository  $sample
     */
    public function __construct(SampleRepository $sample)
    {
        $this->sample = $sample;
    }
}

これをセッターメソッドを用意して行うのがセッターインジェクションです。

上記のコードをセッターインジェクションを用いたコードに書き直すとこんな感じ。

<?php

namespace App\Services;

use App\Repositories\SampleRepository;

/**
 * Class SampleService
 */
class SampleService
{
    /** @var SampleRepository */
    protected $sample;

    /**
     * Constructor
     */
    public function __construct()
    {
        //
    }

    /**
     * @param SampleRepository  $sample
     */
    public function setSampleRepository(SampleRepository $sample)
    {
        $this->sample = $sample;
    }
}

こうすることで以下のような形でSampleRepositoryインスタンスをDIすることができます。

<?php

$sampleService = new \App\Services\SampleService;

// Dependency Injection!!
$sampleService->setSampleRepository(new SampleRepository);

簡単ですよね。

Laravelでのセッターインジェクションのやり方

ここからがこの記事の本編です。

Laravelではセッターインジェクションに対する定義を行うAPIが用意されていません(あったら教えてください…)。

なので方針としては

といった感じでやります。

対象のクラスは先ほど出てきたSampleServiceを利用します。

<?php

namespace App\Services;

use App\Repositories\SampleRepository;

/**
 * Class SampleService
 */
class SampleService
{
    /** @var SampleRepository */
    protected $sample;

    /**
     * Constructor
     */
    public function __construct()
    {
        //
    }

    /**
     * @param SampleRepository  $sample
     */
    public function setSampleRepository(SampleRepository $sample)
    {
        $this->sample = $sample;
    }
}

このクラスをそのままLaravelで利用したら、普通に動きます。

しかしながらセッターは実行されないのでインスタンス生成直後にセッターメソッドを実行したい。

そういった場合はIlluminate/Container/Container::extendを利用します。

任意のServiceProviderでこんな処理を書きます。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class DependencyServiceProvider extends ServiceProvider
{
    public function register()
    {
        // set SampleRepository instance
        $this->app->extend(\App\Services\SampleService::class, function ($sampleService, $app) {
            $sampleService->setSampleRepository(new \App\Repositories\SampleRepository);
            return $sampleService;
        });
    }
}

また、extend等を通じて完全に依存解決された後にセッターインジェクションしたい場合にはIlluminate/Container/Container::resolvingを使用できます。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class DependencyServiceProvider extends ServiceProvider
{
    public function register()
    {
        // set SampleRepository instance
        $this->app->resolving(\App\Services\SampleService::class, function ($sampleService) {
            $sampleService->setSampleRepository(new \App\Repositories\SampleRepository);
            return $sampleService;
        });
    }
}

こうすることでDIコンテナによって生成されたSampleServiceインスタンスにセッターインジェクションを行うことができました!

セッターインジェクションの利用場面

このセッターインジェクションですが、普通の実装をしているとあまり使う場面は出てきません。

というのも大抵のインスタンスはコンストラクタインジェクションで事が足りるからです。

しかし、この方法を覚えておくと設計の幅が広がります。

例えばインスタンス生成の方法が複雑でコンストラクタの拡張が困難な場合などはセッターを生やした継承クラスを作成し、bindした上でセッターインジェクションするといったことが可能です。

他にも例えばログやトランザクションといった汎用的な処理を行うインスタンスのセッターをTraitで作成し、DIコンテナでセッターインジェクションすればクラス本体を汚さずに欲しいインスタンスを注入することも可能です。

まとめ

Laravelの良さはDIコンテナの柔軟さだと思ってます。

DIコンテナをしっかり利用できれば開発も楽になりますし何より楽しくなるので、依存解決で難解な場面に出くわすことがあればぜひこのセッターインジェクションという方法を思い出してみてください。

明日の記事はIganinTeaの記事です。お楽しみに!