猫でもわかるWeb開発・プログラミング

本業エンジニアリングマネージャー。副業Webエンジニア。Web開発のヒントや、副業、日常生活のことを書きます。

【PHP】LaravelでServiceProviderでInterfaceに実装を注入したりstringを注入する方法【DI】

f:id:yoshiki_utakata:20181004212834j:plain

Interfaceを利用する意義

LaravelはDIを勝手にいい感じにしてくれます。例えばこうです。

<?php

class RiotGamesApi 
{
    public function getAllChampions(): array 
    {
        // 実装
    }
}

class Controller
{
    public function __construct(RiotGamesApi $riptGamesApi)
    {
        $this->riotGamesApi;
    }
    
    public function get()
    {
        return $this->riotGamesApi->getAllChampions();
    }
}

この場合はLaravelがいい感じに Controller のコンストラクタに RiotGamesApi を注入してくれるということはみんな知っているでしょう。*1

さて、RiotGamesApi には色々なメソッドが生えている可能性があります。例えばこうです。

<?php

class RiotGamesApi
{
    public function getAllChampions(): array
    {
        // 実装
    }
    
    public function getSummonerData(string $summonerName): array
    {
        // 実装
    }
}

class Controller
{
    public function __construct(RiotGamesApi $riptGamesApi)
    {
        $this->riotGamesApi;
    }

    public function get()
    {
        return $this->riotGamesApi->getAllChampions();
    }
}

ここで、ControllerRiotGamesApi クラスのうち getAllChampions メソッドしか利用していません。そこで interface を利用して以下のように表現できます。

<?php

class RiotGamesApi implements ChampionGetter
{
    public function getAllChampions(): array
    {
        // 実装
    }

    public function getSummonerData(string $summonerName): array
    {
        // 実装
    }
}

interface ChampionGetter
{
    public function getAllChampions(): array;
}

class Controller
{
    public function __construct(ChampionGetter $championGetter)
    {
        $this->$championGetter;
    }

    public function get()
    {
        return $this->$championGetter->getAllChampions();
    }
}

Controller のコンストラクタは ChampionGetter インタフェースで受け取るわけです。 RiotGamesApi クラスは ChampionGetter を実装しているので Controller に注入することができるわけです。

こうするとどこがありがたいのか?もう少し例を拡張してみます。

<?php

class RiotGamesApi implements ChampionGetter, SummonerGetter
{
    // 略
}

class ChampionCache implements ChampionGetter 
{
    // 略
}

interface ChampionGetter
{
    public function getAllChampions(): array;
}

interface SummonerGetter
{
    public function getDummonerData(string $summonerName): array;
}

class Controller
{
    public function __construct(ChampionGetter $championGetter)
    {
        $this->$championGetter;
    }

    public function get()
    {
        return $this->$championGetter->getAllChampions();
    }
}

インタフェースとして ChampionGetterSummonerGetter が登場してきました。RiotGamesApi クラスは両方実装しています。ChampionCacheChampionGetter だけしか実装していません。

ControllerChampionGetter を要求しているので、RiotGamesApi でも ChampionCache でもどっちでもいいわけです。ここはインフラ側の都合で、APIに利用制限がないなら RiotGamesApi を、APIに利用制限があったり、実行速度の問題があるなら ChampionCache を使うなどの使い分けができるわけです。

一方で SummonerGetter を要求している場合は、こちらはキャッシュはありませんので、 RiotGamesApi を使うしかないということがわかります。

このようなことがコード上で表現できるのがインタフェースの一つの利点です。

Laravelでどうやって注入するの?

さてここで疑問です。

<?php

class Controller
{
    public function __construct(RiotGamesApi $riptGamesApi)
    {
        $this->riotGamesApi;
    }  
}

この場合Laravelが勝手にDIしてくれました。

<?php

class Controller
{
    public function __construct(ChampionGetterInterface $championGetter)
    {
        $this->$championGetter;
    }
}

この場合 ChampionGetterInterface はインタフェースなので勝手にDIはしてくれません。インタフェースを実装しているクラスは複数ある可能性があり、どれを注入していいか明確でないからです。

こういった場合はサービスプロバイダをにDI設定を記述することになります。

サービスプロバイダとは

サービスプロバイダは、app/Providers 以下にあるクラス群です。デフォルトで AppServiceProvider というプロバイダがあると思います。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

サービスプロバイダは、アプリケーションが起動した時(ユーザーからのHTTPリクエストを受けた時)に最初に実行される処理を記述したものです。Bootstrap的なものです。

サービスプロバイダには registerboot 二つのメソッドがあります。Laravelに登録された全てのサービスプロバイダの register が呼ばれた後、Laravel に登録された全てのサービスプロバイダの boot が呼ばれます(呼ばれる順序が重要です)。

Laravelに登録されているサービスプロバイダ一覧は config/app.php に書かれています。

<?php
...

    'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,
        // 略

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,

AppServiceProvider もここに登録されているのがわかります。

Laravel公式のサービスプロバイダのドキュメントは以下です。

readouble.com

register メソッドでDIの設定を書く

Laravelではこの register メソッドで DI の設定を書いていくことになります。Laravelのサービスプロバイダのドキュメントには

registerメソッドの中ではサービスコンテナへの登録だけを行わなくてはなりません。

と書いてあります。サービスコンテナのドキュメントは以下なのですが、

readouble.com

Laravelのサービスコンテナは、クラス間の依存を管理する強力な管理ツールです。依存注入というおかしな言葉は主に「コンストラクターか、ある場合にはセッターメソッドを利用し、あるクラスをそれらに依存しているクラスへ外部から注入する」という意味で使われます。

要するにサービスコンテナというのはDIの設定を管理してくれるツールということです。

新しくサービスプロバイダを作ってもいいですが、とりあえず AppServiceProvider にDI設定を書いていくことにします。書き方はこんな感じです。

<?php

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

これは「 ChampionGetter というインタフェースがが指定されたらその実装は RiotGamesApi を使いますよ。 RiotGamesApi はシングルトンとして使います」といった意味の記述になります。

Interfaceへの実装注入以外の用途でも使える

例えばこういう使い方もできます。

<?php

class RiotGamesApi
{
    public function __construct(string $apiKey) 
    {
        $this->apiKey = $apiKey;
    }
}

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(RiotGamesApi::class, function ($app) {
            return new RiotGamesApi(env('API_KEY'));
        });
    }
}

このように、string型は普通は勝手に注入することができないのですが、サービスコンテナに登録することでenvから取ってきて注入するということができます。

まとめ

  • Laravelでインターフェースに実装を注入するにはサービスプロバイダを使う
  • サービスプロバイダの register にサービスコンテナへの登録(=DI設定)を書く
  • インタフェースをうまく使っていこう!

新装版 達人プログラマー 職人から名匠への道

新装版 達人プログラマー 職人から名匠への道

*1:なぜこのようにコンストラクタで注入するかの説明は今回はしません。