Laravelのサービスコンテナについて簡単に書いてみた#2


例を作成する

なるべくテストしやすく、拡張可能な設計をしたいため、コンストラクタインジェクションを意識して作成してみた。
[DI]https://qiita.com/1000k/items/aef6aed46b0fc34cc15e

簡単な例を作る sampleという文字列をDBから引っ張ってきて、Serviceクラスに呼び、それをコントローラーで出力する。

SampleRepository.php
<?php

namespace App\Http\Repositories;

class SampleRepository
{
    //今回はDBからとるのは、パスして仮文字列を用意し返す。
    public function sampleRepo()
    {
        $sampleStr = 'sample';

        return $sampleStr;
    }

}

コンストラクタインジェクションを使用するため、コンストラクタの引数に使用する。Repositoryクラスを指定し、それをsampleSerメソッドで利用している。

SampleService.php
<?php

namespace App\Http\Services;

use App\Http\Repositories\SampleRepository;

class SampleService
{
    protected $sampleRepository;

    public function __construct(SampleRepository $sampleRepository)
    {
        $this->sampleRepository = $sampleRepository;
    }

    public function sampleSer()
    {
        $sampleStr = $this->sampleRepository->sampleRepo();

        return $sampleStr;
    }

}

前回記事のバインドしていない解決を使用する(リンク)

SampleController.php
<?php

namespace App\Http\Controllers;

use App\Http\Services\SampleService;

class SampleController extends Controller
{

    public function sampleCon()
    {
        $service = app(SampleService::class);

        dd($service->sampleSer());
    }

}

サービスコンテナにバインドするための記述

AppServiceProvider.php
<?php

namespace App\Providers;

use App\Http\Repositories\SampleRepository;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    //サービスコンテナに結合(登録する処理を書く)
    public function register()
    {
        $this->app->bind(SampleRepository::class, function (){
            return new SampleRepository();
        });
    }

    //ほかの全サービスプロバイダーが登録後に呼ばれる(サービスコンテナに結合)
    public function boot()
    {

    }
}
実行出力内容 "sample"

ちょっと脱線すると、register()boot()はどちらもサービスコンテナに結合させる処理を中に書きます。
違いは、bootはすべての登録後に呼ばれるので、ビューコンポーザをサービスプロバイダで登録する処理を書くのに向いています。
ビューコンポーザは、必要な情報をテンプレートに紐づけられる(使い所:ログイン情報など)
[ビューコンポーザ]https://qiita.com/youkyll/items/c65af61eb33919b29e97

◆説明とメリット

コンストラクタインジェクションを使うと、どうしても引数の数が増えていくと想像できます。それをコントローラー側で呼び出すときは、その分インスタンス化したサービスクラスの引数を埋めてあげなければいけません。

class SampleService
{
    protected $sampleRepository1;
    protected $sampleRepository2;
    protected $sampleRepository3;
    protected $sampleRepository4;

    public function __construct(
        SampleRepository1 $sampleRepository1,
        SampleRepository2 $sampleRepository2,
        SampleRepository3 $sampleRepository3,
        SampleRepository4 $sampleRepository4,)
    {
        $this->sampleRepository1 = $sampleRepository1;
        $this->sampleRepository2 = $sampleRepository2;
        $this->sampleRepository3 = $sampleRepository3;
        $this->sampleRepository4 = $sampleRepository4;
    }
}
class SampleController extends Controller
{

    public function sampleCon()
    {
        $sampleRepository1 = new SampleRepository1();
        $sampleRepository2 = new SampleRepository2();
        $sampleRepository3 = new SampleRepository3();
        $sampleRepository4 = new SampleRepository4();

        $service = new SampleService($sampleRepository1, $sampleRepository2, $sampleRepository3, $sampleRepository4);

    }

}

これは、設計次第では解決できるパターンもあると思いますが、もしこうなった形になってしまったとき、どうするか?

サービスクラスでは、一度書いてしまえば引数をもらうだけなので、いいとして、問題はコントローラーのメソッドにあると思います。
なぜなら、コントローラーのメソッドは増えれば増えるほど、毎回この記述を書く可能性があるためです。コントローラーでこのサービスクラスを呼び出す処理をまとめたとしても、コントローラーに一つだけ異質なメソッドができてしまう。また、サービスクラスを呼び出す処理をまとめたクラスをつくり、ワンクッション入れるとしてもファイルが増えてめんどくさい。

それらの問題を解決してくれる機能がLaravelにありました。上のサービスを解決する処理を見ていただきたいのですが、

$service = app(SampleService::class);

前回の記事で説明させていただいたバインドしていない場合の解決を利用し、コントローラーでapp(SampleService::class)を解決しています。前回の内容では、Sampleクラスのコンストラクタの引数の指定がないので、解決がうまくいきました。今回は、サービスクラスのコンストラクタに引数の指定が存在するのになぜうまく"sample"を出力できたかというと、

プロバイダークラスをみると、

$this->app->bind(SampleRepository::class, function (){
    return new SampleRepository();
});

という記述で、SampleRepositoryクラスをサービスコンテナにバインドしています。こうすることで、サービスコンテナはbindするときにクラスまたはインターフェースで指定すると、それをコンストラクタの引数で使用しているクラス(今回ならSampleServiceクラス)をapp(SampleService::class)でインスタンスをよんだときに、自動でSampleServiceクラスのコンストラクタの引数を注入してくれます。これを利用すれば、コントローラーのメソッドは格段に短くなるのでメリットだと思いました。
僕も作成させていただくときは使いたいなぁーと思った・・・!(^^)!

[補足メソッドインジェクション]

上記方法(自動でコンストラクタの引数注入)はメソッドインジェクションを利用するときでも同様に使用できる

例 上の例で作ったサービスクラスのメソッドを改造
public function sampleSer(SampleRepository $sampleRepository)
{
    return $sampleRepository->sampleRepo();
}

これをコントローラーで呼ぶときは

例 上の例で作ったコントローラークラスのメソッドを改造
public function sampleCon()
{
    $service = app(SampleService::class);

    dd(app()->call([$service, 'sampleSer'], []));
}

call([バインドしたクラス, メソッド名], [引数])
今回は、引数がバインドしたクラス以外ないので空の配列を渡している。
個人的には、コントローラーの記述量が引数が入ってくると増えて、上の階層(コントローラー)の記述が増え、下の階層(サービス)の記述が増えるのは、少し違和感があるので、コンストラクタインジェクションを利用したほうがいいんじゃないかなぁと思った。

[サービスコンテナ]https://readouble.com/laravel/5.7/ja/container.html