sfTwigPluginで遊んでみた

id:cocoitiおにいちゃんががんばってくれているsfTwigPluginで遊んでみました。Twigは最近注目のテンプレートエンジンですね。

symfonyのpluginsディレクトリにsfTwigPluginいれるまでは終わってるとして、まずはPluginを有効に。config/ProjectConfiguration.class.phpを修正

<?php
// sfCoreAutoload...

class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    // ...
    $this->enablePlugins('sfTwigPlugin');
  }

次は各アクションがテンプレート出力時にsfPHPViewクラスではなくsfTwigViewを利用するように指定。通常、Viewクラスの変更は各モジュールのconfig/module.ymlで行いますが、これらのファイルは各アプリケーションのconfigやプロジェクトのconfigに置いておいても遡って読みこんでくれるので、とりあえずプロジェクトのconfigにmodule.ymlを作成。

all:
  view_class: sfTwig
  partial_view_class: sfTwig

パーシャルやコンポーネントで使われるpartial_view_classも置き換えておきます。これでTwigが使われるようになります。

んで、最初にわからなかったのが、拡張子が.htmlになること。layoutもxxxSuccessも.phpから.htmlに置き換えます。

次にレイアウトの修正。apps/(frontend)/templates/layout.phpを次のように置き換えます。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    {{ ''|include_http_metas() }}
    {{ ''|include_metas() }}

    <title>ここいちおにいちゃんだぁいすき</title>

    <link rel="shortcut icon" href="/favicon.ico" />

    {{ ''|include_stylesheets() }}
    {{ ''|include_javascripts() }}
  </head>
  <body>
    {{ sf_content }}
  </body>
</html>

{{ ... }}の部分がTwigの出力部分ですね。

たとえば{{ ""|include_http_metas() }}をみてみましょう。""は空文字です。その次に「|」(パイプ)で区切られて、include_http_metas()というsymfony使いの方なら見慣れた関数名が記述されています。このパイプ処理は、TwigのFilterという機能です。前に記述されているテキストを引数に、Filterとして関数が実行される感じです。この場合実際のPHPコードとしては、<?php include_http_metas("") ?> に展開されます。ちなみにFilterが関数っぽくなっていますが、{{ ''|include_http_metas }}のように引数(パイプされる文字列を除く)を渡さない場合は()を省略しても動きます。

こんな面倒なことをせず、{{ include_http_metas() }}とやりたいのですが、どうやら関数呼び出しはサポートされていないようです。{{ null|hogehoge() }}のようにnullを指定したり、true/falseを指定することもできるので、引数に空文字を渡すとデフォルトの挙動と変わったりする場合はこれらを使いましょう。

あとは通常のテンプレートです。もともとが次のようなテンプレートだとします。

<?php echo $form->renderFormTag(url_for('@article_update?id='.$article->getId())) ?>
  <?php echo $form->renderHiddenFields() ?>
  <?php echo $form->renderGlobalErrors() ?>
  
  <dl>
    <dt><?php echo $form['subject']->renderLabel() ?></dt>
    <dd>
      <?php echo $form['subject']->renderError() ?>
      <?php echo $form['subject']->render(array('size' => 60)) ?>
    </dd>
  </dl>
  
  <dl>
    <dt><?php echo $form['body']->renderLabel() ?></dt>
    <dd>
      <?php echo $form['body']->renderError() ?>
      <?php echo $form['body']->render() ?>
    </dd>
  </dl>

  <p>
    <input type="submit" value="Update" />
  </p>

</form>

これをTwigに落とすと次のようになります。

{{ form.renderFormTag(('@article_update?id=' ~ article.id)|url_for) }}
  {{ form.renderHiddenFields() }}
  {{ form.renderGlobalErrors() }}
  
  <dl>
    <dt>{{ form['subject'].renderLabel() }}</dt>
    <dd>
      {{ form['subject'].renderError() }}
      {{ form['subject'].render(['size': 60]) }}
    </dd>
  </dl>
  
  <dl>
    <dt>{{ form['body'].renderLabel() }}</dt>
    <dd>
      {{ form['body'].renderError() }}
      {{ form['body'].render() }}
    </dd>
  </dl>

  <p>
    <input type="submit" value="Update" />
  </p>

</form>

基本的には同じような感覚でオブジェクトを扱えると思います。何点か感覚の違う点があると思うので解説します。

まずはurl_forです。renderFormTag()メソッドに渡している引数は次のようになっています。

('@article_update' ~ article.id)|url_for

「~」(チルダー)とかPHPではあまり使わないですが、これはconcat、つまり文字列の結合です。「.」はメソッド呼び出しで使用されているので結合は~を使います。結合した全体を()で囲んでいます。こうしないと、'@article_id?id='とurl_for($article->getId())の結合になってしまうので文字列全体を()で囲んであげます。演算子の順番があるのかなどは知りません。ちなみに()のことを内部ではsetと呼んでいるようです。

あとさりげなく、$article->getId()をarticle.idとしちゃいましたが、内部でうまいことgetXxx()を呼び出すようになっています。

あとはsfFormField::render()に引数を渡している個所です。

{{ form['subject'].render(['size': 60]) }}

配列はで定義します。配列と連想配列ともにです。[1, 2, 3, 'key': 'value']のような感じだと思います。

後は一覧系でのループとかは

{% if articles|length %}
  <dl>
    {% for article in articles %}
      <dt>{{ article.subject }}</dt>
      <dd>
        {{ article.body }}
      </dd>
    {% endfor %}
  </dl>
{% endif %}

こんな感じです。実際にはarticle.body|nl2brとかやりたいのですが、デフォルトではできませんでした。というのも、Filterは関数を直接呼び出しているわけではなく、Twig側に登録している必要があるようで、url_forなどはsfTwigPluginが全部登録する処理を行ってくれているためできるのですが、nl2brは用意されていないので自前で登録する必要があるようです。この辺りはExtensionって名前のついたクラスを探してみるといいですよ!

あとはforeach ($articles as $i => $article)っぽいことしたければ、

{% for i in articles|key %}
  {% set article as articles[i] %}
  <li id="row_{{ i }}">
    {{ article.subject }}: {{ article.body|truncate_text }}
  </li>
{% endfor %}

のように、|keys Filterでキーをとってきてあげればできるんじゃないでしょうか。あとsetってのを使うと変数に代入ができます。ちなみにtruncate_textってのはTextHelperのtruncate_text()で、Twigに用意されているわけではないので注意してください。

(追記: http://www.twig-project.org/book/02-Twig-for-Template-Designersに乗っていたのですが、{% for key, value in articles %} とかできちゃうようです。とりあえずこのドキュメントを見ておくとだいたいわかるかと思います)


継承とかは、まだあまり調べていません。調べたら書きます。困ったら、lib/vendor/Twig/test/の中を眺めてみるといいと思います。

キャッシュはdebugモードの場合はされないようですが、prodで試したらちゃんとされていました。ビューのレンダリングのタイミングでキャッシュの有無をチェックするので、アクションのキャッシュとかはsymfonyの機構をそのまま使えると思います。たぶん。。。この辺りはsfTwigViewクラスを参照。sfConfig::get('sf_template_cache_dir')の直下にキャッシュが作られるのですが、この挙動を変えるためにsfTwigEnvironmentを継承したクラスを作ろうと思ったのですが、クラス名がsfTwigViewクラスにべた書きしているためそれらも書き換える必要がありそうです・・・。なおファイル以外のキャッシュはデフォルトではないっぽいですね。

id:cocoitiおにいちゃんいわく、

個人的にはレイアウト側のディレクトリにすべてのテンプレートファイルを配置して、アプリケーションが書き換えたいところ(formとかエラーメッセージ)だけ継承して使うという形を想定しているの

で現状の仕組みではそれを実現できない。

というか、元の作者もsfPHPView置き換えただけだよみたいなこといってたのでそれほどノリノリじゃないっぽいので、もうちょっとごにょごにょするか、がつっと別のものを作りたい。

sfTwigPluginを弄ってる - 個々一番のHTTP通信

とのことなので、もうちょっとかっこよくなるかもしれませんね!id:cocoiti++