Symfony Advent Calendar 2020 の22日目の記事です。
機能は @ttskch 先輩の @Templateアノテーションを使わないほうがいい理由 でした。
昨日の記事で@Templateアノテーションは使わないほうがいいと書かれていたわけですが、僕はSymfonyの@Templateが大好きです。
なので@Templateの内部的な構造がどうなっているかを説明して、非推奨だけど使ってくれる人が増えてくれてと良いなと思い記事を書こうと思います。
@Templateの魅力とは?
@Templateの魅力は 「コントローラの戻り値でViewのレンダリングを行わない」 ところです。 「いやべつにレスポンス返すのはviewなわけだしコントローラでリターンしたっていいじゃん」って思う人もいるかもしれません。
でも本当にそれでいいでしょうか?
Acceptヘッダー
HTTPヘッダーにはAcceptというヘッダーがあります。 以下、MDNからの引用です
HTTP の Accept リクエストヘッダーは、クライアントが理解できるコンテンツタイプを MIME タイプで伝えます。 コンテンツネゴシエーションを使用して、サーバーは提案のうちの一つを選択し、それを使用してクライアントに Content-Type レスポンスヘッダーで選択を伝えます。
Acceptでapplication/jsonとクライアントが送ってきているのにHTML返すってのどうなんでしょうか?(まあそんなことはないんですが…) クライアントからの要求を無視して一方的に返すのがHTTPに関心があるフレームワークと言えるのでしょうか? (いやまあBEAR.Acceptに憧れているだけなんですが…)
Controllerから配列を返すようにしておけば、やろうと思えばAcceptヘッダーを見てjsonを返すか、HTMLを返すか選択することもできますよね。
JSONレスポンスへの柔軟な切り替え
今までHTMLでレスポンスしていたものをJSONでレスポンスしたいなんてことは最近だと結構あると思います。
$this->render()
をレスポンスしてしまっていると実装コードを変更しなければなりません。
でも@Templeteを使っていればpolidog/simple-api-bundle の@Api
アノテーションに切り替えるだけで簡単にJSONレスポンスに変更できます。 (完全に宣伝ですね…てか SimpleApiBundleメンテしてなくてごめんなさい。)
なぜ@Templateは非推奨になってしまったのか?
issueには以下のような2つの理由があげられています。
- It involves some “magic” (not exactly magic, but “hidden conventions”); -> 訳: これにはいくつかの「魔法」(正確には魔法ではありませんが、「隠された慣習」)が含まれています。
- It’s common to use @Template without any parameters, which makes it more difficult to know which template is being rendered. -> パラメータなしで@Templateを使うのが一般的で、どのテンプレートがレンダリングされているのかを知るのが難しくなります。
この理由だと@Routeとかもやめたほうがいいのでは?とか思うんですがどうでしょうか?
イマイチ僕はこの主張が腑に落ちません。
というかこの記事を書くあたって初めて @Template
の非推奨の理由見ましたw
僕はてっきりパフォーマンス的に微妙だからという話かと思っていたのですが、違ったんですね…
@Templateがどのような仕組みか見てみる
@Template
からテンプレートをレンダリングする仕組みがどうなっているか実装を見てみましょう。
そんなに複雑な実装ではないのでコードリーディングしやすいです。(最近読んでたReactHooksのコードのほうが3000倍難しいです😇)
Symfonyフレームワークのイベントについて
コードリーディングの前にSymfonyフレームワークのイベントを理解しておくと良いかもしれません。 知っている方も多いとは思いますが、軽く解説しておきます。
イベントは全部で7個あります。
- kernel.request
- kernel.controller
- kernel.controller_arguments
- kernel.view
- kernel.response
- kernel.finish_request
- kernel.terminate
- kernel.exception
kernel.request
コントローラが決定する前に発火するイベント。
リクエストの処理を止めるために、リクエストに情報を追加したり、レスポンスを早めに返したりするのに便利。
認証が通ってなかったら落とすとか、ユーザーの状態を見て違うページにリダイレクトさせるとか結構便利なイベントですよね。
kernel.controller
実行するコントローラが解決された後、実行前に発火するイベントですね
コントローラが必要とするものを初期化するのに便利で、たしかParamConverterとかもこの辺のイベントで解決しているので併用する際は Priorityに気をつけたほうがいいですね。
kernel.controller_arguments
コントローラが呼ばれる直前に発火するイベント。
コントローラに渡す引数を設定するのに便利。
一般的に、これは URL ルーティングパラメータを対応する名前付き引数にマップするために使われている。
ArgumentValueResolverはSymfonyフレームワークのイベントで発火するわけではない。
若干話しが脱線しますが、SymfonyのArgumentValueResolverはeventをトリガーにしていません。
HttpKernelを読めばわかると思いますがkernel.controllerの後kernel.controller_argumentsの前に実行します。
kernel.view
名前からするとViewがレンダリングされる前に発火するのかなとか思いがちなんですが、これはコントローラが実行された後に発火するものです。(kernel.finish_controllerとかでもいいのでは・・・)
kernel.response
controllerや kernel.view リスナーがレスポンスオブジェクトを返した後に発火するイベント。
普通に仕事でSymfony使っているとこの辺はあまり使わない気がします。
kernel.finish_request
このイベントは kernel.response イベントの後に発火します。
アプリケーションのグローバルな状態をリセットするのに便利ですが、あんま使わないですね…
kernel.terminate
このイベントは、レスポンスが送信された後(handle()メソッドの実行後)に発火します。
レスポンス返したあとにやりたい処理に使うと便利ですが、これもあんまり使わないですね。。。。
kernel.exception
このイベントは、HTTP リクエストの処理中にエラーが発生するとすぐに発火されます。エラーから回復したり、レスポンスとして送信された例外の詳細を修正したりするのに便利です。
エラー処理のときになにかしたときは良くここを利用します。
イベントに関する詳しいことは
こまかい詳細の仕様は以下のドキュメントを参照してください。結構勉強になりますねここの記事は。
The HttpKernel Component
@Templateの実行順序
@Templateからテンプレートがレンダリングされる順序は以下のとおりです。
- kernel.controllerイベント発火時にControllerListenerでControllerのアノテーションが解析される
- kernel.viewイベント発火時にTemplateListnerでレンダリング処理が実行される
kernel.controllerイベント発火時にControllerListenerでControllerのアノテーションが解析される
ControllerListenerでアノテーションが解析されます。
最終的には_template
という値としてRequestオブジェクトのAttributeとして@Templateのアノテーション情報がセットされます。
https://github.com/sensiolabs/SensioFrameworkExtraBundle/blob/d77e6765ff65c01d2b53af10248d6022e7b62166/src/EventListener/ControllerListener.php#L84
class annotationとmethod annotationはどちらが優先されるのか?
class annotationとmethod annotationのどちらが優先されるかもControllerListenerに記述があります。 https://github.com/sensiolabs/SensioFrameworkExtraBundle/blob/d77e6765ff65c01d2b53af10248d6022e7b62166/src/EventListener/ControllerListener.php#L74
array_mergeしているだけなので両方に記述があった場合は ** method annotationが優先 ** されるようです。
(え?Routeアノテーションは??って思っている人がいるかも知れませんが、ここでポイントなのは ConfigurationInterface
をもつAnnotationを解析するのでRouteアノテーションはここでは処理されないんですよね。)
kernel.viewイベント発火時にTemplateListnerでレンダリング処理が実行される
https://github.com/sensiolabs/SensioFrameworkExtraBundle/blame/d77e6765ff65c01d2b53af10248d6022e7b62166/src/EventListener/TemplateListener.php#L115
この行にレスポンス処理をしているところが書いてあります。
もちろんStreamedResponseに対応しています。
その場合は @Template(streamable=true)
としてあげれば大丈夫です。
Templateの名の決定について
@Template
でテンプレート名を定義してない場合は、kernel.controller
イベントで解決されます。
以下のリンクに処理が記述されています。
https://github.com/sensiolabs/SensioFrameworkExtraBundle/blame/d77e6765ff65c01d2b53af10248d6022e7b62166/src/EventListener/TemplateListener.php#L71
そんなにTemplateListner自体は複雑ではない
一見 @Template
アノテーションつけただけでレンダリングされるのは魔法に見えるかもしれませんが意外と処理は単純なんです。
なのでそんなに怖がる必要はないですよ!!!!!!
余談: 自作 annotationについて
コントローラに自作アノテーション追加したい場合はどうしたらいいのか?
これはすごく簡単で Sensio\Bundle\FrameworkExtraBundle\Configuration\ConfigurationInterface
を継承したAnnotationを作れば、ControllerListenerでパースしてくれます。
あとはコントローラなり好きな場所で $request->attribute->get()
してあげればいい。
ちょっとしたアノテーション作りたいときは非常に便利です。
まとめ
- Best Practiceから外れた理由や、非推奨の理由が若干微妙(個人的意見)
- Symfony @Template アノテーションは実装自体はそんなに複雑ではない
- 使うのは結構ありだと思う
- よかったらSimpleApiBundleも使ってみてください