symfony1/Symfony2の起動シーケンスの違い

この記事は、Symfony アドベントカレンダー 2010 に参加しています。


Symfony2のソースを初めて見たのは先月行われた第1回 Symfony2 勉強会が初めてだったんですが、大幅に変わった起動シーケンスにびっくりして、さらにその素直なシーケンスに大変感動しました。

というわけで、今日はsymfony1とSymfony2の起動シーケンスの違いを簡単に比較したいと思います。

まずは、symfony1(symfony 1.4)ですが、web/index.php は以下のようになっています。

<?php


require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php');

$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'prod', false);
sfContext::createInstance($configuration)->dispatch();

流れとしては以下のようなものになっています。

  • 設定ファイルを読み込む(フレーム本体のfactories.yml/アプリケーションのfactories.yml)
  • 読み込んだ設定ファイルからFrontControllerを決定し、そのdispatchメソッドを読み込む

アプリケーション側のfactories.ymlで上書きしなければ、sfFrontWebController#dispatch が呼ばれます。その中では、リクエストパラメータからモジュール名とアクションが決定されるという形になります。

sfFrontWebController#dispatch は以下のようなものです。

  public function dispatch()
  {
    try
    {
      // reinitialize filters (needed for unit and functional tests)
      sfFilter::$filterCalled = array();

      // determine our module and action
      $request    = $this->context->getRequest();
      $moduleName = $request->getParameter('module');
      $actionName = $request->getParameter('action');

      if (empty($moduleName) || empty($actionName))
      {
        throw new sfError404Exception(sprintf('Empty module and/or action after parsing the URL "%s" (%s/%s).', $request->getPathInfo(), $moduleName, $actionName));
      }

      // make the first request
      $this->forward($moduleName, $actionName);
    }
    catch (sfException $e)
    {
      $e->printStackTrace();
    }
    catch (Exception $e)
    {
      sfException::createFromException($e)->printStackTrace();
    }
  }

その後決定されたモジュール/アクションに応じてフィルターの構築が行われ、フィルター実行の一環としてアクションが実行されるというものでした。(この流れですが、symfony1の元となったMojaviの流れとあまり変わりません。それはもともとMojaviを使っていた人がsymfonyに移行しやすかった要因の一つともいえると思います)

さて、Symfony2の方はどうなっているかというと、先日でたPR4のweb/app.php は以下のようになっています。

<?php

require_once __DIR__.'/../app/AppKernel.php';

use Symfony\Component\HttpFoundation\Request;

$kernel = new AppKernel('prod', false);
$kernel->handle(new Request())->send();

Symfony2ではHttpKernelInterfaceを実装したクラスがベースの処理を受け持ちます。これはHTTPのシーケンスを素直にマッピングしたようなものになっており、上記のファイルの最後のメソッドも以下のように読むことができます。

  • リクエストを受け取り、
  • それをハンドリングしてレスポンスを作成し、
  • その結果をクライアントに送信する

具体的に追っかけていくと、AppKernel#handleは Symfony\Component\HttpKernel\Kernel#handle なので以下のような処理です。

    public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
    {
        if (false === $this->booted) {
            $this->boot();
        }

        return $this->container->get('http_kernel')->handle($request, $type, $catch);
    }

この「$this->boot()」の処理は8日目のDIコンテナの起動とエクステンション - ゆっくり*ゆっくりにあるようにDIContainerの初期化を実行しています(詳しくはそちらをご覧ください)。その後、DIContainerに http_kernel として登録された Symfony\Component\HttpKernel\HttpKernel#handle を呼び出します。

    public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
    {
        $masterRequest = HttpKernelInterface::MASTER_REQUEST === $type ? $request : $this->container->get('request');

        $this->container->set('request', $request);

        $response = parent::handle($request, $type, $catch);

        $this->container->set('request', $masterRequest);

        return $response;
    }

ここでも親クラスのhandleの処理結果のレスポンスを返すという素直な実装になってます。さて、親クラスは Symfony\Component\HttpKernel\BaseHttpKernel なので、そのhandleは以下のようになっています。

    public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
    {
        try {
            return $this->handleRaw($request, $type);
        } catch (\Exception $e) {
            if (false === $catch) {
                throw $e;
            }

            // exception
            $event = new Event($this, 'core.exception', array('request_type' => $type, 'request' => $request, 'exception' => $e));
            $this->dispatcher->notifyUntil($event);
            if ($event->isProcessed()) {
                return $this->filterResponse($event->getReturnValue(), $request, 'A "core.exception" listener returned a non response object.', $type);
            }

            throw $e;
        }
    }

で、BaseHttpKernel#handleRawは以下のようになってます。

    protected function handleRaw(Request $request, $type = self::MASTER_REQUEST)
    {
        // request
        $event = new Event($this, 'core.request', array('request_type' => $type, 'request' => $request));
        $this->dispatcher->notifyUntil($event);
        if ($event->isProcessed()) {
            return $this->filterResponse($event->getReturnValue(), $request, 'A "core.request" listener returned a non response object.', $type);
        }

        // load controller
        if (false === $controller = $this->resolver->getController($request)) {
            throw new NotFoundHttpException('Unable to find the controller.');
        }

        $event = new Event($this, 'core.controller', array('request_type' => $type, 'request' => $request));
        $this->dispatcher->filter($event, $controller);
        $controller = $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)));
        }

        // controller arguments
        $arguments = $this->resolver->getArguments($request, $controller);

        // call controller
        $retval = call_user_func_array($controller, $arguments);

        // view
        $event = new Event($this, 'core.view', array('request_type' => $type, 'request' => $request));
        $this->dispatcher->filter($event, $retval);

        return $this->filterResponse($event->getReturnValue(), $request, sprintf('The controller must return a response (instead of %s).', is_object($event->getReturnValue()) ? 'an object of class '.get_class($event->getReturnValue()) : is_array($event->getReturnValue()) ? 'an array' : str_replace("\n", '', var_export($event->getReturnValue(), true))), $type);
    }

処理されるControllerを決定しているのは「$this->resolver->getController($request)」になります。(メインのControllerをcall_user_func_arrayで実行しているというのは軽く衝撃をうけました)

リクエストを受け取って、処理した結果のレスポンスを出力するというとっても素直な流れというのはわかりやすいですし、またこのわかりやすい流れはフレームワーク本体にがっちり組み込まれているわけではなく、FrameworkBundleという外部部品を使用して構成されているという作りにも拡張性の高さを感じます。

Symfony2はMojavisymfonyという流れをいったん断ち切って全く新しいものとなっています。そういう意味では今までその流れで使っていた人にはちょっと取っつきにくいかもしれませんが、それはそれ Fabien っぽいコードになれていればさらさら読めると思います。

また、今までsymfonyはまったく触ったことがなくてという方も今までのベースは全く必要ない新しいフレームワークになっているので、是非Symfony2を触ってみてはいかがでしょうか!

Symfony Advent 2010であなたの記事を公開してみませんか?

Symfony Advent 2010では12月1日から12月24日までを使って日替わりでsymfonyで イイなと思った小さなtipsから内部構造まで迫った解説などをブログ記事にして公開していくイベントです。

参加についてはATNDで参加表明の上、Google GroupのSymfony Advent 2010に追加リクエストを送信ください。

Symfony Advent 2010チーム一同、あなたの参加をお待ちしております。

日本Symfonyユーザー会 Symfony アドベントカレンダー2010

Symfony Advent 2010はsymfony好きな有志で集まったチームです。