お問合わせ開発ブログ株式会社Nextatのスタッフがお送りする技術コラムメインのブログ。11:20LaravelのControllerのライフサイクルとサービスコンテナへの束縛登録のベタープラクティスこんにちは、ナカエです。Qiitaの13日目の記事です。昨日はういろうさんのLaravel6.x系以降のバージョニングについての解説記事でした。【Laravel】6.xからバージョンが進むのが早い理由と、バージョンアップのやり方【790日目】はじめに本記事では"Serviceクラス"という言葉をFWユーザが作成した独自クラスサービスコンテナによってControllerに注入されるクラスという程度の広い意味で用いています。"Serviceクラス”の中にHTTP層の処理が混ざっていても気にしないでください。TL;DRServiceクラスをControllerにコンストラクタ注入した場合とメソッド注入した場合では依存解決されるタイミングが大きく異なるControllerのコンストラクタのタイミングでは、HTTPリクエストから引き出したい情報(Cookie、セッションデータ、ログインユーザetc.)が不完全な状態になっていることがあるHTTPリクエストや実行時刻に依存するランタイムの情報をコンストラクタ注入するクラス設計は避けるべき。ランタイムの情報はメソッドの引数として渡すどうしても避けられない場合は、ランタイムの情報を引数としてServiceクラスを遅延生成するためのファクトリを噛ませると良い初期化時の処理とランタイムの処理を区別していこうBEAR.SundayはいいぞControllerのコンストラクタでログインユーザが取れない事案Controllerの複数のアクションでログインユーザを利用するので処理を共通化したい、というのはLaravel初学者が考えがちなことの一つだと思います。処理を共通化するならコンストラクタだな!と単純に考えた結果、下記のようなコードが生まれます。}}ログに出力される$this->userは見事nullとなります。当たり前じゃんと笑うことなかれ。今となっては3年以上前の話ですが、Laravel5.2まではこのコードでも$userが取得できていたのですよ。5.3へのアップグレードでこの暗黙の実装が変更され、現在では動的なMiddleware登録による少しトリッキーなコードを書く必要があります。5.3へのアップグレードで困った人々Taylor氏曰く、ログインユーザはControllerのメソッドに渡されるRequestから導出されるのが自然で、Requestが渡っていないコンストラクタで取得できているのがそもそもおかしかったとのこと。ごもっともではあるのですが、納得できない、スッキリしないという声をたまに聞きます。コンストラクタに処理を移しただけで挙動が変わるのは直感に反するようです。Service、Controller、Middlewareのインスタンス化とメソッド実行の順序を理解すると、この挙動の謎は全て解けます。以下、簡単に実行順序を検証してみましょう。検証時のLaravelフレームワークのバージョンは6.2です。Service、Middleware、Controllerを作成し、コンストラクタおよびメソッドにログ出力処理を埋め込みます。略}イベントリスナの設定ルートの決定とHTTPリクエストのハンドリング終了のイベントを拾うためにイベントリスナを設定します。serveなどを利用してWebサーバを立ち上げてアクセスします。ログ出力ログには下記のように出力されます。行末の括弧つきの数値は実際には出力されない識別用の番号です。今回出力したログは、Laravelアプリケーション全体のライフサイクルの中では、Kernelのブートストラップが完了した後の処理に相当します。はじめにRouterでRouteが検索され(1)、決定したRouteに応じて必要なMiddlewareの情報を収集します。Controllerのコンストラクタで動的に登録されるMiddlewareも収集対象とするためです。MyControllerが依存しているInjectedIntoConstructorServiceが、MyControllerよりも先に解決されています(2)。続いて、収集したMiddlewareは優先順位を考慮して並べ替えられ、アクションと合わせてミドルウェアパイプラインが構成されます。HTTPリクエストを構成したパイプラインに送り込んで処理します。このパイプラインの処理の中で、Middlewareのコンテナ経由のインスタンス化(4)とMiddlewareによるHTTPリクエストの処理の前半部分(5)が順次行われていきます。次にControllerのアクションが呼び出されるわけですが、メソッドを呼び出す前に引数を補完するため、ルートパラメータの解決やコンテナからの注入による依存解決が行われます(6)。Controllerアクションが返したHTTPレスポンスは、Middlewareの後半の処理を通って出力されます(11)、(12)。アプリケーションがHTTPレスポンスを返し終えた後に残るは、後始末(terminate)の処理です(14)。今回はMiddlewareをシングルトンとしてコンテナに登録していないため、handle時とは別のインスタンスが生成されています(13)。これらの中で、(2)のInjectedIntoConstructorServiceのコンストラクタだけが、MiddlewareによるHTTPリクエストの処理(5)の前に呼び出されています。Middlewareの担当する処理)がまだ行われていないことが読み取れます。Middlewareで処理された後でないと、セッションからデータを取得したり、ユーザを取得することができないということです。Middlewareを用いて設定をコンストラクタのタイミングから遅延させる方法が提示されているのはこのためです。ここまでのまとめServiceクラスをControllerにコンストラクタで注入した場合とメソッドで注入した場合では、Serviceクラスのインスタンスが依存解決されるタイミングが大きく異なります。アクションメソッドに注入していた時は正常に動いていたServiceクラスが、コンストラクタ注入に変えた途端動かなくなるということも起こり得ます。逆に言えば、Controllerのコンストラクタを起点として依存解決されるServiceクラスのコンストラクタにおいて、HTTPリクエストから導出される情報を使うことを避ければこの手の不具合は回避できます。記事の後半では、サービスコンテナによる依存解決のタイミングに起因する問題をより一般化して捉え、サービスコンテナへの束縛登録のベタープラクティスについて考察します。一般的に、Webアプリケーションの処理はHTTPリクエストに依らず行える初期化時の処理とHTTPリクエストがないと決定できないランタイムの処理に分けられます。PHPの通常の実行方式では、HTTPリクエストごとに状態を残さないことを基本とするため、リクエストごとに初期化処理とランタイムの処理が続けて行われるという特有の事情があります。このため、初期化時の処理とランタイムの処理の区別を意識するPHP開発者の割合は、他のプログラミング言語を利用する開発者のそれに比べて低い印象です。DIコンテナもHTTPリクエストごとにリセットされる前提のライブラリが多いです。リクエスト依存の、もしくはミュータブルなオブジェクトをコンテナに登録していて、2回目以降のHTTPリクエスト時に状態がらみのバグを引き起こすような経験をすることはほとんどないでしょう。我々PHP開発者は、PHPの割り切った設計によってHTTPリクエストをまたぐアプリケーションの状態に無頓着でいることが許されているとも言えます。BEAR.Sundayに見る早期束縛と遅延束縛の区別そんなPHPのFWにも例外が一部あります。本記事で例にあげるのはBEAR.Sundayです。BEAR.Sundayにおけるアプリケーションの初期化のタイミングはコンパイルタイムと呼ばれ、ランタイムとは明確に区別されます。Binding)と呼びます。Inejctorを利用して依存を解決するのが公式に推奨される方針です。ランタイムにおける遅延束縛が必要な場合は、AOPフレームワークであるRay.AOPによるアスペクト(横断的処理)によって依存を注入し解決します。この制約によって、BEAR.Sundayのユーザはコンパイルタイムとランタイムの区別を明確に意識する必要が出てきます。BEAR.Sundayは早期束縛と遅延束縛を分ける制約の他にもいくつかの制約を課すことで、依存解決されたアプリケーションのルートオブジェクトを丸ごとキャッシュし、PHPのFWでありながらHTTPリクエストごとの初期化処理を前倒しにして大幅にスキップすることを可能にしています。コンパイルタイムにより多くの処理を済ませておく本番環境向けの設定は、ランタイムのエラーの低減につながり、高速化だけでなく安全性にも貢献します。Laravelへの応用LaravelではHTTPリクエストそのものやFormRequestなど一部のランタイムに依存するクラスをサービスコンテナから解決することも可能なため、早期束縛も遅延束縛の区別なく依存解決をサービスコンテナに任せてしまうことができます。できる限り早期束縛を優先するという方針はFWやプログラミング言語の枠を超えて有用です。イミュータブルな設計を促進し、記事冒頭で例に挙げたような、FW利用者の直感と異なる不具合が起こる可能性を減らしてくれます。ランタイムの情報をコンストラクタの引数に取らないHTTPリクエストや実行時刻なしには定まらないランタイムの情報(特にセッションデータ、ログインユーザなど)をコンストラクタの引数に取らないのがまず一点目です。Controllerに引数として渡ってくるRequestクラスから、必要な情報を取り出し、Serviceクラスのメソッドに渡すようにしましょう。Request依存のため、Auto-wiringによるServiceの注入がやりづらい。}}ランタイムの情報を引数とするファクトリを間接層として使うどうしてもコンストラクタでランタイムの情報を受けるに準ずる設計をしたい場合は、ランタイムの情報を引数としてクラスを生成するファクトリを間接層として用意します。}}利用したいランタイムの情報がUserクラスの場合は、Userの取得を遅延できる既存のGuardやAuthManagerを利用するのも一つの選択肢です。Binding)を活用する同じIntefaceに対する異なる実装を使い分けるため、Controllerなどで動的に依存を修正するケースも見受けられますが、束縛の登録がServiceProvider以外に散らばると処理を追いづらい場合も多いです。}}コンテキストによる束縛を利用することで、ServiceProvider内で束縛を完結させることができる場合もあります。FormRequestってどう?FormRequestはRequestを継承したランタイムに依存するクラスですが、FormRequestをControllerにコンストラクタ注入しようとする人はそうそういないでしょう。Controllerアクションごとのバリデーションを行うことを前提とした設計は、自然とメソッドへの注入を選択させます。間違った使い方をしにくいという点において優れていると言えますが、FormRequestServiceProviderを見るとそのしわ寄せに依存解決の挙動が少し複雑になっていることがうかがえます。最後に色々と書きたいことが多く長文になりました。要は、Controllerのコンストラクタを起点とする依存解決は初期化時に使うもの、アクションメソッドを起点とする依存解決はランタイムに使うものと認識しておくと良いでしょう。上記でPHPは事情が特殊であると述べましたが、PHPにもWebアプリケーションの初期化処理をサーバやワーカの起動時の一度きりにする実行形式を採用していこうとする流れが一部であります。ReactPHP、Swoole、RoadRunnerなどがそれです。今後は、初期化時の処理とランタイムの処理の区別、DIコンテナへの束縛の設定方針、およびHTTPリクエストをまたいでもイミュータブルとなる設計が重要になる場面も増えるだろうと予想しています。お問合わせプライバシーポリシ。