【Laravel】サービスプロバイダが遅延ロードされない罠にはまった話

意外とここらへんの処理について知っている人は多くないのではないかと思ったのでまとめました。

※ 執筆時点で一般的に使われているであろうバージョン5.1から5.3までの話とします。



# 何が起きたのか
とあるサービスプロバイダを実装していて、遅延ロードをさせたくてdeferプロパティを変えてみたが、遅延ロードしてくれなかった。



# 結論

先に結論を書いておきます。
(サービスプロバイダのことをよく知らない人とか暇な人は下のほうも見て\:pray:)

サービスプロバイダのロードでは、

- __ロードするサービスプロバイダ一覧__ と __遅延ロードさせるかどうかを分別したサービスプロバイダ一覧__ をキャッシュするようになっている。
- キャッシュ更新は、ロードするサービスプロバイダ一覧のみを比較し、差異があった場合のみ更新する。 __遅延ロードさせるかどうかは判定に関係ない。__
- キャッシュしたファイルを元に遅延ロードするもの、しないものを判定する。

そのため、例えば、ある状態でキャッシュされた後に遅延ロードのフラグを変更しても、ロードするサービスプロバイダ一覧に変更がなければ、それはキャッシュに反映されません。これが今回はまった原因です。

解決策として、`php artisan clear-compiled`などでキャッシュを削除すれば良いです。

以上です。

ちなみにここらへんの処理はRegisterProvidersクラスで実装されているので、気になった方は一度見てみると良いです。



# サービスプロバイダやそれに関係あること

ここでは上記の結論に辿りつくために必要な知識などを書いていきます。



## サービスプロバイダとは

サービスプロバイダとは、Laravelアプリケーションが起動する時の初期処理として実行される処理を記述したクラスのことを言います。例えばサービスコンテナの結合処理はこのサービスプロバイダで処理されます。

サービスプロバイダでサービスコンテナの結合処理をする場合、registerメソッド内で定義する必要があり、通常はこの初期処理でregisterメソッドが呼び出されて結合処理が行われます。しかし初期処理で結合処理をさせる必要がない場合は遅延ロードフラグのdeferプロパティを切り替えることで結合処理を遅らせることができます。これを遅延プロバイダと言い、この設定を適切に行うことで無駄なロードを避けることができ、アプリケーションのパフォーマンスを向上させることができます。

今回の話はこのサービスコンテナの結合処理を遅らせたいときの処理まわりではまったことを書いています。

詳しく知りたい方はリファレンス等を参照してください。
https://laravel.com/docs/5.3/providers



## サービスプロバイダのロードまでの流れ

スタート地点は`app/Http/Kernel.php`や`app/Console/Kernel.php`の親クラスにあるhandleメソッド(オーバーライドしていないと仮定)です。ここで様々なメソッドが呼ばれた後、bootstrapメソッドで`Illuminate/Foundation/Application.php`のbootstrapWithメソッドがbootstrappersプロパティを引数として呼び出されます。このプロパティに含まれる`Illuminate\Foundation\Bootstrap\RegisterProviders`がサービスプロバイダのロードに関係があります。

その後`Illuminate/Foundation/Application.php`のregisterConfiguredProvidersメソッドでProviderRepositoryクラスがインスタンス化され、loadメソッドがロードするサービスプロバイダ一覧を引数として呼び出されます。ちなみにこの一覧は`config/app.php`のproviders配列で確認することができます。

loadメソッドではロードするサービスプロバイダ一覧が渡ってきますが、これを元に __ロードするサービスプロバイダ一覧__ と __遅延ロードさせるかどうかを分別したサービスプロバイダ一覧__ をキャッシュするようになっています。また、既にキャッシュが存在する場合は、そのキャッシュとの差異を確認し、差異があればキャッシュ更新が行われるようになっています。

キャッシュとの差異は、以下のような方法で確認しています。

```php
public function shouldRecompile($manifest, $providers)
{
    return is_null($manifest) || $manifest['providers'] != $providers;
}
```

配列の比較です。これはロードするサービスプロバイダ一覧のみを比較しており、遅延ロードさせるかどうかは比較対象に入っていません。ですのでキャッシュが存在し、遅延ロードのフラグのみを変更した場合、キャッシュ更新は行われずに既に存在するキャッシュが使われてしまいます。

ちなみにここの処理では配列を`!=`で比較をしていますが、`!=`と`!==`で動作が異なるので、各自確認しておいたほうが良いです。

```php
<?php
$a = [0 => 'x', 1 => 'y', 2 => 'z'];
$b = [1 => 'y', 2 => 'z', 0 => 'x'];
var_dump($a != $b);  // bool(false)
var_dump($a !== $b); // bool(true)
```

キャッシュまわりの処理の後、通常のサービスプロバイダはその場でロードされます。また、遅延プロバイダはdeferredServicesプロパティに追加されます。ここで追加されたプロバイダは必要となったタイミングでロードされるようになります。



## Artisanコマンド (clear-compiled)

サービスプロバイダのキャッシュまわりの仕様により、今回のような場合はキャッシュを削除する必要があります。直接`rm`で削除するのも良いですが、Artisanコマンドにはclear-compiledがあるのでこっちを使います。

このコマンドは`Illuminate/Foundation/Console/ClearCompiledCommand.php`で実装されています。



## おまけ (services.phpとservices.json)

これまで話していたキャッシュは`app/bootstrap/cache/`の下に保存されますが、そのファイル名がバージョン5.2の途中くらいで`services.json`から`services.php`に変わっているのにお気づきでしょうか。(私はバージョン5.1を使うことが多いので、この変化には最近気づきました。)

気になって履歴を調べてみると、この変更は速度改善のためらしく、`json_encode`、`json_decode`から`var_export`を使うように変更されていました。

https://github.com/laravel/framework/commit/8c67e93f21dc5f253f85c85f729314abbda6b340

調べてみてなるほどなという気持ちになったので、おまけとして載せておきます。