Symfony2で動的値を自動でテンプレートにアサインする

タイトルの通り、サイトを作っているうえで動的値をテンプレートに自動的にアサインしたい事って結構あるんですよね。
例えば、ログイン機能がついているサイトではログイン中のユーザー情報とか。

Symfony2の場合、Controllerで毎回ログインしているユーザーを取得し、テンプレートに渡すのは非常に面倒です…
軽くググってみたけど、詳細な実装方法が記述されているページがすぐには見つからなかったので書いてみます。

ちなみに静的値をテンプレートに自動的にアサインしたい場合は下記に実装方法が載っています。


動的値の場合には上記ページにも記載されていますが、Twig Extensionを利用して実装します。
ログインしているユーザーをテンプレートに自動的にアサインする例を下記に書いてみます。

1. ログインユーザーを自動的にアサインするTwig Extensionクラスを作成

<?php

namespace Acme\DemoBundle\Twig\Extension;

use Symfony\Component\DependencyInjection\ContainerInterface;

class GlobalAssignExtension extends \Twig_Extension
{
    protected $container;

    /**
     * __construct
     *
     * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
     */
    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    /**
     * テンプレートにアサインする変数を設定
     *
     * @return array
     */
    public function getGlobals()
    {
        return array(
            'loginUser' => $this->getLoginUser(),
        );
    }

    /**
     * ログイン中のユーザーを取得
     *
     * @return mixed
     */
    protected function getLoginUser()
    {
        $token = $this->container->get('security.context')->getToken();

        if (is_null($token)) {
            return null;
        }

        return $token->getUser();
    }

    /**
     * Returns the name of the extension.
     *
     * @return string
     */
    public function getName()
    {
        return 'global_assign_extension';
    }
}

動的値をテンプレートに自動的にアサインするにはTwigExtensionを継承し、getGlobalsメソッドの返り値として配列を返すとKeyが変数名となりテンプレートで利用することが可能です。
上記のコードではDIコンテナを利用して、ログインしているユーザーを取得してきています。

このクラスを作成した後はサービスとして上記クラスを登録します。

2. 作成したTwig Extensionのサービス登録 (services.yml)

global.assign.twig.extension:
    class: Acme\DemoBundle\Twig\Extension\GlobalAssignExtension
    arguments: [ @service_container ]
    tags:
        - { name: twig.extension }

上記のarguments: [ @service_container ]の行はコンストラクタにコンテナを渡す設定です。
Twig Extensionのサービスを登録するうえで注意が必要なのは、必ずtwig.extensionのタグを指定して下さい。
これがないと自動的に呼ばれることはなく、テンプレートでも変数を利用することは出来ません。

実装は以上でここまで完了すると、どこのテンプレートでもloginUserという変数が利用可能です。

Symfony2.1によるmonologの設定

あるアプリケーションをSymfony2.0系からSymfony2.1に移行していた時にmonologの設定で少しはまったので備忘録を。
Symfony2.0系では下記のようなmonologの設定をしていて、正常にdebug以上はログに記述し、error以上のログはメールで受け取れていた。

monolog:
    handlers:
        main:
            type:         fingers_crossed
            action_level: debug
            handler:      grouped
        # streamed, bufferedに渡す
        grouped:
            type:    group
            members: [streamed, buffered]
        # debug以上をログファイルに書き込み
        streamed:
            type:  stream
            path:  %kernel.logs_dir%/%kernel.environment%.log
            level: debug
        # error以上をメールする
        buffered:
            type:    buffer
            handler: swift
        swift:
            type:       swift_mailer
            from_email: %system_mail_from%
            to_email:   %log_mail_to%
            subject:    "エラーだよ!"
            level:      error

上記の設定のまま、Symfony2.1に移行するとメールが一切受け取れなくなった。
ソースを追ってみると、SwiftMailer自体にはログ情報は渡っているが、最終的に下記のソースの部分まで処理が行き送信自体されていない。

vendor/swiftmailer/swiftmailer/lib/classes/Swift/Plugins/MessageLogger.php

<?php
    ~

    /**
     * Invoked immediately after the Message is sent.
     *
     * @param Swift_Events_SendEvent $evt
     */
    public function sendPerformed(Swift_Events_SendEvent $evt)
    {
    }

しょうがないので、色々ネットで調べてみると下記の記事を見つけた。

In fact, the problem is a known issue and is present when using a buffered swift_mailer Monolog handler with a memory spool.
As a workaround, to avoid any problems, comment the "spool: { type: memory }" in config.yml

See the following threads:
https://github.com/Seldaek/monolog/issues/154
https://github.com/symfony/symfony-standard/issues/425

http://forum.symfony-project.org/viewtopic.php?t=47160&p=167193

結局、ここに書いてあるconfig.ymlのswiftmailerの設定でspool: { type: memory }をコメントアウトしたらちゃんと動いたのでよかった!

Doctrine2.3がすごくよくなってる

Symfony2.1を使っていて感動した!
Symfony2.0系ではformでEntityを利用した場合、同じform内に同一のEntityがあった場合でもデータは別途取得される仕様だった。

<?php
    $builder
            ->add('product1', 'entity', [
                'class' => 'Acme\\DemoBundle\\Entity\\Product',
            ])
            ->add('product2', 'entity', [
                'class' => 'Acme\\DemoBundle\\Entity\\Product',
            ])
            ->add('product3', 'entity', [
                'class' => 'Acme\\DemoBundle\\Entity\\Product',
            ]);

こんな感じでformを生成するとdoctrine2.1系では3つの同一のDQLを発行し、formにバインドする。
Symfony2.1(Doctrine2.3)系ではこれが1つのDQLのみ発行し、使いまわされるように効率化されている。

同じform内で同一のEntityを利用することはそんなにあるわけではないが、Doctrine2.1系のこういう部分を見るとほんといらっときていたので、個人的にはこれはかなり嬉しい変更点だった。

Twigでエンティティオブジェクトにアクセスする際の注意点

Twigテンプレート内でエンティティオブジェクトにアクセスする際にforで回しながら処理したかったんですが、エラーが出て見事に少しハマった。
下記のようにtypesに入っている値を利用して、エンティティオブジェクトにアクセスするとエラーが出る。

{% for type in types %}
    {% set column = type ~ 'Status' %}
    {{ entity[column] }}
{% endfor %}

Twigのドキュメントを読んでみたところ、attribute関数というものを発見した。
http://twig.sensiolabs.org/doc/templates.html#variables

attribute関数を使って試してみるとうまくいった。

{% for type in types %}
    {% set column = type ~ 'Status' %}
    {{ attribute(entity, column) }}
{% endfor %}

なぜこれで動くのかは不明なのでattribute関数の中身を後で読んでみる。

SwiftMailerでRFC違反のメールアドレスでもメールを送信したい

Symfony2の標準メールライブラリはSwift_Mailerが使われている。

メール送信を試していたが、どうも送信前にメールアドレスのチェックを行うらしくRFC違反しているメールアドレスだと例外が投げられて送れない…

例外が投げられる部分は下記の箇所

vendor/swiftmailer/lib/classes/Swift/Mime/Headers/MailboxHeader.php

  private function _assertValidAddress($address)
  {
    if (!preg_match('/^' . $this->getGrammar()->getDefinition('addr-spec') . '$/D',
      $address))
    {
      throw new Swift_RfcComplianceException(
        'Address in mailbox given [' . $address .
        '] does not comply with RFC 2822, 3.6.2.'
        );
    }
  }

とりあえずこのチェックを回避するオプションはないっぽいので、throwの部分をコメントアウトして強制的に送るしかないっぽい

Symfony2でTwigを文字列から読み込みたい

昼間にTwigローダーを処理中に変更し、文字列をTwigに渡してリソースを生成する方法を書いたけど、
単純なテンプレート機能として使うのであれば全然問題ないが、form_widgetとか使う場合だと昼間の方法だと無理だった。

とりあえず文字列ベースのTwigLoaderでform_widgetとかも使えるバンドルを作ったので置いておきます。
Twigの内部処理の勉強にかなりなったなー

ryster/HaouTwigBundle · GitHub

利用方法は下記の通り。

// ソース
$source = '{% form_theme form "AcmeDemoBundle:Form:form_layout.html.twig" %}  {{ form_rest(form) }}';

$render = $this->get('haou_twig')->render($source, $context);
return new Response($render);

Symfony2のコントローラ内でTwigローダーを変更する

コントローラでの処理中にファイルからではなく文字列からTwigに渡してレスポンスを生成したかった。
普通にTwig_Environmentを生成してやれば簡単に出来たんだけど、assetsとか使いたくてExtensionも引き継ぎたかったので途中で変更する方法を取った。

こんな感じ。

$this->get('twig')->setLoader(new \Twig_Loader_String());
$resource = $this->get('twig')->render("{{ asset('hogehoge') }}");

return new Response($resource);