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 }をコメントアウトしたらちゃんと動いたのでよかった!

PHPでTCPサーバを作ってみる

この記事はPHP Advent Calender 2012の2日目の記事になります、詳細は以下をどうぞ。

PHP Advent Calender 2012

フレームワークCMS的な記事が多いので、あまり参考例のないTIPSを書きたいと思います。

PHPでTCPサーバを立ててみる

PHPでTCPなサーバを作るとなると、socket関数やfsockopenなどを使った例を多く見かけます。
PHPというよりかはLinuxなネタになってしまいますが、ここではxinetdを使った例を書いてみたいと思います。

xinetdとは?

今回は、スーパーサーバーと呼ばれる xinetd の設定方法について説明していきます。スーパーサーバーとは、ポート監視用のデーモンプログラムで、あるポートに対してアクセスがあると、設定ファイル (/etc/xinetd.d/ 等) を元にポートに対応したサービス (ftp 等) を起動します。この際、ポートとサービスの関係は、/etc/services によって導かれます。

http://www.express.nec.co.jp/linux/distributions/knowledge/network/xinetd.html

ということで、xinetdとは定義した設定ファイルを元にサービスを起動してくれるデーモンということです。
これを使ってPHPでサーバを立ててみます。

まずはxinetdの設定ファイルを作成します。
xinetdがLinuxに入っていない場合にはyum install xinetdを行えばインストールできます。

/etc/xinetd.d/php-server

# サービス名を指定
service php-server
{
    # 設定の有効無効を指定します、noに設定すると有効になる
    disable = no

    # サービスの種類を指定、/etc/rpc, /etc/servicesに記述のないサービスを扱うにはUNLISTEDを指定
    type = UNLISTED

    # ソケットの種類を指定します、ここではTCPサーバを扱うのでstreamを指定
    socket_type = stream

    # 使用するプロトコルを指定、/etc/protocols内に記述されたプロトコルを指定
    protocol = tcp

    # サービスを実行するユーザーを指定
    user = root

    # マルチスレッドの有効無効を指定、noの場合シングルスレッド
    wait = no

    # サービスで利用するポート番号を指定
    port = 10000

    # ポートにアクセスされた際に起動するプログラムを指定
    server = /srv/server.php
}

上記の通り、xinetd設定自体は非常に簡単です。
portで指定したポート番号はiptablesで開放しておきましょう。

次に「server = 〜」で指定したプログラムを作成します。

/srv/server.php

#!/usr/bin/php -q
<?php
$stdin = fopen('php://stdin','r');

while (true) {
    $buffer = trim(fgets($stdin, 4096));

    if ('exit' === $buffer) {
        break;
    }

    echo $buffer . "\n";
}

プログラム自体は非常に簡単で、入力を受け取り単純に入力された値を表示するだけのプログラムです。
exitが入力された際にはプログラムが終了します。
作成したプログラムには実行権限をつけておきます。

chmod +x /srv/server.php

ここまで作成したらxinetdを再起動します。

/etc/init.d/xinetd restart

以上で完了です、では接続してみましょう。
下記の画像のように入力したものが応答で帰ってきます、最後にexitを入力し終了させています。

f:id:ryster:20121203070204j:plain

非常に簡単にPHPを使ったTCPサーバーが作成できました。
あとは実装次第で色々なサーバを作ることが出来るでしょう、勉強のためにハニーポットなんかを作ってみるのもいいかもしれません。

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系のこういう部分を見るとほんといらっときていたので、個人的にはこれはかなり嬉しい変更点だった。

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);

Symfony2のエラーページをカスタマイズしてみる

Symfony2でエラーページをカスタマイズしたかったのでやり方を調べてみた。

エラーページのカスタマイズ方法 | Symfony2日本語ドキュメント

エラーページをカスタマイズするには下記の2種類の方法があるみたい。

  1. エラーテンプレートのカスタマイズ
  2. 例外処理のカスタマイズ

1の方法は上記のドキュメントにも載っている通り非常に簡単にできるけど自由度が効かないので、今回は2の方法を試してみた。

例外処理をカスタマイズするには、まず初めに例外処理をするクラスをサービス登録します。
この時、kernel.exceptionイベント発生時に動くようにeventにはkernel.exceptionを設定します。

src/Acme/DemoBundle/Resources/config/services.yml

custom.exception.listener:
    class: Acme\DemoBundle\Listener\CustomExceptionListener
    arguments: ['@templating']
    tags:
        - { name: kernel.event_listener, event: kernel.exception, method: onKernelException }

ここまで設定したら次は実際に例外処理を行うクラスを作成します。
自分は管理画面の時だけこの処理を実行したかったので、URLが/adminの時だけ処理されるように設定しています。
また、Securityコンポーネントでユーザーと権限管理を行なっているので、権限が足りなかった際のページも用意しています。

src/Acme/DemoBundle/Listener/CustomExceptionListener.php

<?php
namespace Acme\DemoBundle\Listener;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Bundle\TwigBundle\TwigEngine;

class CustomExceptionListener
{
    /**
     * @var TwigEngine
     */
    protected $templating;


    /**
     * __construct
     *
     * @param \Symfony\Bundle\TwigBundle\TwigEngine $templating
     */
    public function __construct(TwigEngine $templating)
    {
        $this->templating = $templating;
    }

    public function onKernelException(GetResponseForExceptionEvent $event)
    {
        static $handling;

        $exception = $event->getException();

        if (true === $handling) {
            return;
        }

        if (method_exists($exception, 'getStatusCode')) {
            $code = $exception->getStatusCode();
        }

        // URLが/adminの時だけ実行
        if (0 === strpos($event->getRequest()->getPathInfo(), '/admin')) {
            $handling = true;

            // Securityコンポーネントで権限が足りないときの処理
            if ($exception instanceof AccessDeniedHttpException) {
                $message = $this->templating->render('AcmeDemoBundle:Exception:access_denied.html.twig');
            }

            // HTTP 404 Status
            if (isset($code) && 404 == $code) {
                $message = $this->templating->render('AcmeDemoBundle:404.html.twig', array());
                $response = new Response($message, $code);
                $event->setResponse($response);
            }

            if (isset($message)) {
                $response = new Response($message, $code);
                $event->setResponse($response);
            }
        }

        $handling = false;
    }
}

ここまで出来たら後はテンプレートを用意してあげれば完了。
もっと色々エラー内容を拾って処理したいって人は元の例外処理クラスを参考にしてみるといいかもしれません。
元々の例外処理をしているクラスは下記のファイルです。

src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php