はじめに
次のような、コンストラクタでendpoint
とtoken
を受け取り、APIの操作をするクラスがあったとします。
<?php
namespace App\Service;
class ApiClient
{
public function __construct(private string $endpoint, private string $token)
{}
public function run()
{
...
}
}
このクラスを、DIできるようにするには、サービスプロバイダでLaravelのコンテナに登録しますよね。
<?php
namespace App\Providers;
use App\Service\ApiClient;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(ApiClient::class, fn () => new ApiClient('https://hoge.example.com', 'xxxxxxxxxx'));
}
}
<?php
namespace App\Usecase;
use App\Service\ApiClient;
class FugaUsecase
{
// DIされる
public function __construct(private ApiClient $apiClient)
{}
public function exec()
{
$this->apiClient->run();
}
}
しかし、異なるendpoint
とtoken
でApiClient
を使いたいケースが出てきたらどうしますか?
ん?ApiClient
の中身をコピペしたクラスをもう一つ作る?さらに異なるendpoint
とtoken
でApiClient
を使いたいケースが出てきたら?修正が発生した時は全部のコードを直しますか?
いいえ、実はそれ、Providerの設定だけでできますよ!という話です。
<?php
namespace App\Usecase;
use App\Service\ApiClient;
class FugaUsecase
{
// endpointとtokenの異なる ApiClient が欲しい!
public function __construct(private ApiClient $apiClient)
{}
public function exec()
{
$this->apiClient->run();
}
}
やり方
サービスコンテナへ登録する時、$this->app->bind()
を使用しますが、この第一引数、実はクラス名じゃなくても大丈夫なんです。ここで指定する値は「サービスをコンテナに登録する際のID」として使われます。ここの値をクラス名と一致させることで「型を使ったDI」ができるようになりますが、その機能を使わないのであれば、必ずしもクラス名にする必要はありません。
よって、まず異なるendpoint
とtoken
を持つ2種類のApiClient
を異なるIDでコンテナに登録します。
コンテナからは$this->app->make(<ID>)
で登録したサービスを取得することができるので、必要に応じてサービスを取得し、依存性を解決します。
<?php
namespace App\Providers;
use App\Service\ApiClient;
use App\Usecase\FugaUsecase;
use App\Usecase\HogeUsecase;
use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
// 2種類のサービスを登録
$this->app->bind('api_client.hoge', fn () => new ApiClient('https://hoge.example.com', 'xxxxxxxxxx'));
$this->app->bind('api_client.fuga', fn () => new ApiClient('https://fuga.example.com', 'yyyyyyyyyy'));
// コンテナから取得した ApiClient を使ってサービスを登録する
$this->app->bind(HogeUsecase::class, fn (Application $app) => new HogeUsecase($app->make('api_client.hoge')));
$this->app->bind(FugaUsecase::class, fn (Application $app) => new FugaUsecase($app->make('api_client.fuga')));
}
}
また、ApiClient
を使い回す必要がないのであれば、次のように依存性解決と同時にサービスの定義を行うこともできます。
<?php
namespace App\Providers;
use App\Service\ApiClient;
use App\Usecase\FugaUsecase;
use App\Usecase\HogeUsecase;
use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(
HogeUsecase::class,
function (Application $app) {
$client = $app->make(ApiClient::class, ['endpoint' => 'https://hoge.example.com', 'token' => 'xxxxxxxxxx']);
return new HogeUsecase($client);
}
);
$this->app->bind(
FugaUsecase::class,
function (Application $app) {
$client = $app->make(ApiClient::class, ['endpoint' => 'https://fuga.example.com', 'token' => 'yyyyyyyyyy']);
return new FugaUsecase($client);
}
);
}
}
終わりに
今回は、重複コードを発生させずに、パラメータの異なる複数のサービスを定義する方法を紹介しました。自前で依存性を管理したり、サービスプロバイダのあまり使わない機能を使ったりするので、初心者には少しハードルが高いかもしれません。
しかし「パラメータの異なるクラスを沢山作りたいとき」や「対象のサービスがかなり大きいクラス」である場合は重複コードを作るよりも、大きなメリットになるので、覚えておいて損はない機能だと思います。使えるタイミングがあれば是非使ってみてください。