laravelのサービスコンテナの挙動に関するメモ

# 初めに

この記事は、laravelのサービスコンテナの挙動について説明します。
コードはLaravel 6.2(PHP 7.2.24)で動作を確認しています。

# サービスコンテナに値の生成方法を登録する

サービスコンテナの主要な役割の一つが、ある値の生成方法を登録することです。
bind()関数を用いてキーとなる文字列とその生成方法をサービスコンテナに登録することができます。

```
>>> app()->bind('rand', function() {return rand();})
=> null
```

make()関数を用いることで、登録した生成方法を用いて値を生成できます。

```
>>> app()->make('rand')
=> 2114203198
>>> app()->make('rand')
=> 576409439
```

makeには第二引数としてパラメタを渡すことができます。
パラメタはarrayである必要があります。
渡したパラメタはbindの第二引数の第二引数として渡されます。
（ちなみに第一引数`$app`にはサービスコンテナ自身が渡されます。）

```
>>> app()->bind('p', function($app, $param) {dump($param);return 1;})
[]
=> null
>>> app()->make('p', ['hoge' => 'fuga'])
array:1 [
  "hoge" => "fuga"
]
=> 1
```
makeWithという関数もありますが、これはmakeと同じ動作をします。 [^1]

[^1]: https://github.com/laravel/framework/blob/v6.2.0/src/Illuminate/Container/Container.php#L614-L617

```
>>> app()->makeWith('rand')
=> 449067795
>>> app()->makeWith('p', ['hoge' => 'fuga'])
array:1 [
  "hoge" => "fuga"
]
=> 1
```


bindの第二引数に文字列を指定することもできます [^4]

[^4]: https://github.com/laravel/framework/blob/v6.2.0/src/Illuminate/Container/Container.php#L237-L239

```
>>> app()->bind('another_rand', 'rand')
```


これは以下の書き方とほぼ同じ挙動をします

```
app()->bind('another_rand', function($app, $param){$app->make('rand', $param);})
```

値を何度も生成したくない場合はsingleton関数を使うことができます。
これを使うと値は一度だけ生成されてキャッシュされ、二回目以降はキャッシュされた値が返されます。

```
>>> app()->singleton('singleton_rand', function() {return rand();})
=> null
>>> app()->make('singleton_rand')
=> 259982949
>>> app()->make('singleton_rand')
=> 259982949
>>> app()->make('singleton_rand')
=> 259982949
```

# サービスコンテナを使ったインスタンス作成

サービスコンテナの主要な役割のもう一つが、サービスコンテナを用いてインスタンスを作成する際にクラスの依存関係を自動的に解決することです。
そのあたりの挙動についてみていきます。

makeに登録されていない文字列が与えられた場合、サービスコンテナはそれをクラス名とみなしてそれをnewしようと試みます。

```
>>> class Hoge{function __construct(){echo 'Hoge constructed';}}
>>> app()->make(Hoge::class)
Hoge constructed
=> Hoge {#2999}
```

この際newしようとしたクラスのコンストラクタに引数が必要だった場合、サービスコンテナは以下のルールにのっとりその引数の解決を試みます。（上の条件が優先）[^3]
[^3]: https://github.com/laravel/framework/blob/v6.2.0/src/Illuminate/Container/Container.php#L855-L875
1. makeのパラメタで引数名が指定されていればその値を使う
2. 引数にクラスが定義されていればそのクラスをサービスコンテナを用いて生成する。
3. 定義されていなければその引数名をキーとしてサービスコンテナを用いてその値を生成する（この場合コンテキストによる結合のみが参照される）

順番に詳しく見ていきます。

もっとも直接的な指定としてmakeのパラメタを通じてコンストラクタの引数に与える値を指定できます

```
>>> class Hoge2{function __construct($i){echo "i=$i";}}
>>> app()->make(Hoge2::class, ['i' => 1]);
i=1
=> Hoge2 {#3012}
```

makeのパラメタで値が指定されていない場合、コンストラクタの引数のクラスを見てそのクラスをサービスコンテナを用いて生成します。

```
>>> class Hoge3{}
>>> app()->bind(Hoge3::class, function() {echo 'Hoge3 is constructed by service container!'; return new Hoge3();})
>>> class Hoge4{function __construct(Hoge3 $hoge3){}}
>>> app()->make(Hoge4::class)
Hoge3 is constructed by service container!
=> Hoge4 {#3011}
```

もし、クラスによって注入するクラスを分けたい場合、コンテクストによる結合を行うことができます。
これはwhen, needs, give関数を使って以下のように書けます

```
>>> class Hoge5{}
>>> class ExtendedHoge5 extends Hoge5{}
>>> class Fuga{function __construct(Hoge5 $hoge){echo get_class($hoge) . ' given';}}
>>> class Fuga2{function __construct(Hoge5 $hoge){echo get_class($hoge) . ' given';}}
>>> app()->when(Fuga2::class)->needs(Hoge5::class)->give(function($app) {return $app->make(ExtendedHoge5::class);})
>>> app()->make(Fuga::class)
Hoge5 given
=> Fuga {#3048}
>>> app()->make(Fuga2::class)
ExtendedHoge5 given
=> Fuga2 {#3015}
```

このようにコンテキストによる結合を指定したFuga2だけHoge5ではなくExtendedHoge5が注入されています。

クラス名が指定されていない場合変数名をキーにして値のサービスコンテナによる生成が試みられます。
この場合コンテキストによる結合のみが参照されます。

```
>>> class Hoge6{function __construct($i){echo $i;}}
>>> app()->make(Hoge6::class)
Illuminate/Contracts/Container/BindingResolutionException with message 'Unresolvable dependency resolving [Parameter #0 [ <required> $i ]] in class Hoge6'
>>> app()->bind('$i', function(){return 1;})
=> null
>>> app()->make(Hoge6::class)
Illuminate/Contracts/Container/BindingResolutionException with message 'Unresolvable dependency resolving [Parameter #0 [ <required> $i ]] in class Hoge6'
>>> app()->when(Hoge6::class)->needs('$i')->give(1);
=> null
>>> app()->make(Hoge6::class)
1
=> Hoge6 {#3026}
```