TwigからEntity::findするフィルターを書いてみた (Symfony Advent Calendar JP 2011 2日目)

初日の @brtriver さんに引き続き Symfony Advent Calendar JP 2011 の2日目です。


ここ数ヶ月 Symfony2 を触っています。実は Symfony 1.X の頃は興味ありながらもほとんど触っていなくて、2になってから触り始めたビギナー&ニューカマーなんです。諸先輩方よろしくお願いします。

Symfony Advent Calendar JP 2011ができるまで

昨年はSymfony Advent 2010をやっているのは知ってたので、今年はどうするのかなーやらないのかなー? と思って

とつぶやいたら、さっそく@ さんからリプライがあって、という流れで、何故か新人なのに取りまとめ役をやることになりました。不用意につぶやくと話が転がって面白い事になるという良い例です(笑)

Twig フィルターを書いてみた

プロパティの動的参照

さて本題です。Symfony2 の標準テンプレートエンジンの位置にある Twig ですが、開発しているのが Symfony の作者である Fabien さんだけあって、View としての役割がきっちり守られていて、余計なことはできないようになっています。


例えばオブジェクトのプロパティを動的に取得するようなことも、現在 Symfony2 にバンドルされている Twig 1.1.2 ではできません。Github Issues で話題になった時も Fabien は

This feature won't be included in Twig. Twig is a template system, not a fully-features language. Dynamic properties is clearly out of Twig scope.
訳: それは Twig の機能として含まれるべきではないよ。Twig はただのテンプレートシステムでフル機能の言語じゃないんだ。動的なプロパティ参照は Twig のスコープ外だよ。

https://github.com/fabpot/Twig/issues/41

と答えています。(その後考え直したのか Twig 1.2 から attribute 関数が導入されて、オブジェクトのプロパティを動的に取得できるようになりました。)

Entityから動的に取得する Twig フィルター

さて今回作成した Twig Filter は、Entity に対して検索をして値を取得するという、上記をさらに一歩踏み越えたある種のキワモノです。ロジックは Model や Controller でという原則からは外れるので安易に使うべきではありません。とは言えそういうキワモノを作るにはそれなりの理由もあるのです。

たとえばユーザの属性情報を考えてみましょう。ユーザはたくさんの属性情報を持っている場合があります。そのたくさんの属性のうちどれをどうやって表示するかを決めるのは View ですが、それを実際に表示するには Model や Controller 側でお膳立てをしておく必要があります。あるアイテムの所有者としてユーザ情報を表示するときに、そのユーザ情報の種類がすごく多かったり、直接 JOIN できなかったりする場合、M/C 側で全てを準備することは逆に現実的ではないと思うのです。

Symfony2 で Twig Extension

ということで、こんな Twig フィルターを作りました。

<?php
namespace Koyhoge\HogeBundle\Twig;
class HogeExtension extends \Twig_Extension {
    public function getFilters() {
        return array(
            'entity' => new \Twig_Filter_Method($this, 'entityFilter'),
            );
    }
    public function entityFilter($entity, $id, $prop, $by = null) {
        if (empty($by)) {
            $obj = $entity->find($id);
        } else {
            $objs = $entity->findBy(array($by => $id));
            $obj = $objs[0];
        }
        if ($obj == null) {
            return '';
        }
        $method = 'get' . ucfirst($prop);
        $call = array($obj, $method);
        if (!is_callable($call)) {
            throw new \Exception('Not callable:' . $method);
        }
        return call_user_func($call);
    }
    public function getName() {
        return 'hoge';
    }
}
使い方:Controller 側

Controller 側で以下のように Entity を直接 Twig に渡します。

<?php
namespace Koyhoge\HogeBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class DefaultController extends Controller {
    public function indexAction() {
        $users = $this->getDoctrine()->getRepository('KoyhogeHogeBundle:User');
        $items = array(
            array('user_id' => 1),
            array('user_id' => 11),
        );
        return $this->render('KoyhogeHogeBundle:Default:index.html.twig',
                             array('users' => $users', 'items' => $items));
    }
}
使い方:Twig テンプレート側

テンプレート側では以下のように呼び出せます。

{% for item in items %}
  {{ users|entity(item.user_id, 'name') }}
{% endfor %}

{{ users|entity(item.user_id, 'name') }}

の部分で、User Entityから item.user_id で find() を行い、その結果のオブジェクトから name プロパティを取得する、という処理を行います。

他のキーで findBy()

別のユニークキーで findBy() したい場合は

{{ users|entity(item.other_id, 'name', 'other_key') }}

などのように3番目の引数に渡します。

まとめ

正直あまり実用的ではない tips ですが、View に必要なデータを Controller 側ですべてお膳立てするのはなんか違うなーと感じていたので、試しに実装して見ました。何にでも多用することは危険だと思われますが、デザインの都合によっていつ表示するか分からない基本的な情報をこのやり方で処理するのは、有効なのではないかと思っています。


明日は、今回の Advent Calendar 企画を私に振った張本人、日本の Symfony 界の重鎮 @hidenorigoto さんです。よろしくお願いします!