Symfony2のControllerResolverを読む

"Create your own framework... on top of the Symfony2 Components"を読んで、Symfony2とSilexに興味がわいてきたので色々実験してみました。とりあえず"Create your own framework..."でやったCalendarをSilexでどう実装するかを考えてみます。

Silexといえば

$app->get('/hello/{name}', function ($name) use ($app) {
    return 'Hello '.$app->escape($name);
});

$app->run();

こんな感じのコードというイメージがありますが、中身としてはpart6で出てきたSymfony\Component\HttpKernel\Controller\ControllerResolverの派生クラスであるSilex\ControllerResolverを使っていて、$app->get()の第二引数には無名関数以外のものも渡すことができます。

ではどのようなものを渡すことができるのか、ControllerResolver#getController()を見ると以下のようになっているようです。

1. コールバック配列。つまり array($class_name, $method_name) 又は array($obj, $method_name)
2. __invokeを実装するオブジェクト
3. __invokeを実装するクラスのクラス名
4. 関数名
5. クラス名::メソッド名

無名関数の場合は2.になります。"Create your own framework..."では

$routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array(
    'year' => null,
    '_controller' => 'LeapYearController::indexAction',
)));

のようにルートを定義していたので、同様に、Silexでも

$app->get('/hello/{name}', 'LeapYearController::indexAction');

と書くこともできます。しかしこれはあまり嬉しくありません。LeapYearController::indexAction()に$nameを渡すことはできますが、無名関数を使った時とは異なり、サービスロケーターである$appを参照することはできないからです。

(2014/04/01追記:実は簡単にできました Symfony2/SilexのControllerResolverを読む2 - iakioの日記)

ではSymfony2の場合はどうなっているのでしょうか。Symfony2のコントローラーはController#getContainer()でDIコンテナを参照することができます。これは、Symfony2のControllerResolverはSymfony\Bundle\FrameworkBundle\Controller\ControllerResolverであり、そこでControllerResolver#createController()をオーバーライドして、コントローラーをnewした後にController#setContainer()を呼び出しているためです。

Silexでも同様のControllerResolverを作ることも可能です。非常に雑に書くとこんな感じ。

<?php
require_once __DIR__.'/../vendor/.composer/autoload.php';

use Silex\Application;
use Silex\ControllerResolver;

class MyControllerResolver extends ControllerResolver
{
    function __construct($app) {
        $this->app = $app;
    }

    protected function createController($controller) {
        list ($obj, $method) = parent::createController($controller);
        $obj->app = $this->app;
        return array($obj, $method);
    }
}

class LeapYearController
{
    function indexAction($year) {
        //...
    }
}

$app = new Application();
$app['resolver'] = $app->share(function () use ($app) {
    return new MyControllerResolver($app);
});

$app->get('/is_leap_year/{year}', 'LeapYearController::indexAction');

return $app;

と、$app['resolver']に独自のControllerResolverのインスタンスを返す無名関数をセットしてやれば良いわけです。

とまあ長々と書きましたがこれはあくまで実験で、実際のところこういうやり方はSilexっぽくは無いと思いますw。ただ、これだけでもSilexやSymfony2がとても柔軟にできていることは実感できました。

続く予定。