アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

<Symfony Componentsシリーズ(2)> Symfony 2の秘密兵器: Request Handler

小川です。

本日第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()



<?php

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



<?php
// 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



<?php
// 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



<?php
// 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
<?php
$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()



<?php

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は自分があまり触っていないため、もっと勉強してからになりそうです。それでは皆さま、次回をお楽しみに!