<Symfony Componentsシリーズ(2)> Symfony 2の秘密兵器: Request Handler
- 2010/03/11
小川雄大
小川です。
本日第2弾となるこの記事では、今回はリクエストを受けてからレスポンスを返すまでの全体の流れを司る、Request Handlerというコンポーネントをご紹介します。
--------------------------
追記
どうやらコンポーネントの名称がRequestHandlerからHttpKernelに変更されたようです。まだまだ開発中なので内部実装も色々と変更が行われています。この記事はあまり当てにならないのでご注意ください。
--------------------------
Request Handlerを知るにあたって、Event Dispatcherコンポーネントを理解しておく必要があります。
先に書いた<Symfony Componentsシリーズ(1)> オブジェクトをつなぐEvent Dispatcherという記事まだ読んでいない方は、そちらから読んでいただければと思います。(読んでもさっぱりわからない!というのであればご連絡ください・・・)
◆ Request Handlerコンポーネントの構成
まず、Request Handlerコンポーネントのディレクトリ構成です。以下のようになります。
symfony 1系には当然RequestクラスとResponseクラスがありますが、それらもまとめてこのRequest Handlerコンポーネントに含まれます。
RequestInterfaceとResponseInterfaceというインターフェースがありますが、現在のところRequestInterfaceは空っぽ、ResponseInterfaceはsend()メソッドのみのインターフェースなので、RequestやResponseを自前のクラスや他のフレームワークのクラスを使いたい場合も大して難しくはないでしょう。
ただSymfony上で使う場合は、RequestHandler以外の部分で様々なメソッドを使いますので、RequestやResponseを置き換えたい場合は、Request Handlerコンポーネント内のクラスを継承するのが無難かと思われます。
Request Handler用の例外クラスが4つ定義されています。HttpExceptionは他3つの親となる例外クラスです。ただ、定義こそされていますがRequestHandlerが実際に処理中に投げるのはNotFoundHttpExceptionくらいです。その他にSPL例外を投げたりはします。
◆ RequestHandlerクラス
ではいよいよ、RequestHandlerクラスを見ていきましょう。全体の流れを司るクラスと聞くと非常に大きなクラスかと思われますが、実際にはコメントを含めても200行に満たない程度の、非常にシンプルなクラスです。
実際に様々な処理を行っているわけではなく、本当に「アプリケーションの流れを形成」しているだけです。メソッドもコンストラクタを含めて4つだけしか定義されていません。
* public function __construct(EventDispatcher $dispatcher)
* public function handle(RequestInterface $request, $main = true)
* public function handleRaw(RequestInterface $request, $main = true)
* protected function filterResponse($response, $message, $main)
これだけです。重要なのがhandle()メソッドです。引数をみると、RequestInterfaceを実装したクラスを受け取っているのがわかります。
Symfony 2ではこのRequestHandlerをKernelが制御します。といっても、RequestHandlerのhandle()メソッドを呼び出す前にBundleを読み込んで、DIコンテナの設定をする程度で、後はRequest Handlerによって処理が流れていきます。
ではより突っ込んでみていきましょう。
◆ RequestHandler::handle()
handleRaw()を呼び出して、戻り値をそのまま返すだけのメソッドです。handleRaw()の戻り値はResponseInterfaceを実装したオブジェクトになります。
また、例外が発生した場合はcore.exceptionイベントを通知しています。ここでEventに対してisProcessed()とgetReturnValue()という2つのメソッドを実行しています。isProcessed()は登録してあるリスナーのどれかが処理を行ってtrueを返した場合にtrueを返します。つまりイベントに対して何かしらの処理がされたかをチェックするためのものです。getReturnValue()はリスナーが指定した戻り値を取得するためのものです。リスナーがreturnした値ではなく、リスナー側で$event->setReturnValue()を実行してセットされたものになります。
Symfonyはここで、例外に対して例外用の画面を表示するためのレスポンスを作成して返します。filterResponse()メソッドを呼び出していますが、これはレスポンスを返す際に必ず通す処理で、レスポンスを返すためのイベントを通知するためのものです。このメソッドにはResponseInterfaceを実装したオブジェクトを渡さないと例外となるようです。詳しくは後ほど見ていきましょう。
◆ RequestHandler::handleRaw()
例外が起きた場合の処理はhandle()メソッドに任せるとして、正常なフローを一通り行うのがこのhandleRaw()です。
ここでいうフローとは、ずばりイベントです。順番に次のようにイベントが呼び出されます。
* core.request
* core.load_controller
* core.controller
* core.view
実際に処理を分割しながらみていきましょう。
### core.request
Symfony側ではこのイベントが発生したら、まずルーティングが行われます。
次のイベントがcore.load_controllerですので、コントローラを読み込む処理だと思われます。それまでに必要なことはこのイベントで行います。
isProcessed()がtrueの場合は、イベントの戻り値をfilterResponse()に渡しています。
filterResponse()に戻り値を渡しているということは、Responseオブジェクトを期待していることになります。
コントローラを読み込む必要がないようなリクエストや、コントローラの処理そのものがキャッシュ場合などは処理を止めてレスポンスを返すのかなと思います。
### core.load_controller
コントローラをロードする処理ですね。ここでisProcessed()がfalseの場合、つまりコントローラが読み込まれなかった場合はNotFoundHttpExceptionがスローされます。
読み込んだ後は、イベントの戻り値にコントローラとその引数を設定する必要があるようです。
コントローラは関数呼び出し可能でなければならないようですが、Symfonyでは通常、アクションとなるメソッドが指定されると思われます。
シンプルにやりたい場合は、無名関数が戻り値でも十分だということですね。
### core.controller
core.load_controllerで返ってきた$controllerを実際に実行するのが、core.controllerです。
このイベントにリスナーを指定しなくても、コントローラの呼び出しは行われるようです。
### core.view
handleRaw()から通知される最後のイベントです。先ほどcore.controllerを行った結果をフィルタリングしています。その後、イベントの戻り値をfilterResponse()に渡しています。
filter()にリスナーが登録されていない場合、filter()に指定した第2引数がそのまま戻り値として設定されます。
説明を一切しなかったのですが、handleとhandleRaw()には第2引数に$mainというものがあります。これはデフォルトではtrueです。falseになるのはどういった状況なのでしょうか。
symfonyをよく知っている方はもしかしたらなんとなくわかるかもしれません。これがfalseで呼び出されるのは、forwardが行われた時です。
Symfonyのforwardとは、同一リクエストの中で、特定のアクションから別のアクションを呼び出す処理のことです。forwardが行われた場合は、再度handle()が呼び出されるようになります。
◆ RequestHandler::filterResponse()
最後にご紹介するのが、散々出てきたfilterResponse()です。まず第1引数がResponseInterfaceを実装していなければ、実行時例外であるRuntimeExceptionが投げられます。
その後、レスポンスオブジェクトに対してfilterを通知しています。handle()の戻り値は例外が最終的にキャッチされなかった場合を除き、常にこのメソッドを通るため、確実にResponseInterfaceを実装したオブジェクトになります。
Request Handlerコンポーネントの紹介は以上です。説明の途中でSymfonyがイベントに対する処理についていくつか記述している箇所があります。
実際にフレームワークとしてコントローラまわりなどの処理は、Symfony\Framework\WebBundleというBundleに含まれています。このBundleの中にListenerというディレクトリがあり、そこにいくつかのイベントリスナーが用意されています。これらがSymfonyとしての流れを実際に処理しています。
そこを具体的に話し出すと、この記事が終わらなくなってしまうので本日はご紹介しませんが、そこで行っていること基本的な内容は書いてある通りです。
さて、長くなりましたが、Symfony 2の秘密兵器であるRequest Handlerコンポーネントの説明は以上になります。
イベントによって流れが作られているため、非常に柔軟になっています。この仕組みを知っておくことで、多少は全体が見えやすくなるかと思います。
さて、今回タイトルに、Symfony Componentsシリーズと入れています。今後も様々なコンポーネントを紹介していきたいと思っています。次は重要性を考えるとDependency Injectionでしょうか。Templatingは自分があまり触っていないため、もっと勉強してからになりそうです。それでは皆さま、次回をお楽しみに!
本日第2弾となるこの記事では、今回はリクエストを受けてからレスポンスを返すまでの全体の流れを司る、Request Handlerというコンポーネントをご紹介します。
--------------------------
追記
どうやらコンポーネントの名称がRequestHandlerからHttpKernelに変更されたようです。まだまだ開発中なので内部実装も色々と変更が行われています。この記事はあまり当てにならないのでご注意ください。
--------------------------
Request Handlerを知るにあたって、Event Dispatcherコンポーネントを理解しておく必要があります。
先に書いた<Symfony Componentsシリーズ(1)> オブジェクトをつなぐEvent Dispatcherという記事まだ読んでいない方は、そちらから読んでいただければと思います。(読んでもさっぱりわからない!というのであればご連絡ください・・・)
◆ Request Handlerコンポーネントの構成
まず、Request Handlerコンポーネントのディレクトリ構成です。以下のようになります。
- Components/RequestHandler:
- Exception/
Request.php RequestHandler.php RequestInterface.php Response.php ResponseInterface.php - Components/RequestHandler/Exception:
- ForbiddenHttpException.php
HttpException.php NotFoundHttpException.php UnauthorizedHttpException.php
symfony 1系には当然RequestクラスとResponseクラスがありますが、それらもまとめてこのRequest Handlerコンポーネントに含まれます。
RequestInterfaceとResponseInterfaceというインターフェースがありますが、現在のところRequestInterfaceは空っぽ、ResponseInterfaceはsend()メソッドのみのインターフェースなので、RequestやResponseを自前のクラスや他のフレームワークのクラスを使いたい場合も大して難しくはないでしょう。
ただSymfony上で使う場合は、RequestHandler以外の部分で様々なメソッドを使いますので、RequestやResponseを置き換えたい場合は、Request Handlerコンポーネント内のクラスを継承するのが無難かと思われます。
Request Handler用の例外クラスが4つ定義されています。HttpExceptionは他3つの親となる例外クラスです。ただ、定義こそされていますがRequestHandlerが実際に処理中に投げるのはNotFoundHttpExceptionくらいです。その他にSPL例外を投げたりはします。
◆ RequestHandlerクラス
ではいよいよ、RequestHandlerクラスを見ていきましょう。全体の流れを司るクラスと聞くと非常に大きなクラスかと思われますが、実際にはコメントを含めても200行に満たない程度の、非常にシンプルなクラスです。
実際に様々な処理を行っているわけではなく、本当に「アプリケーションの流れを形成」しているだけです。メソッドもコンストラクタを含めて4つだけしか定義されていません。
* public function __construct(EventDispatcher $dispatcher)
* public function handle(RequestInterface $request, $main = true)
* public function handleRaw(RequestInterface $request, $main = true)
* protected function filterResponse($response, $message, $main)
これだけです。重要なのがhandle()メソッドです。引数をみると、RequestInterfaceを実装したクラスを受け取っているのがわかります。
Symfony 2ではこのRequestHandlerをKernelが制御します。といっても、RequestHandlerのhandle()メソッドを呼び出す前にBundleを読み込んで、DIコンテナの設定をする程度で、後はRequest Handlerによって処理が流れていきます。
ではより突っ込んでみていきましょう。
◆ RequestHandler::handle()
- public
function handle(RequestInterface $request, $main = true) - {
$main = (Boolean) $main; try { return $this->handleRaw($request, $main); } catch (\Exception $e) { // exception $event = $this->dispatcher->notifyUntil(new Event($this, 'core.exception', array('main_request' => $main, 'request' => $request, 'exception' => $e))); if ($event->isProcessed()) { return $this->filterResponse($event->getReturnValue(), 'A "core.exception" listener returned a non response object.', $main); } throw $e; } - }
handleRaw()を呼び出して、戻り値をそのまま返すだけのメソッドです。handleRaw()の戻り値はResponseInterfaceを実装したオブジェクトになります。
また、例外が発生した場合はcore.exceptionイベントを通知しています。ここでEventに対してisProcessed()とgetReturnValue()という2つのメソッドを実行しています。isProcessed()は登録してあるリスナーのどれかが処理を行ってtrueを返した場合にtrueを返します。つまりイベントに対して何かしらの処理がされたかをチェックするためのものです。getReturnValue()はリスナーが指定した戻り値を取得するためのものです。リスナーがreturnした値ではなく、リスナー側で$event->setReturnValue()を実行してセットされたものになります。
Symfonyはここで、例外に対して例外用の画面を表示するためのレスポンスを作成して返します。filterResponse()メソッドを呼び出していますが、これはレスポンスを返す際に必ず通す処理で、レスポンスを返すためのイベントを通知するためのものです。このメソッドにはResponseInterfaceを実装したオブジェクトを渡さないと例外となるようです。詳しくは後ほど見ていきましょう。
◆ RequestHandler::handleRaw()
例外が起きた場合の処理はhandle()メソッドに任せるとして、正常なフローを一通り行うのがこのhandleRaw()です。
ここでいうフローとは、ずばりイベントです。順番に次のようにイベントが呼び出されます。
* core.request
* core.load_controller
* core.controller
* core.view
実際に処理を分割しながらみていきましょう。
### core.request
- //
request - $event
= $this->dispatcher->notifyUntil(new Event($this, 'core.request', array('main_request' => $main, 'request' => $request))); - if
($event->isProcessed()) - {
return $this->filterResponse($event->getReturnValue(), 'A "core.request" listener returned a non response object.', $main); - }
Symfony側ではこのイベントが発生したら、まずルーティングが行われます。
次のイベントがcore.load_controllerですので、コントローラを読み込む処理だと思われます。それまでに必要なことはこのイベントで行います。
isProcessed()がtrueの場合は、イベントの戻り値をfilterResponse()に渡しています。
filterResponse()に戻り値を渡しているということは、Responseオブジェクトを期待していることになります。
コントローラを読み込む必要がないようなリクエストや、コントローラの処理そのものがキャッシュ場合などは処理を止めてレスポンスを返すのかなと思います。
### core.load_controller
- //
load controller - $event
= $this->dispatcher->notifyUntil(new Event($this, 'core.load_controller', array('main_request' => $main, 'request' => $request))); - if
(!$event->isProcessed()) - {
throw new NotFoundHttpException('Unable to find the controller.'); - }
- list($controller,
$arguments) = $event->getReturnValue(); - //
controller must be a callable - if
(!is_callable($controller)) - {
throw new \LogicException(sprintf('The controller must be a callable (%s).', var_export($controller, true))); - }
コントローラをロードする処理ですね。ここでisProcessed()がfalseの場合、つまりコントローラが読み込まれなかった場合はNotFoundHttpExceptionがスローされます。
読み込んだ後は、イベントの戻り値にコントローラとその引数を設定する必要があるようです。
コントローラは関数呼び出し可能でなければならないようですが、Symfonyでは通常、アクションとなるメソッドが指定されると思われます。
シンプルにやりたい場合は、無名関数が戻り値でも十分だということですね。
### core.controller
- //
controller - $event
= $this->dispatcher->notifyUntil(new Event($this, 'core.controller', array('main_request' => $main, 'request' => $request, 'controller' => &$controller, 'arguments' => &$arguments))); - if
($event->isProcessed()) - {
try { return $this->filterResponse($event->getReturnValue(), 'A "core.controller" listener returned a non response object.', $main); } catch (\Exception $e) { $retval = $event->getReturnValue(); } - }
- else
- {
// call controller $retval = call_user_func_array($controller, $arguments); - }
core.load_controllerで返ってきた$controllerを実際に実行するのが、core.controllerです。
このイベントにリスナーを指定しなくても、コントローラの呼び出しは行われるようです。
### core.view
- //
view - $event
= $this->dispatcher->filter(new Event($this, 'core.view', array('main_request' => $main)), $retval); - return
$this->filterResponse($event->getReturnValue(), sprintf('The controller must return a response (instead of %s).', is_object($event->getReturnValue()) ? 'an object of class '.get_class($e->getReturnValue()) : str_replace("\n", '', var_export($event->getReturnValue(), true)) ), $main);
handleRaw()から通知される最後のイベントです。先ほどcore.controllerを行った結果をフィルタリングしています。その後、イベントの戻り値をfilterResponse()に渡しています。
filter()にリスナーが登録されていない場合、filter()に指定した第2引数がそのまま戻り値として設定されます。
説明を一切しなかったのですが、handleとhandleRaw()には第2引数に$mainというものがあります。これはデフォルトではtrueです。falseになるのはどういった状況なのでしょうか。
symfonyをよく知っている方はもしかしたらなんとなくわかるかもしれません。これがfalseで呼び出されるのは、forwardが行われた時です。
Symfonyのforwardとは、同一リクエストの中で、特定のアクションから別のアクションを呼び出す処理のことです。forwardが行われた場合は、再度handle()が呼び出されるようになります。
◆ RequestHandler::filterResponse()
- protected
function filterResponse($response, $message, $main) - {
if (!$response instanceof ResponseInterface) { throw new \RuntimeException($message); } $event = $this->dispatcher->filter(new Event($this, 'core.response', array('main_request' => $main)), $response); $response = $event->getReturnValue(); if (!$response instanceof ResponseInterface) { throw new \RuntimeException('A "core.response" listener returned a non response object.'); } return $response; - }
最後にご紹介するのが、散々出てきたfilterResponse()です。まず第1引数がResponseInterfaceを実装していなければ、実行時例外であるRuntimeExceptionが投げられます。
その後、レスポンスオブジェクトに対してfilterを通知しています。handle()の戻り値は例外が最終的にキャッチされなかった場合を除き、常にこのメソッドを通るため、確実にResponseInterfaceを実装したオブジェクトになります。
Request Handlerコンポーネントの紹介は以上です。説明の途中でSymfonyがイベントに対する処理についていくつか記述している箇所があります。
実際にフレームワークとしてコントローラまわりなどの処理は、Symfony\Framework\WebBundleというBundleに含まれています。このBundleの中にListenerというディレクトリがあり、そこにいくつかのイベントリスナーが用意されています。これらがSymfonyとしての流れを実際に処理しています。
そこを具体的に話し出すと、この記事が終わらなくなってしまうので本日はご紹介しませんが、そこで行っていること基本的な内容は書いてある通りです。
さて、長くなりましたが、Symfony 2の秘密兵器であるRequest Handlerコンポーネントの説明は以上になります。
イベントによって流れが作られているため、非常に柔軟になっています。この仕組みを知っておくことで、多少は全体が見えやすくなるかと思います。
さて、今回タイトルに、Symfony Componentsシリーズと入れています。今後も様々なコンポーネントを紹介していきたいと思っています。次は重要性を考えるとDependency Injectionでしょうか。Templatingは自分があまり触っていないため、もっと勉強してからになりそうです。それでは皆さま、次回をお楽しみに!
コメント
コメントフォーム
トラックバック
最近の記事
- もうすぐ健康診断があるんだ・・・ [2010年09月02日 : 阿部恵]
- Photoshopで壁紙を作りながら、基本的な使い方を覚える [2010年09月01日 : 鴨田健次]
- はじめての共同作業 Canvas編 (node.js + websocket) [2010年09月01日 : 中川善樹]
- 「PHP×Flex(後編)」PHPテクニカルセミナー(無料)第4弾の募集を開始しました!! [2010年08月26日 : 和田記光]
- 【HTML5】Canvasでお絵かきしてみた(前編) [2010年08月25日 : 橋本章史]
- MacにgroongaのMySQL用ストレージエンジン [2010年08月23日 : 笹亀弘]
- Appleのサイトで見たiPhone4をFireworksで描いてみました-1/2 [2010年08月19日 : 和田記光]
- iPad版の会社紹介を作ってみました [2010年08月19日 : 小林有佳]
- iPhoneアプリ開発開始時に気をつけるべきファイルの取り扱い (2) [2010年08月19日 : 亀本大地]
- symfonyセミナー動画無料公開! [2010年08月13日 : 岡本雄樹]



大変申し訳ございませんが、More with symfonyの日本語翻訳版、"もっと知りたいsymfony"はまだ校正が終わっておらず、出版には至っておりません。
校正が終わるのが4月にかかりそうとのことですので、それが終わり次第となります。
現状はWeb版(http://www.symfony-project.org/more-with-symfony/1_4/ja/)のみとなりますので、何卒ご了承ください。