このブログははてなブログからの移行記事です。
この記事は
Laravelアドベントカレンダー8日目の記事です。
前提知識
この記事では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
これによって何が嬉しいかというと、開発者は例えば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の記事です。お楽しみに!