読者です 読者をやめる 読者になる 読者になる

Silexのコントローラ遅延読み込み時のサービス読み込みについて(ServiceControllerServiceProvider)

前回ServiceControllerServiceProviderで、ちゃんとコントローラの読み込み遅延されるね。という確認をしたので今回はサービスの読み込みもちゃんと遅延するね。という確認をします。

コードはmatsu-chara/SilexLazyControlerLoadSample2 · GitHubに有ります。

前回に引き続きコントローラサービスの登録などが面倒なのでAPIのひな形としてsilex-simple-restを使用してます。

調べ方

前回と同じくdummy, dummy2, api/v1/dummy, api/v1/dummy2というURLを持つ4つのAPIを用意。全部("dummy" or "dummy2"という文字列を返すだけのAPI)

今回は、 dummy, api/v1/dummyのコントローラクラスをインスタンス化するときにそれぞれbaseService、baseServiceForLazyを渡す仕様にしました。

    //dummy APIの場合
    public function __construct(Logger $monolog, BaseService $baseService)
    {
        $this->monolog = $monolog;
        $this->monolog->addInfo("dummy constructed");
    }

一方dummy2/, api/v1/dummy2の方はbaseServiceは渡さず、前回と同じ仕様にしました。(依存サービスはmonologだけ)

    //dummy2 APIの場合
    public function __construct(Logger $monolog)
    {
        $this->monolog = $monolog;
        $this->monolog->addInfo("dummy constructed");
    }

dummy/, dummy2/のコントローラはsrc/app.php

$app->mount('/dummy', new App\Controllers\DummyControllerProvider($app['monolog'], $app['baseService']));
$app->mount('/dummy2', new App\Controllers\Dummy2ControllerProvider($app['monolog']));

と登録。

api/v1/dummy/とapi/v1/dummy2のコントローラはsrc/App/Controllers/RoutesLoader.php

$api->get('/dummy', "lazyDummy.controller:getDummy");
$api->get('/dummy2', "lazyDummy2.controller:getDummy");

のように登録。(ルートマッチ後にコントローラのインスタンスが生成される。) *1

結果

/*
 * API:dummy/
 */
[2014-01-12 04:32:40] application.INFO: baseService constructed [] []
[2014-01-12 04:32:40] application.INFO: Dummy constructed [] []
[2014-01-12 04:32:40] application.INFO: Dummy2 constructed [] []
[2014-01-12 04:32:40] application.INFO: Matched route "_dummyGET_" (parameters: "_controller": "{}", "_route": "_dummyGET_") [] []
[2014-01-12 04:32:40] application.INFO: > GET /dummy/ [] []
[2014-01-12 04:32:40] application.INFO: < 200 [] []

/*
 * API:dummy2/
 * baseServiceはdummy2に必要ないが、dummyがインスタンス化されてしまう関係でbaseServiceまで一緒にインスタンス化されてしまう。
 */
[2014-01-12 04:32:45] application.INFO: baseService constructed [] []
[2014-01-12 04:32:45] application.INFO: Dummy constructed [] []
[2014-01-12 04:32:45] application.INFO: Dummy2 constructed [] []
[2014-01-12 04:32:45] application.INFO: Matched route "_dummy2GET_" (parameters: "_controller": "{}", "_route": "_dummy2GET_") [] []
[2014-01-12 04:32:45] application.INFO: > GET /dummy2/ [] []
[2014-01-12 04:32:45] application.INFO: < 200 [] []

/*
 * API:api/v1/dummy
 * baseServiceForLazyが必要なのでインスタンス化される。
 * baseServiceは必要ないが、dummyが無駄にインスタンス化されるので、インスタンスが生成されてしまう。
 */
[2014-01-12 04:32:52] application.INFO: baseService constructed [] []
[2014-01-12 04:32:52] application.INFO: Dummy constructed [] []
[2014-01-12 04:32:52] application.INFO: Dummy2 constructed [] []
[2014-01-12 04:32:52] application.INFO: Matched route "_api_v1GET_dummy" (parameters: "_controller": "lazyDummy.controller:getDummy", "_route": "_api_v1GET_dummy") [] []
[2014-01-12 04:32:52] application.INFO: > GET /api/v1/dummy [] []
[2014-01-12 04:32:52] application.INFO: baseServiceForLazy constructed [] []
[2014-01-12 04:32:52] application.INFO: lazyDummy constructed [] []
[2014-01-12 04:32:52] application.INFO: getDummy [] []
[2014-01-12 04:32:52] application.INFO: < 200 [] []

/*
 * API:api/v1/dummy2
 * baseServiceForLazyは不要なのでインスタンス化されない。
 */
[2014-01-12 04:32:56] application.INFO: baseService constructed [] []
[2014-01-12 04:32:56] application.INFO: Dummy constructed [] []
[2014-01-12 04:32:56] application.INFO: Dummy2 constructed [] []
[2014-01-12 04:32:56] application.INFO: Matched route "_api_v1GET_dummy2" (parameters: "_controller": "lazyDummy2.controller:getDummy", "_route": "_api_v1GET_dummy2") [] []
[2014-01-12 04:32:56] application.INFO: > GET /api/v1/dummy2 [] []
[2014-01-12 04:32:56] application.INFO: lazyDummy2 constructed [] []
[2014-01-12 04:32:56] application.INFO: getDummy [] []
[2014-01-12 04:32:57] application.INFO: < 200 [] []

結局$app->share(function(){~~});で登録したサービスはコントローラのインスタンス生成の際に一緒に生成される(不要なサービスも含めて全部生成されてしまう)けど、ServiceControllerServiceProviderを使えばそもそも不要なコントローラのインスタンスが生成されないのでサービスの方も、不要なものは生成されないというお話。

コントローラの生成ってそんなに重いの?と思ってましたけどサービスの生成も絡んでくると、パフォーマンスに影響する可能性もありそうななさそうな感じです。

投げやりな考察

サービス無駄生成防止機能じゃなかったら何の意味があるんだ。という印象を受けますが、それなりに意味があります。 今回の例では、ServiceControllerServiceProviderを利用したLazyDummyControllerProviderを見ると、依存関係が非常に明確になっていて、簡単にインスタンスを用意することが可能(テスタブルな設計になっている)ことが分かります。*2

普通のコントローラだと何が入っているかよくわからない$appを用意しなければいけません。app.phpを呼ぶと$appが生成されて返ってくる仕様にすれば、$appを用意することは可能ではありますが、「実際に$appに何が入っているか」・「実際にコントローラで必要なサービスが何か」が明らか(かつ制御可能)でない状態は望ましい状態とは言えないと思います。

今回のデモでは分かりにくいですが公式のサンプルを見ると、もはやSilexにすら依存していないコントローラが作成可能であることが分かります。サービスコントローラの枠組みを利用してコントローラを記述することで、コントローラの依存性が非常に明確になり、全体の構成を見通せるようになるようです。一方で$app使えなくて面倒だったりします。(コントローラを$appに依存させることも可能ですが、それをやるとこの辺のメリットは得られません。)便利に速く書くか、時間を書けて堅牢に書くかを開発期間やアプリケーションの規模と相談してユーザが選べるのはマイクロフレームワークであるSilexの面白いところであり、(理解していないと意味分からん書き方になるという点で)難しいところでもあると個人的には思っています(が、もしかしたら全面的に勘違いしている可能性があります)。

ちなみに今回の話はpimpleのshareの仕様みればやらなくてもよかったんじゃないかとか、コントローラの__constructの引数でタイプヒンティングできるんだからインスタンス化されてるに決まってるだろjkとかつっこみどころがありますが、その辺は置いておいてください。

*1:lazyDummyの依存性などは同じくRoutesLoader.phpのinstantiateControllersメソッドに記述しています。

*2:無駄にuseが入っていて明確じゃないですね。消すの面倒だったんです・・・。