157
102

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

LaravelAdvent Calendar 2019

Day 16

第3回 なんでもツッコんで気軽に取り出すLaravelサービスコンテナの核心「結合」

Last updated at Posted at 2019-03-20

3.jpg

そういえばちゃんと学んだことがなかったサービスコンテナを改めて勉強してみたシリーズ第3回。今回はいよいよサービスコンテナの核心「結合」に触れて、サービスコンテナに何でもかんでもツッコめるようになっていきたいと思います。

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

目標

  • サービスコンテナに「サービス」を「コンテナ」してもらう方法を知る
  • 気軽に「コンテナ」を使えるようになって出し入れに自由を!

復習と課題

第1回でサービスコンテナは newしてくれる君 だと学びました。第2回ではその newする方法にはいろいろあって サービスプロバイダで定義すればいいらしい、ということに触れました。

その、いろいろあるらしい「newする方法」ってどんなのがあるの?
どうやってそれを指示するの?

今回はそれを見ていきます。

結論

結合の基本形はこれです。

app()->bind( $abstract, $concrete );

書く場所は第2回でも触れたとおりどこでもいいのですが、とりあえず、AppServiceProvider に書いておきましょう。

app/Providers/AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        // こっちでもいいけど
        // app()->bind( $abstract, $concrete );
    }

    public function register()
    {
        // こっち
        app()->bind( $abstract, $concrete );
    }
}

#さらっと boot() が出てきましたが、boot と register の違いは実行されるタイミングで、先に register が実行されて、すべての ServiceProvider が register し終わった後に、boot が実行されます。なので、他の Service のインスタンスを使いたい場合は boot に、そうじゃない独立した初期化は register に書きます。

解説

さぁ実行だ! といってももちろんエラーです。この $abstract$concrete が未定義です。コイツラはいったいナニモノなのでしょうか? そもそも英単語がわからない。 abstract と concrete じゃ訳すのも大変です。もう少しわかりやすい名前にしてみましょうか。

app()->bind( $label, $service );

あー。イメージできました。最初からこうしてくれれば良いものを。
さて、ちょっと単純化しすぎている感はありますが、しばしこの前提で説明を進めます。

4.jpg

抽象 abstract

ラベルです。ちょうど衣装ケースに「夏物」とラベルをするように、サービスコンテナに入れるものにはそれぞれちゃんとラベルを貼ります。ラベルなので、文字列です。1つのアプリ内で同じラベルのものは1つしか入れられません。つまり、

  • 文字列
  • ユニーク

であれば何でも良い、ということです。

こんなイメージです。

// 預ける
app()->bind('date', $concrete1);
// 取り出す
app('date'); // ==> concrete1

// ラベルは何でも良いよ
app()->bind('App\User', $concrete2);
app('App\User'); // ==> concrete2

// もちろんクラス名でもね
app()->bind( MyClass::class, $concrete3 );
app( MyClass::class ); // ==> concrete3

// 同じラベルを再定義すると、先に入っていたものは破棄されて置き換わります
app()->bind( MyClass::class, $concrete4 );
app( MyClass::class ); // ==> concrete4

第2回のsingleton結合ではクラス名を使用していましたが、あれは「便宜上」「そうしたほうがわかりやすいので」そうしているだけで、実はクラス名である必要はなかった、ということになります。

具象 concrete

さて、上のサンプルではあえて $concrete と謎のままでしたが、これにはいったい何が入っているのでしょうか? これもとてもシンプルで、次の2つのいずれかです。

  • 文字列(クラス名)
  • クロージャー

文字列(クラス名)

具象の1つはクラス名です。クラス名が指定されていると、サービスコンテナはそのクラスを new してインスタンスを返してくれます。このとき、コンストラクタに引数があると型を調べて自動的に入れてくれる、というのは第1回で触れた機能です。

// 預ける
app()->bind('StdClass', 'StdClass');
// 取り出す
$a = app('StdClass');
var_dump($a);
//>>> class stdClass#2911 (0) {
//>>> }

毎回初期化される

ポイントは取り出すたびに new していることです。
得られるのは毎回初期化された別のオブジェクト。
試してみましょう。

app()->bind('StdClass', 'StdClass');

// インスタンスのIDを調べると…
$a = app('StdClass'); spl_object_hash($a);
//>>> "000000007dc70f4a0000000054e8bfb6"
$a = app('StdClass'); spl_object_hash($a);
//>>> "000000007dc70f7e0000000054e8bfb6"
                    ^^^ 違うオブジェクト

具象を省略すると

また、具象を省略すると、抽象と同じ文字列が使用されます。
つまり、抽象にクラス名を指定して、それと同じクラスを結合する場合は省略可能です。

// この2つは同じ
app()->bind('StdClass', 'StdClass');
app()->bind('StdClass');

さらに言うと実は、app()->bind('StdClass'); これだけなら、この文自体が省略可能です。単にクラス名を指定してそれを new してもらうだけなら、結合を明記しなくても最初からやってくれるというのは、第1回で見てきたとおりです。

第2回のシングルトン結合の復習

さぁ思い出してください。第2回のシングルトンはこう書きました。

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

これは具象が省略されていたんですね。つまり

// この2つは同じ
app()->singleton('App\MyClass');
app()->singleton('App\MyClass','App\MyClass');

さらに、実はこの singleton は中で bind に置き換えられています。つまり、

// この2つは同じ
app()->singleton('App\MyClass','App\MyClass');
app()->bind     ('App\MyClass','App\MyClass', true);

この bind の第3引数はsharedモードで、先程「取り出すたびにnewする」と説明していたところが「newするのは最初の1回だけで、次からは生成済のインスタンスを返す」と変わります。まさに、シングルトンですね。試してみましょう。

app()->bind('std','stdClass',true);

// インスタンスのIDを調べると…
$s = app('std'); spl_object_hash($s);
//>>> "000000002fa3c3390000000018649e8a"
$s = app('std'); spl_object_hash($s);
//>>> "000000002fa3c3390000000018649e8a" 同じ!

// ということは、オブジェクトを変更して
$s->sample = "sample";
// 別のところで再取得すると
$s2 = app('std'); 
echo $s2->sample;
//>>> "sample"  反映されている!

クロージャー

文字列はとても手軽で、さらにコンストラクタの引数が自動代入される機能まで備えていてパワフルですが、逆に具象はふわふわして流動的だし、抽象はクラス名から逃れにくくて、あまり抽象度が高くありません。そこでクロージャー。ルールはとてもシンプルで

  • 具象を返す

だけ。

例えば

// DateTime というクラス名で、そのインスタンスを得る ※1
app()->bind( 'DateTime', function(){ return new DateTime; } );
echo app( 'DateTime' )->format('Y-m-d');
//>>> 2019-03-20

// 抽象は別にクラス名じゃなくてもいいし、
// 具象はもっと具体的に初期化されていてもいい
app()->bind( 'birthday', function(){ return new DateTime('1991/04/29'); } );
echo app( 'birthday' )->format('Y-m-d');
//>>> 1991-04-29

// fourty-two というラベルで、プリミティブ値 42 を得る
app()->bind( 'fourty-two', function(){ return 42; } );
echo app( 'fourty-two' );
//>>> 42

// 生成済のインスタンスを預けてみる ※2
$instance = new DateTime('2019/02/10');
app()->bind( 'some-date', function() use ($instance) { return $instance; } );
echo app( 'some-date' )->format('Y-m-d');
//>>> 2019-02-10

// ----------------------------------------------------
// ※1 DateTimeクラスは文字列での結合ができません
// app()->bind( 'DateTime', 'DateTime' ); // NG
// なぜならコンストラクタの引数にタイプヒントがなくてサービスコンテナには特定できないから。

// ※2 生成済インスタンスを預けるには instance という別の結合メソッドが便利です。
// 以下2つは同じ結果。
// app()->bind( 'some-date', function() use ($instance) { return $instance; } );
// app()->instance( 'some-date', $instance );

怖ろしいことに「具象」とは、クロージャーが返せるものだったら何でもオッケー!なのです。

ありとあらゆるものがツッコめて、文字列1つでどこからでも取り出せる。
それがサービスコンテナだったんですね。

まとめ

サービスコンテナは「コンテナ」というだけあって、何かを預かってくれて、必要なときに取り出すための仕組みです。その「コンテナ」。なんだか難しそうだと思っていたのに、出し入れするのに必要なのはこれだけです。

// 入れる
app()->bind( $label, 'ClassName' );
app()->bind( $label, function(){ return $anything; } );

// 出す
app( $label );

なんてこった!

こんなに何でもかんでもツッコんでおけるのか?
こんなに気軽に取り出せるのか?!

そんな風に思っていただけたなら、この上ない喜びです。

補足:つかいどころ

ちょっとまてカンタンすぎるぞ!

ええええ。今回はちょっと頑張って説明してみたつもりだったんですが…。文章量も多いでしょ!

うーむ。そうですね。じゃあ結合の具体例をいくつか見てみましょうか。実際にプロジェクトで使用したものです。解説は最小限ですので「こんな使い方もあるのかー」というのを感じていただければ幸いです。

固定値で初期化しておく

例えばRedisのクライアントクラスなど。サーバー情報など特定の設定値で初期化するクラスに、その設定値を指定して、シングルトンとして定義しておきます。config()ヘルパと一緒に使うのが常套手段です。※env()は使用しないように。理由はこちら

AppServiceProvider.php
$this->app->singleton(\Predis\Client::class, function ($app) {
    return new \Predis\Client([
        'scheme' => config('database.redis.default.scheme', 'tcp'),
        'host'   => config('database.redis.default.host', '127.0.0.1'),
        'port'   => config('database.redis.default.port', 6379),
    ], [
        'parameters' => [
            'password' => config('database.redis.default.password', null),
            'database' => config('database.redis.default.database', 0),
        ],
    ]);
});

こうすると、アプリケーションの他の場所では

$redisClient = app( \Predis\Client::class );

とすれば、どの様に初期化されているか気にせず、キチンと初期化された状態で手に入ります。

まさにServiceをProvideしている好例です。
こういうのが最もよく見かける使い方じゃないかなーと思います。

インターフェースから実装クラスを得る

上級者向けのお話ですが、クラスはインターフェースと実装に分けたほうが良いそうで、機能を使いたいクラスからは提供側のinterfaceを参照することになっています。で、実際に動かすときに、そのインターフェースを実装したクラスを注入するわけですが、誰が?いつ?どこからどうやって注入するの?

それ、結合をキチンと定義しておくと、Laravelさんが自動的にやってくれます。

AppServiceProvider.php
// インターフェースに対応した実装をバインド
$this->app->singleton(\App\ProductInterface::class, \App\Product::class);
$this->app->bind(\App\SearchConditionInterface::class, \App\SearchCondition::class);

機能を使う側のクラスはこう書きます。

MyClass.php
class MyClass 
{
  // コンストラクタインジェクション!
  public function __construct( \App\ProductInterface $productService )
  {
      $productService->... // \App\Product のインスタンスが注入される

newせずにインスタンスを初期化する

クラスのインスタンスはいつも必ずnewから始まるわけではありません。例えばセッションやファイルにシリアライズされて保存されていたインスタンスがあった場合。あった場合はそれを使うし、ない場合もあるのでそのときはnewしましょ。そんなテクニックも使ったことがあります。

AppServiceProvider.php
// セッションに保存されていたシングルトンを復旧
$this->app->singleton(\App\SessionStore::class, function ($app) {
    $obj = session('_SESSION_STORE', null);
    return $obj ?? new \App\SessionStore();
});

機能を差し替える

Laravelアプリケーションは最初からいくつかのインスタンスがサービスコンテナに入っています。先程「同じラベルのものを再定義すると置き換わる」と書きましたが、つまり置き換えちゃうとアプリケーション全体の機能をそっくり別のものに置き換えることができます。

例えば下記は、Bladeコンパイラを独自拡張したものに置き換えた例。Laravelは「blade.compiler」というラベルでBladeコンパイラのインスタンスを保管しているので、それを上書きして差し替えています。

AppServiceProvider.php
// Bladeコンパイラを独自拡張したものに差し替え
$this->app->singleton('blade.compiler', function () {
    return new \App\Extentions\ExtendedBladeCompiler(
        $this->app['files'], $this->app['config']['view.compiled']
    );
});

このテクニックを利用してconfigヘルパの機能を変更する例を「未定義を例外にするconfig関数を作る」に書いています。

次回予告 Laravel解体新書

いつもご愛読ありがとうございます。最近、Googleさんの琴線に触れたのか、書いた当初はさほどでもなかったサービスコンテナ関連の記事に毎日多くの方に「いいね!」をいただいていて、それなのに実に中途半端なところで止まっていたので申し訳なく、今回ようやく、時間を作ってまとめることができました。

今回はサービスコンテナは「触り」ではなく「核心」に近く、よりその凄さを感じていただけたのではと期待していますが、いかがでしょうか?

わかりにくいよ!(^^)
ていうか間違ってるよ!(^^)

とお感じになられた際はぜひ容赦なくコメントいただけると幸いです。

次回は、今回まとめようとしてまとめきれなかった、Laravelの標準機能のお話か、サービスコンテナをより使いやすくするエイリアスのお話が書ければ、と思っています。

読んでみたい!という方は、いやそうじゃなくても、「いいね」してもらえるとモチベーションが上がります。
よろしくおねがいします。

こんな記事も書いています

Laravelのちょっとマニアックな視点から、誰も書かない記事を書いています(笑
合わせてご覧いただけると幸いです(^^)

157
102
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
157
102

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?