わかりにくいサービスプロバイダーを理解する。
まえがき
Laravelを使っているとしばしばサービスプロバイダーというものを使え、という記事をみることになると思います。
なんとなく言われるがままにコピペして使ってるという人も多いのではないでしょうか?
これって何をするものでどう便利なんでしょうか?
私なりの解釈を書いてみます。
Factoryパターンの応用
サービスプロバイダーはGOFのデザインパターンにあるFactoryパターンを応用して作られています。
つまり、単純に
new MyClass()
とするよりも適した方法でインスタンスを生み出すためにあります。
で、どんなインスタンスを生み出すかというと、文字通りサービスクラスなんですね。
サービスクラスについては別の記事で書いたのでここでは詳しく書きませんが、MVCのModel機能の呼び出し口だったり、Model機能そのものだったりするものです。
Laravelは artisan make:servcieのようなコマンドがないためサービスクラスを作らずにコントローラに直接書く人もいますが、アンチパターンなのでよっぽど小さなプロジェクト以外ではあんまりやらない方がいいです。
ざっくりえばメール送信サービスとか、CSV読み書きサービスとか、DB読み書きサービスとか。
機能に何かの機能・処理に特化したクラスがサービスクラスですね。
DI(依存性の注入)
そのサービスクラスって別にサービスプロバイダーなんて使わなくても利用できるんですよ。
上記のように new MyClass()しても使えるし、メソッドインジェクションやコンストラクタインジェクションでも使えます。
メソッドインジェクションはこういうやつですね。
コントローラでクラス名を引数の型に指定すると自動的にインスタンスが生成されて渡されるものです。
public function store(MyService $service)
{
こんな呼び方もあります。
app()はLaravel標準のグローバルなヘルパー関数です。
$adminsList = app()->make('userService')->getAdminUsersList();
インスタンスの生成方法を指定するメリット
では、なぜあえてわざわざサービスプロバイダーを挟むのか。
まず、メモリ管理や実行速度の観点。
サービスクラスに記述する内容は同じインスタンスを使いまわしても問題ない内容が多いです。
サービスプロバイダーなら以下のように簡単にシングルトンパターンの実装が行えます。
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->singleton('myService', MyService::class);
}
}
そしてもっと重要なのはインスタンス作成時に条件を加えたりできることです。
環境や開発バージョンで違うサービスクラスを生成できます。
以下は、ローカル環境ではモッククラスを生成し、本番環境では正規のクラスを生成する方法です。
こうすることで、このサービスクラスのDB読み書き処理などが完成していなくても、関数名させ同じなら固定値などが返ってくるハリボテクラスを使いつつ、コントローラクラスなど、他の処理の開発を進めることが可能です。
public function register()
{
$this->app->singleton('myService', function ($app) {
// 環境に応じて異なるクラスをバインド
if (config('app.env') == 'local') {
// ローカル環境ではモッククラスを生成する。
return new myMocService();
}
// 本番環境などその他の場合はmyServiceを使用
return new myService();
});
}
コントローラは以下のように1行書くだけで、どっちのクラスを使うかは書いていません。
サービスプロバイダーで分岐した結果が注入されます。
つまり、コントローラの開発者は、このサービスの中身が完成しているかどうかを気にする必要なく自分の作業を進めることができます。
$result = app()->make('myService')->getValue();
もし、サービスプロバイダーを使わずに同じことをやりたければ、ここに上記のif文と同じように環境ごとの差を書くことになります。
もし20箇所でこのサービスクラスを呼び出しているとすると、20箇所全部に書かなかればいけません。
そもそも、この分岐って本来の実装のためというより開発のためのコードなので最終取り除いたほうがいいようなものです。
ですから、サービスプロバイダーで一括管理したほうがよいのですね。
環境でまるっと切り替える考え方はGOFのStrategyパターンになります。
補足
ちなみに上の書き方だとモックが入る可能性があるというのがコントローラからわかりにくいので、普通はインターフェースを挟んで以下のように書きます。動作は同じ。
namespace App\Services;
interface MyServiceInterface
{
public function getValue();
}
namespace App\Services;
class MyService implements MyServiceInterface
{
public function getValue()
{
$result = // (中略) 色々難しかったり長い処理の結果がここに入る。
return $result;
}
}
namespace App\Services;
class MyMockService implements MyServiceInterface
{
public function getValue()
{
return 'これはダミーです。'; // 開発用の適当なハリボテの値
}
}
use App\Services\MyServiceInterface;
use App\Services\MyService;
use App\Services\MyMockService;
public function register()
{
$this->app->singleton(MyServiceInterface::class, function ($app) {
// 環境に応じて異なるクラスをバインド
if (config('app.env') == 'local') {
return new MyMockService();
}
// 本番環境などその他の場合はMyServiceを使用
return new MyService();
});
}
namespace App\Http\Controllers;
use App\Services\MyServiceInterface;
class MyController extends Controller
{
public function show()
{
$result = app()->make(MyServiceInterface::class)->getValue();
return response()->json(['value' => $result]);
}
}
おわりに
いかがでしたか?慣れないと意味不明かもしれません。
でも使えるようになるとスッキリかけて便利ですし、ライブラリの使用法とかでやたら出てくるので簡単なのから自分で書いてみて分かる部分を増やすことでメール送信の記事とかも理解しやすくなると思います。
ちなみに最後の補足の書き方だけは似たような記事を昔読んだのですが、それだけだと難解だと思ったのでもっと噛み砕いて書いたつもりです。
みなさんの理解の助けになれば幸いです。
株式会社ONE WEDGE
【Serverlessで世の中をもっと楽しく】 ONE WEDGEはServerlessシステム開発を中核技術としてWeb系システム開発、AWS/GCPを利用した業務システム・サービス開発、PWAを用いたモバイル開発、Alexaスキル開発など、元気と技術力を武器にお客様に真摯に向き合う価値創造企業です。
Discussion