第2回 シングルトンはたった1行 サービスプロバイダの使いどころを知ってLaravelのサービスコンテナともっと仲良くなる


そういえばちゃんと学んだことがなかったサービスコンテナを改めて勉強してみたシリーズ第2回。前回に続いて、2回目はサービスプロバイダに触れます。

第1回 サービスコンテナ 「それは新しい new だった」
第2回 サービスプロバイダ 「シングルトンはたった1行」
第3回 結合 「なんでもツッコんで気軽に取り出す」

前提

  • 「サービスプロバイダ」ってよく聞くけど、何なのかよくわからない。
  • 公式ドキュメントの「サービスプロバイダ」を読んだけどさっぱりだった。
  • 前回を読んで、サービスプロバイダは基本的に要らないということを知った。
  • 結合ってなにそれおいしいの?
  • シングルトンってそういえば聞いたことある。
  • Class::class'Class' は同じものだって? 知ってました?!

目標

  • サービスプロバイダの使い方がわかる
  • でも使ってみるとめっちゃ便利かもしれない、と思ってもらえる

復習と課題

前回、サービスコンテナを使う最小ステップを下記のように定義してみました。

  1. サービス化したいクラスを作る
  2. サービスコンテナにインスタンスを提供してもらう

これはさすがに省略しすぎたので、このステップにもう1つだけ追加させてください。

  1. サービス化したいクラスを作る
  2. インスタンス化する方法を定義しておく
  3. サービスコンテナにインスタンスを提供してもらう

結論

サービスプロバイダは、その 「インスタンス化する方法を定義」しておくのに便利な場所 です。

解説

サービスコンテナがインスタンス化する方法

前回、 サービスコンテナは new と同じ と説明していましたが、実際にサービスコンテナがクラスのインスタンスを作るときは、内部でこのような動きをしています。

  1. クラスのコンストラクタの引数をチェックする
  2. 引数のタイプヒントにクラスが指定されていると、そのクラスをサービスコンテナで再帰的に生成する
  3. その生成されたインスタンスをコンストラクタに渡しながら、クラスを new する。

ということは、クラスのコンストラクタに引数がないと…

  1. クラスのコンストラクタの引数をチェックする
  2. クラスを new する。

new と全く同じになります。

ただ、ホントに new するだけだったら new でいいんだし、依存を解決してくれるのはすごく便利だけど、それだけだとちょっと物足りないと感じることがあります。そこで、この「インスタンスの生成方法」をカスタマイズするための仕組みが 「結合 binding」 です。

 結合=インスタンス化する方法。

この結合、ただでさえちょっとややこしいのに、公式他のドキュメントではいきなりこの「結合」の説明から入るのでしばらくわからない話が続くことになります。だから今回は1つだけ。

シングルトンモード

app()->singleton('App\SampleClass');

サービスコンテナにApp\SampleClassを生成するときはシングルトンとして生成しなさい」と指示するメソッドです。

こうしておくと、サービスコンテナは、最初の app('App\SampleClass') で生成されたインスタンスをストックしておき、2回目以降はそのインスタンスを渡してくれます。アプリケーション中で何回 app('App\SampleClass') しても、生成されるインスタンスは絶対に1つだけ。まさにシングルトン。
必要なのは、ただこの1行だけ。クラスの定義も、その呼び出し元も、全くそれと意識せず(特別なコードを1行も書かず)シングルトンを実現できてしまいます。これはすごい。

ところで、この1行、どこに書くの?

サービスプロバイダ

正解は「どこにでも書ける」です。ぶっちゃけ初めて app('App\SampleClass') する直前の行でも良いんです。

でも「できればここに書いておくことをオススメするよ!」というのが、サービスプロバイダ。満を持しての登場です。

さらに。これも誤解を恐れず言い切ります。
とりあえず手軽に結合を書いておくだけなら、AppServiceProviderを使います。もともとこのAppServiceProviderはそういう目的で設置しておいてくれているものなんだそうです(デフォルトでは空っぽです)。

App/Providers/AppServiceProvider.php
namespace App\Providers;

use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('App\SampleClass'); // もちろん $this-> ではなく app()->singleton としても同じことです($this->のほうがちょっとだけ早い)
    }
}

新しいServiceProviderを用意するのは、ある程度クラスが増えてきて、それをまとめたくなってきてから使えば大丈夫と思います。作り方は難しくないので、使ってみたくなってきたときに、公式ドキュメントを参照していただければと思います。

まとめ

シングルトンの使いどころは?
こちらにも書きましたが、シングルトンを作ろうと思うと、ちょっとひと手間掛かります。クラスに専用のメソッドを用意して、呼ぶ側でそれを呼び出して…とするのが王道ですが、Laravelではそんな必要ありません。

  1. クラスをふつうに作る
  2. AppServiceProviderで、app()->singleton('Classname') としておく
  3. app('Classname') でインスタンスをもらう

これだけの手順でクラスがシングルトンとして手に入ります。
超カンタン♬

補足 サービスプロバイダとは何なのか?

ちょっとまてカンタンすぎるぞ!
そうです、すみません。確かにちょっと端折りすぎました。

サービスプロバイダとは何なのか? それにもう少し具体的に答えようとすると、それは「クラスインスタンスを初期化するための場所」だと思います。

アプリケーションの初期化を行うための仕組みや方法はいろいろ用意されていますが、「特定のクラスが使う初期値」や「特定のクラスが使う別のクラスの初期化」などは、クラス定数やクラスのコンストラクタに書くことが多かったように思います。だって、コンストラクタって、クラスインスタンスを初期化するためのものって聞いていたから…。

でも、これがクラスの環境依存度を上げ、移管、移植、テスト、変更のしにくいバッドプラクティスなクラスにしてしまいます。しかも、初期化方法や依存関係がアチコチに書かれることになって、メンテナンス性もひどいことに…。

それを解決するために、「クラスインスタンスを初期化する方法」や「クラスインスタンスの依存関係」を書いておくための場所が、サービスプロバイダ。
まとめるとこのような感じです。

初期化すること 書く場所 サービスプロバイダの出番
実行環境によって変わる初期値
ベースドメインや
DBサーバーのIPなど
.env
アプリケーション実行中はどこでも変わらない定数
都道府県IDや
特定項目の選択肢など
/config/xxx.php
特定のクラスが使う定数
外部APIのエンドポイントやTOKEN
①クラス定数
②.env に書いて、クラスのコンストラクタで参照
.envに書いてサービスプロバイダで与える
特定のクラスが使う別クラスインスタンス
クロウラーが依存しているHTTPモジュールなど
(特定クラスのコンストラクタ…?) サービスプロバイダ
アプリケーション全体で使うインスタンス
サービスクラス
シングルトン
(ドコ…?) サービスプロバイダ
実行しているユーザーによって変わる設定
好みの画面サイズとか
DBに保存しておいてログイン時にセッションに…

補足2 結合は何を結合しているの?

結合と言うんだから、何かと何かを結びつけているわけですが、今回は全く触れていないのでちょっと意味がわからないかもしれません……。

結合が結びつけているのは、abstruct と concrete です。「抽象」と「具象」。はい、まったくわかりません。なんでこんな言い方をするかというと、いろんなものを結びつけるからです。

例えば

  • クラス名と、そのクラス※
  • クラス名と、それはさておき実際にインスタンス化するクラス(ぜんぜん違うクラスでもいい)
  • クラス名と、それはさておき渡すインスタンス(ぜんぜん違うクラスでもいい)
  • インターフェース名と、実際にそれをインプリメントしたクラス
  • 適当な名前と、インスタンス化されたクラスオブジェクト
  • 適当なサービス名と、シングルトン

今回のシングルトンは、結合するAとBを指定したんじゃなくて、その結合方法を指定していました。なので singleton は正確には「結合」というより「結合の別バージョン」。何を結合していたかというと、※の「クラス名と、そのクラス」です。

次回予告 結合を理解する

というわけで、その「結合」。
サービスコンテナの基礎と、サービスプロバイダの役割がわかってきたところで、その本当のチカラに迫っていきます。

と言いたいところですが、文量と時間の都合でまた次回。

第1回 サービスコンテナ 「それは新しい new だった」
第2回 サービスプロバイダ 「シングルトンはたった1行」
第3回 結合 「なんでもツッコんで気軽に取り出す」