11
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

Organization

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

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

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

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

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

<?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_encodejson_decodeからvar_exportを使うように変更されていました。

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
11
Help us understand the problem. What are the problem?