プログラミング - THIS IS IT !

より良い開発をすべく日々奮闘しているプログラマーのブログです。設計に興味があります。主にPHPネタを書いてます 日本Symfonyユーザー会

Symfonyでメールのインテグレーションテストを行う

f:id:okapon_pon:20151205123050p:plain

この記事はSymfony Advent Calender 2018 15日目の記事です。

はじめに

Symfonyでメールのテストを行う方法ですが、公式ドキュメントにもある通りファンクショナルテスト内でプロファイラーを利用したメールのテストは行うことができます。

symfony.com

しかしコントローラのテストまで行うのはややオーバスペックであり、メール単体でテストを行いたいという場合もあると思います。

特にtwigでメール本文を記述していてif文で表示の出し分けを行っていたりすると、実際にメールを送るのは大変ですし簡単にテストコードが書けたらいいなと思うケースはあるのではないかと思います。

本記事はそのようなケースで参考になる内容となっているのではないかと思います。

実は以前似たような記事を書いたことがありました。

以前の記事 okapon-pon.hatenablog.com

しかしながら、MailServiceを継承する方法は制約も多く実用性に欠けますし、もっとスマートな方法があったため改めてご紹介したいと思います。

メールの検証に Swift_Plugins_MessageLogger を使う

まずはじめに、この内容はSymfony3.4ベースです。

Symfonyを使っている方なら既に知っていると思いますが、メールクラスの実態は \Swift_Message クラスですね。 以降 \Swift_Massge クラスを Messge クラスと省略させてもらいます。

プロファイラーを利用したテストはできると先程書きましたがプロファイラーは送信したMessageクラスを保持しているので、そこからMessageクラスを取得してきてメールの検証を行うことができます。

そのプロファイラーがどうやってMessageクラスの情報を保持してきているかというと、MessageDataCollector クラスを通して情報を収集していて DataCollector はさらに \Swift_Plugins_MessageLogger クラスから情報を集めます。

Swift_Mailer クラスからMessageが送信されるタイミングで \Swift_Plugins_MessageLogger クラスはMessageを保持するようになっています。

これらはSymofnyの環境がdevもしくはtestの場合に行われます。

そのため既にMessageクラスを収集している、この Swift_Plugins_MessageLogger を利用すれば簡単にメールの検証を行うことができます。

以下では実際の利用コードを示します。

サンプルコード

Symfony3.4

<?php

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class MyMailTest extends WebTestCase
{
    public function testSendMail()
    {
        $kernel = self::bootKernel();
        $mailer = $kernel->getContainer()->get('mailer');
        $logger = $kernel->getContainer()->get('swiftmailer.mailer.default.plugin.messagelogger');

        // 実際は実コード側でメール送信を行うが、ここでは分かりやすくテストコード中に記述してます
        $message = new \Swift_Message();
        $message->setTo('to@example.com', 'Aさん');
        $message->setFrom('system@example.com');
        $message->setBody('本文です。');
        $mailer->send($message);

        // メールを検証します
        $this->assertSame(1, $logger->countMessages());
        /** @var \Swift_Message $message */
        $message = $logger->getMessages()[0];
        $this->assertSame(['to@example.com' => 'Aさん'], $message->getTo());
        $this->assertContains('本文です', $message->getBody());
    }
}

未確認ですが、Symfony4.1以降であればprivateなサービスクラスも取得できる方法があるため以下のようなコードでも動くと思います。

<?php

class MyMailTest extends WebTestCase
{
    public function testSendMail()
    {
        self::bootKernel();

        // gets the special container that allows fetching private services
        $container = self::$container;

        $mailer = $container->get(\Swift_Mailer::class);
        $logger = $container->get(Swift_Plugins_MessageLogger::class);

         // ...略
    }
}

参考

New in Symfony 4.1: Simpler service testing (Symfony Blog)

このサンプルコードだけ見ればどうということはないのですが、自分はここに到達するまでにわりと遠回りしてしまってました。。。

SwiftMailerBundleのリファレンスにloggingという項目があり、そこからヒントを得ました。 Mailer Configuration Reference (SwiftmailerBundle) (Symfony 3.4 Docs)

まとめ

ファンクショナルテスト(Controllerのテスト)でなくてもサービスコンテナを用いたメールのインテグレーションテストを簡単に書くことができます。

メールのテストを行う場合、ブラウザをポチポチして実メールを送る必要もなくテストコードでさくっと検証できるので開発がはかどります。

以上です。

Symfonyフレームワークにおけるデザインパターン活用

この記事はSymfony Advent Calender 2018 5日目の記事です。

はじめに

今回はSymfonyフレームワークにおいて、どのようにデザインパターンが使われているかについてのお話します。 Symfonyは既に4系リリースされてますが、ここではSymfony3.4系 Standard Editionをベースに解説しています。

Symfony4系でもコードベースはそれほど変わっていないと思います。

これまでにデザインパターンをなんとなく勉強したことはあるけど、実務で活かせた経験がないない。メリットがいまいち分からなかった。

といった方が「なるほど。そういう使い方をするのか」と気づいてもらえればうれしいです。

それでは本題

Strategyパターン

Strategyパターンはオープンクローズドの原則に則っていて、ロジック(ここではStragetyと呼んでいます)の切り替えや追加を簡単に行えるようするデザインパターンです。

GofのStrategyパターンは、最も有名なデザインパターンがの1つではないかと思います。

通常フレームワークでは利用する「ロジックを簡単に切り替え」たり、「拡張点があらかじめ提供」してくれています。

「ロジックの切り替えとは?」と思った方もいるかもしれませんが、例えばMySQLPostgreSQLといった自分が使いたいデータベースを自由に選択することができます。

他にもキャッシュの保存先、セッションの保存先など実装者はFWが用意してくれた多くの選択肢の中から自由に選ぶことができます。

1つの選択肢しか取れないとなると非常に困りますよね。

Symfonyにも勿論このような機能が提供されています。

configファイルに記述することで簡単にロジック(ストラテジー)を選択できるようになっています。

そういった、ロジックの切り替えを行う機能を提供するのにうってつけのパターンが「ストラテジーパターン」になります。

また、ここでは直接触れませんが自分の要望に合うものがなければ自作のストラテジーを用意して追加することもできます。

説明が長すぎても伝わらないと思うので、実際にコードを見ていきましょう。

例に取り上げるのはSessionHandlerです。

SessionHandlerInterface 自体はPHPで標準に提供されています。

PHP: SessionHandlerInterface - Manual

そしてSymfonyにおける抽象クラスの定義がこちら。

<?php

abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
{
    /**
     * @param string $sessionId
     *
     * @return string
     */
    abstract protected function doRead($sessionId);

    /**
     * @param string $sessionId
     * @param string $data
     *
     * @return bool
     */
    abstract protected function doWrite($sessionId, $data);

    /**
     * @param string $sessionId
     *
     * @return bool
     */
    abstract protected function doDestroy($sessionId);
}

各サブクラスではこれらの抽象メソッドが実装していくこととなります。(この部分だけ見ると)

サブクラスにはsessionをDBに保存する PdoSessionHnadlerMemcachedに保存する MemcachedSessionHandler 等があります。

愚直にコードを書くと以下のような利用イメージとなります。

<?php

// DBに保存したい場合
$pdo = new \PDO(...);
$sessionStorage = new NativeSessionStorage([], new PdoSessionHandler($pdo));
$session = new Session($sessionStorage);

// Memcachedに保存したい場合
$memcached = new \Memcached();
$memcached-> addServers(...);
$sessionStorage = new NativeSessionStorage([], new MemcachedSessionHandler($memcached)));
$session = new Session($sessionStorage);

PdoSessionHnadler, MemcachedSessionHandler が各ストラテジーとなっており実装者は自由に選択することができます。

以下は PdoSessionHnadler を用いた例ですが、Symfonyでは下記の通りconfigを設定することで利用できます。

config.yml

framework:
    session:
        handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler
services:
     Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
        arguments:
            - 'mysql:dbname=mydatabase, host=myhost'
            - { db_table: 'sessions', db_username: 'myuser', db_password: 'mypassword' }

ここまで見てきて分かることは、実際のところこのSessionクラスにおいては1つのストラテジーのみが利用できることとなっています。

Symofnyにはコンパイルフェイズ(Bundleの起動)があり、選択された1つのSessionHandlerがDIコンテナによって注入されることになります。

このSessionHandlerはアプリケーションの実行時に変える必要がないため、configの設定に応じた唯一のSessionHnaderだけが注入されているのは理にかなっていると思います。

余談: 一方、実務で書くStrategyパターンは実行時(ランタイム)で決まることが多いと思います。

例)

  • ロイヤルカスタマーであれば割引率が大きくなる
  • Adminユーザーの場合だけ出力フォーマットを変える など

実行時に使うストラテジーを変更したい場合には、下記記事が参考になりますので見てみるとよいと思います。

medium.com

余談2: Template Methodパターン

https://github.com/symfony/symfony/blob/3.4/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php

<?php

abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
{
    abstract protected function doRead($sessionId);

    abstract protected function doWrite($sessionId, $data);

    abstract protected function doDestroy($sessionId);
}

先程出てきた AbstractSessionHandler ですが、これはTemplate Methodパターンと見ることもできます。

wikipediaによると Template Method パターンとは以下のようなものです、

ある処理のおおまかなアルゴリズムをあらかじめ決めておいて、そのアルゴリズムの具体的な設計をサブクラスに任せること

処理の流れはAbstractSessionHandlerにかかれていて、具体的な処理は PdoSessionHnadlerMemcachedSessionHandler といったサブクラスで実装されています。

Template Method パターンは単に継承を活用しただけなので、それほど難しいパターンではなく実は無意識のうちにわりと書いていたりするのではないかと思います。

Decoratorパターン

既存のクラス/インターフェースを変更することなく元のクラスに機能追加 できるようにする設計パターンです。

個人的にとても好きなデザインパターンの1つです。

以前、別の記事でSymofnyのコントローラーの実行時間の計測にはDecoratorパターンが利用されていると紹介したことがあります。

okapon-pon.hatenablog.com

↑ 余談を項を参照

今回は別の箇所で、Decoratorが利用されている部分を見てみましょう。

例として取り挙げるのは HttpKernel です。

https://github.com/symfony/symfony-standard/blob/3.4/web/app.php#L14

$kernel = new AppKernel('prod', false);
$kernel = new AppCache($kernel); // AppCacheがデコレーター

$response = $kernel->handle($request);
$response->send();

AppKernelに対しリバースプロキシのように動作する Cache機能を付け加えたのがAppCacheクラスです。

同一インタフェースである handle() メソッドがそのまま使え既存のコードには影響がありません。単にCacheの機能が追加されただけとなっています。

Cacheの仕組み自体はここでは解説しませんので、以下ご参照ください。

symfony.com

Decoratorパターンが素晴らしいのは、もし仮に独自拡張のMyKernelクラスを作りたくなったとしても、MyKernelがKernelインタフェースさえ実装していれば、いくらでもクラスを重ねることができ、機能の追加だけを行うことができます。

$kernel = new AppKernel();
$kernel = new AppCache($kernel);
$kernel = new MyKernel($kernel);
$response = $kernel->handle($request);

非常にエレガントな設計だと思いませんか?

Null Objectパターン

Null ObjectパターンはStrategyパターンの応用と考えてよいと思います。

同一インタフェースを実装した何も仕事をしないオブジェクトに差し替えることで、利用側のコードを変えることなく機能を無効化することができるデザインパターンとなります。

例えば、Symfony Standard Editionのテスト実行時には Swift_NullTransport が使われていたりします。

他にもPsr\Log\NullLogger というものもありますね。

このようなクラスに差し替えることでtest環境ではメールを送信しない、ログを記録しないといったことが行なえます。

Null Objectパターンを利用しない場合には、

if ($strategy === null) {
    return;
}

とガード節を書かなけらばならずあまりいい感じはしないですね。

では実際に Null Objectパターンが利用されているコードを紹介したいと思います。

Swift_Mailerクラスをみてください。このクラスはでは Swift_Transport というインタフェースにのみ依存しています。

https://github.com/swiftmailer/swiftmailer/blob/master/lib/classes/Swift/Mailer.php

<?php

class Swift_Mailer
{
    private $transport;

    public function __construct(Swift_Transport $transport)
    {
        $this->transport = $transport;
    }

    public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
    {
        // ....略
            $sent = $this->transport->send($message, $failedRecipients); // interfaceに定義されているsendメソッドに依存
     }

一方でNullObjectパターンが適用された Swift_Transport_NullTransport クラスをみてください。

sendメソッドは実際のメール送信は行わないダミーメソッドとなっています。

https://github.com/swiftmailer/swiftmailer/blob/master/lib/classes/Swift/Transport/NullTransport.php

<?php

class Swift_Transport_NullTransport implements Swift_Transport
{
    public function send(Swift_Mime_Message $message, &$failedRecipients = null)
    {
        // メール送信処理は実際には行わず、ただ送信数をカウントして返却するだけのダミーメソッド
        $count = (
            count((array) $message->getTo())
            + count((array) $message->getCc())
            + count((array) $message->getBcc())
        );

        return $count;
    }
}

SwiftMailerBundel内で行われている、SymfonyのDIの仕組みについてはここでは詳しく触れませんが、下記のymlファイルへの記述だけで Swift_NullTransport が使われるようになります。

Swift_NullTransportSwift_Transport_NullTransport を継承したクラスです。

# config_test.yml
swiftmailer:
    disable_delivery: true

Commandパターン

Command オブジェクトは、動作とそれに伴うパラメータをカプセル化したパターン

Commandパターンのメリットは2点あります。

  • 実際の処理を細かく分離できること
  • 命令を別で行えること(個別実行もできるし Unixのパイプのように連結して実行することもできる)

Commandパターンの実装面での1つの見方としては、Template Methodパターンの派生でより目的特化したものと考えることもできます。

Template Methodパターンでは複数のabstractメソッドが定義されている場合がありますが、Commandパターンとして定義されているのは execute() のみとなります。

開発者はCommandクラスのサブクラスで必要な処理を execute() メソッドに実装します。

ではSymofnyのどこで実際に使われているかですが、もちろんCommadクラス(Consoleコンポーネント)ですね。

symfony.com

ドキュメントにさらっとexecute() を実装するように書かれているのであまり意識したことはないとは思います。

Commandクラスのコードを見てみましょう。

namespace Symfony\Component\Console\Command;

class Command
{
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        throw new LogicException('You must override the execute() method in the concrete command class.');
    }
}

実はexecute()メソッドはabstractメソッドではないので、厳密にはCommandパターンとは呼べないかもしれません。

ですが、Commandコンポーネント開発者の意図としてはこのデザインパターン通りだと思います。

※注 Symofny Commandコンポーネントの場合、実質configure()メソッドの実装もほぼ必須だと思います。

Commandはそれぞれが独立しているので、単独でコマンド実行することができますし

$ php app/console demo:greet Fabien
$ php app/console my:greet okapon

コマンド自体が分離しているので、以下のドキュメントのようにあるコマンドから別のコマンドを呼び出すというようなことも簡単に行なえます。

How to Call Other Commands (Symfony Docs)

その他

ここでは簡単に紹介

Compositeパターン

Formコンポーネントで実際に使われています。

よくあるCompositeパターンの例では、ディレクトリ構造たどっていくサンプルコードがありますが、このFormObjectもまさにそのようなTree構造で問題を扱っています。

まとめ

ぜひSymfonyのコードリーディングにチャレンジしてみてください。

Symfonyでloggerが呼ばれた箇所にPHPStormでファイルジャンプしやすくする

f:id:okapon_pon:20151205123050p:plain

この記事はSymfony Advent Calender 2016 23日目の記事です。

はじめに

今回はloggerについてのお話です。

何か不測の事態にそなえて logger で記録するということは、本番環境でアプリを運用していくにあたり取り組まれていると思います。

ただ、何かログが吐かれて「やばい調査しないと!」となった時に、ログのメッセージでgrepして、ファイル探して開いて、該当行探してっとなると結構面倒だったりします。

モチベーション

そのため今回のモチベーションは以下の通りです。

  • errorログが記録された(loggerが呼びだされた)箇所のコードを素早く調べたい
  • PHPStormで該当ファイル行を素早く開いて調査したい

f:id:okapon_pon:20161223215003p:plain

実はPHPStormのNavigationのfile検索は最後に「:行番行」と入れると該当行にジャンプすることができます。

なのでloggerで「呼び出し元のファイル名 +:番号」をログに出力するようにしれあげれば、ログをコピーしてPHPStormに入力してすぐ該当ファイルにアクセスできるわけです。

Symfonyのloggerを呼んだ箇所にPHPStormでファイルジャンプしやすくする」という内容についての記事ですが、PsrLoggerInterfaceを実装したLoggerを利用したその他フレームワークでも利用可能なテクニックとなっていると思います。

やったこと

PsrLoggerInterfaceのメソッドであるerror()関数は第2引数にcontextという配列を受け取ることができます。

contextはloggerが呼び出されたときの状況を記録するために利用することができます。

(例)ユーザー情報の更新に失敗した場合のロギング

$this->error('Update failed', ['user_id' => $user->getId());

今回の場合はloggerに行番号付きでファイルパスを渡します。 以下のイメージです。

$this->get('logger')->error('Error occurred!', ['caller' => 'path/to/SampleFile.php:100']);

loggerのメソッドが呼ばれた時に、この'caller' という情報がデフォルトで入るようにするのが今回の目的です。

ではどうやって呼び出し元を取得してくるか?

PHPには debug_backtrace() という関数があります。

PHP: debug_backtrace - Manual

<?php
function foo()
{
    $trace = debug_backtrace();
    var_dump($trace);
}
foo();

結果

/PhpstormProjects/test.php:5:
array(1) {
  [0] =>
  array(4) {
    'file' =>
    string(47) "/PhpstormProjects/test.php"
    'line' =>
    int(7)
    'function' =>
    string(3) "foo"
    'args' =>
    array(0) {
    }
  }
}

配列を見てみると 呼び出し元のファイル名や行番号が配列に入っていることが分かります。これを利用して呼び出し元のファイル行数を取得します。

詳細は後で掲載するサンプルコードを参照ください。

どうやって実装するか

既にloggerを利用している箇所が沢山あるのであれば、別のメソッドを足した新しいクラスを作るという選択肢はないでしょう。PsrLoggeerInterfaceを損なわない形でloggerクラスを差し替えたいところです。

幸いLoggerについてはPSR-3にて PsrLoggerInterfaceが定められており、十分安定したInterfaceであると考えられるのでこれに従うのが筋がいいと考えられるます。

1番思いつきそうな実装方法はサブクラスを作る方法

Interfaceの変更さえなければ既にloggerを利用している箇所も問題なく動きます。

悪くはないと思いますが、継承という手段を用いる以上親クラスの実装が置き換わったりすると壊れる可能性はあると思います。 (Loggerだとあまりないとは思いますが…)

そこでデコレータ

デコレータパターンはGoFデザインパターンの一つで、既存のインターフェースを変更することなくクラスに機能追加することができるようにする設計パターンです。

個人的にとても好きなデザインパターンの1つです。

今回やりたいことはloggerの機能はそのままで「'caller'という呼び出し元の付加情報をデフォルトで付与する」という機能の追加になります。 デコレーターパターンを使うのに向いているのではないでしょうか。

実装方法

実はSymfonyのDIには既存のServiceクラスをお手軽にデコレートする方法が提供されています。

http://symfony.com/doc/current/service_container/service_decoration.html

今回はloggerを置き換えてやればよいので以下の様な設定となります。

  app.traceable_logger:
    class: AppBundle\Logging\TraceableLogger
    decorates: logger
    arguments:
      - "@app.traceable_logger.inner"
      - "%kernel.root_dir%"

decorates オプションを指定することで元のlogger サービスを app.traceable_logger でデコレートすることをDIコンテナに伝えることができます。 その際元の logger サービスは @app.traceable_logger.inner としてリネームされ、引き続きコンテナ内に残り続けます。

今回は元のloggerを利用したいのでargumentsで@app.traceable_logger.inner を渡してあげます。

肝心のTraceableLoggerクラスですがPsr/Log/LoggerInterfaceSymfonyDebugLoggerInterfaceを実装します。

Symfonyの場合loggerの実体はSymfony\Bridge\Monolog\Logger なので、DebugLoggerInterface も実装してあげた方が良いでしょう。

コードは以下の通りとなります。

<?php

namespace AppBundle\Logging;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;

class TraceableLogger implements LoggerInterface, DebugLoggerInterface
{
    const NUM_OF_TRACE = 1;

    /**
     * @var LoggerInterface
     */
    private $logger;
    private $projectRootDir;

    /**
     * @param LoggerInterface $logger
     * @param string $kernelRootDir
     */
    public function __construct(LoggerInterface $logger, $kernelRootDir)
    {
        $this->logger = $logger;
        $this->projectRootDir = realpath($kernelRootDir . '/../');
    }

    /**
     * {@inheritdoc}
     */
    public function emergency($message, array $context = [])
    {
        $context['caller'] = $this->getCallerPath();
        $this->logger->emergency($message, $context);
    }

    /**
     * {@inheritdoc}
     */
    public function alert($message, array $context = [])
    {
        $context['caller'] = $this->getCallerPath();
        $this->logger->alert($message, $context);
    }

    /**
     * {@inheritdoc}
     */
    public function critical($message, array $context = [])
    {
        $context['caller'] = $this->getCallerPath();
        $this->logger->critical($message, $context);
    }

    /**
     * {@inheritdoc}
     */
    public function error($message, array $context = [])
    {
        $context['caller'] = $this->getCallerPath();
        $this->logger->error($message, $context);
    }

    /**
     * {@inheritdoc}
     */
    public function warning($message, array $context = [])
    {
        $context['caller'] = $this->getCallerPath();
        $this->logger->warning($message, $context);
    }

    /**
     * {@inheritdoc}
     */
    public function notice($message, array $context = [])
    {
        $context['caller'] = $this->getCallerPath();
        $this->logger->notice($message, $context);
    }

    /**
     * {@inheritdoc}
     */
    public function info($message, array $context = [])
    {
        $context['caller'] = $this->getCallerPath();
        $this->logger->info($message, $context);
    }

    /**
     * {@inheritdoc}
     */
    public function debug($message, array $context = [])
    {
        $context['caller'] = $this->getCallerPath();
        $this->logger->debug($message, $context);
    }

    /**
     * {@inheritdoc}
     */
    public function log($level, $message, array $context = [])
    {
        $context['caller'] = $this->getCallerPath();
        $this->logger->log($level, $message, $context);
    }

    /**
     * {@inheritdoc}
     */
    public function getLogs()
    {
        if ($this->logger instanceof DebugLoggerInterface) {
            return $this->logger->getLogs();
        }

        return [];
    }

    /**
     * {@inheritdoc}
     */
    public function countErrors()
    {
        if ($this->logger instanceof DebugLoggerInterface) {
            return $this->logger->countErrors();
        }

        return 0;
    }

    /**
     * @return string
     */
    private function getCallerPath()
    {
        $trace = debug_backtrace();
        $callerInfo = $trace[self::NUM_OF_TRACE];
        if($callerInfo['file'] && $callerInfo['line']) {
            // project root 以下のパスで返す
            return $this->normalizePath($callerInfo['file']) . ':' . $callerInfo['line'];
        }

        return '';
    }

    private function normalizePath($realPath)
    {
        return str_replace($this->projectRootDir . '/', '', $realPath);
    }
}

LoggerInterfaceには、emergency, alert, critical… etc. と沢山のメソッドがあり、デコレートしなければならないメソッドが多いのはちょっとあれですが…

実際に呼び出し元のファイルパスを取得しているのは getCallerPath() メソッドの部分になります。

少し工夫している点としてdebug_backtrace()で得られるファイルパスは絶対パスのためそれをプロジェクトルート起点のパスに変更しています。

絶対パスはproduction環境とdevelop環境で異なるでしょうし、PHPStormの検索ではプロジェクトルート以下のパスで十分です。

実際に動かしてみると以下のようなlogが得られます。

f:id:okapon_pon:20161224024356p:plain

ファイル検索を実際にしてみると、該当行に直接ジャンプすることができました。

f:id:okapon_pon:20161224025959p:plain

このようにしてデコレーターを活用してあげると既存の機能も損なわず、またコードの利用側にも影響を与えることなく機能追加をすることができます。

デコレーターパターンはインターフェースのパワーを利用した素晴らしい設計手法ですね。

まとめ

  • PHPStormではファイル名の後に「:行番号」でファイルの該当行にジャンプすることができる
  • 呼び出し元ファイルは debug_backtrace() で取得できる
  • SymfonyのDIにはDecorate 機能が用意されており、既存のサービスクラスを簡単に拡張することができる

今回は呼び出し元のファイルパスを取得して、それをloggerのcontextに付加情報として追加するという内容でした。

他にもloggerで常にアクセス元のURLを出すようにしたり、ログインユーザーのIDを出したりとかも簡単に実装できますね。

余談

余談ですが、Symfonyフレームワーク内ではデコレーターが普通に利用されています。

ControllerResolverはdev環境で動かした場合にはTraceableControllerResolverによってデコレートされ、コントローラがresolveされるまでの時間を計測する機能が追加されています。

【参考】

https://github.com/symfony/symfony/blob/5129c4cf7e294b1a5ea30d6fec6e89b75396dcd2/src/Symfony/Component/HttpKernel/Controller/TraceableControllerResolver.php#L58

計測結果はプロファイラーでパフォーマンスとして見ることができます。

Symfonyでダイナミックにconfigリソースを読み込む方法

f:id:okapon_pon:20151205123050p:plain

この記事はSymfony Advent Calender 2016 7日目の記事です。

実は以前Symfony meetupのLTで話した内容(スライド作ってない)ですが、改めてご紹介してみたいと思います。


ここから少し宣伝

宣伝となりますがSymfony meetup ではLT発表枠がありまして、そこではネット上に(特に日本語では)あまり公開されてないノウハウなどが聞くことができるかもしれません。

↓興味がありましたら、meetupの方への参加もよろしくお願いします。

symfony.connpass.com

宣伝終わり


では本題

Symfonyのconfigリソース

Symfonyでは config.yml などの設定ファイルをresourceと呼んでいます。 assetやview、translationなどもresourceと呼びますがここではconfigリソースについて解説します。

Symfonyのコンフィグレーションドキュメント Configuring Symfony (and Environments) (Symfony Docs)

上記ドキュメントから引用&抜粋

Configuration Formats

Throughout the chapters, all configuration examples will be shown in three formats (YAML, XML and PHP). YAML is used by default, but you can choose whatever you like best. There is no performance difference:

The YAML Format: Simple, clean and readable; XML: More powerful than YAML at times & supports IDE autocompletion; PHP: Very powerful but less readable than standard configuration formats.

You can also load XML files or PHP files.

ドキュメントに記載の通り、Symfonyの設定ファイルはyml以外にもxmlphpファイル、またiniファイル(下記リンク参照)も読み込むことが可能です。

【参考】

Loading Resources (The Config Component - Symfony Docs)

本記事で扱う内容

説明にもある通りymlはシンプルに書けるし、symfonyの標準フォーマットでもあるので、基本的にyamlで書くことがほとんどだと思いますが、ここではphpフォーマットについて解説したいと思います。

phpフォーマットの場合なconfigを設定できる点が他のフォーマットと異なります。xmlやiniとyamlの違いはあくまでフォーマットの違いでしかなく、静的な設定ファイルという点においては何も変わりません。

phpファイルをリソースとして読み込む方法

例えば、コンテナにパラメータを設定したい場合には以下のように書けます。

# app/config/config.yml
imports:
    - { resource: 'parameters.php' }
<?php

$container->setParameter('key', 'value');

これだけではあまりに簡単過ぎて「yamlで十分じゃないか」という話になってしまうので、これからphpファイルををリソースとして読み込むことのメリットを解説したいと思います。

phpファイルをリソースとして利用するメリット

yamlではできない動的(ダイナミック)な処理を行うことができる

これに尽きます。

phpファイルであるため、パラメーターの設定や置き換え、特定条件下で実行するなどどんな処理でも行うことができます。

いくつか実例

基本編

1. 環境変数から値を取得してパラメーターをセットする

サービスコンテナで外部パラメータをセットする方法 | Symfony2日本語ドキュメント

Symfony は、 SYMFONY__ の接頭辞の付いたあらゆる環境変数を読み取り、サービスコンテナのパラメータとしてセットすることができます。

こちらのドキュメントにも書いてありますが、元々の機能として SYMFONY__ というprefixがついた環境変数は自動でパラメーターとして設定されます。

しかし、環境変数がこういった命名規則に則っていない場合もあると思います。(複数アプリで同じ値を読まないといけない場合など) その場合もphpなら値を入れられますね。

この方法を用いれば環境変数以外にも外部から取得してきた値を利用することも可能です。

2. 特定ファイルが存在する時にだけ設定を読み込む

<?php
$file = __DIR__ . '/some_file.php';

if (file_exists($file)) {
    // overrideしたいパラメーターをセット
    $params = require $file;
    foreach ($params as $key => $value) {
        $container->setParameter($key, $value);
    }
}

単に設定がphpファイルで書かれている場合にも利用できます。 あまり実用的ではないですが、レガシーコードに依存している場合などどうしようもない状況下では選択肢としてはあり得るのかなと。

※ dev環境では通常コードを書き換えたらコンテナは再生されますが some_file.php を更新してもファイルの変更が検知されるcacheが再生成されないため注意

応用編

これまで parameterの設定にのみ絞って紹介しましたが、ここからは動的にconfigrationする方法を紹介します。

ここまでちゃんと説明してこなかったのですが、実はリソースファイルの読み込みはcontainerのコンパイル前に行われます。 そのため、リソースファイルに書いた設定でframeworkの動作を変更することもできます。 またコンパイルされてキャッシュされるということは、毎回ファイルを読み込むこともないのでパフォーマンス面も気にする必要はありません。

では、引き続き実例を紹介します。

3. 特定環境下でDBのconnectionを変えたい

開発サーバー上とlocalhost上で接続先を変えたい場合など

<?php
// どこかから設定を取ってくる
$connection = [
    'host' => 'localhost',
    'dbname' => 'blog_local',
    'user' => 'root',
    'password' => null,
];
$container->loadFromExtension('doctrine', [
    'dbal' => [
        'connections' => [
                'default' => $connection,
            ],
        ],
    ],
]);

少し応用すれば、DB名にtest_ prefixを付けたり、動的にslaveサーバーのhostを追加することもできます。

4. 自動でassetバージョンを付与する

これが今回紹介する中で一番簡単で実用的かもしれません。

assetバージョンとは?

<link rel="stylesheet" href="/asset/css/style.css?201612070000">

こういうやつです。 cssファイルなどの静的コンテンツにクエリーストリングとして、バージョン番号を付けてやることで、cssを更新した際に古いcssがブラウザにcacheされ続けてしまう問題を回避することができます。

<?php
// app/config/assets.php
$container->loadFromExtension('framework', [
    'assets' => [
        'version' => date('YmdHi'),
    ],
]);

phpリソースファイル中でコンテナが使えるのはなぜ?

リソースファイルが読み込まれる箇所

Kernel.php https://github.com/symfony/symfony/blob/a4edafbd7d82ccea983f04ade3dc220e72f340e2/src/Symfony/Component/HttpKernel/Kernel.php#L659

PhpFIleLoader https://github.com/symfony/symfony/blob/a4edafbd7d82ccea983f04ade3dc220e72f340e2/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php#L39

<?php
    public function load($resource, $type = null)
    {
        // the container and loader variables are exposed to the included file below
        $container = $this->container;
        $loader = $this;

        $path = $this->locator->locate($resource);
        $this->setCurrentDir(dirname($path));
        $this->container->addResource(new FileResource($path));

        include $path; // ここでphpファイルを読み込む
    }

load()メソッド内に$containerというローカル変数を作っているので phpのリソース中から$containerが参照できるのでした。

まとめ

  • configファイルにphpを利用したら黒魔術的ではあるがわりと何でも設定できる。
  • ただし、やり過ぎると何が設定されているか分からなくなってしまうので注意

Symfonyでdebug環境を最適化しコードを追いやすくする

この記事はSymfony Advent Calender 2015 5日目の記事になります。

f:id:okapon_pon:20151205123050p:plain

昨日は@clonedさんの「Symfonyから手早くYAMLのFixtureを読み込めるAliceFixturesBundle」でした。 空いていた12/4を埋めてくださりありがとうございます。私も救世主となるべく投稿することにしました。

実はこの記事が去年のSymfony Advent Calender以来1年ぶりの投稿となってしまいました。 中々アウトプットできてないですね。orz...

さて本題


Symfonyでdebug環境を最適化しコードを追いやすくする

対象環境

Symfony2系(ただしSymfony3系についても少し触れます)

前置き

Symfonyで開発時のデバッグやコードリーディングを行いたい場合、デバッガー立ち上げてコードを追うと、キャッシュファイルが読み込まれてしまって、Symfonyコアのコードが読みづらいということがあると思います。 エラーが発生した場合にも同様で、どのクラスのどの行なのか分かりづらいことがあります。

具体的には、app/bootstrap.php.cacheclasses.php というファイルのことで、このファイルにはAppKernelやRequestクラス等を始めフレームワークの多くのコアクラスが1ファイルに記述されています。

1ファイルに多数のクラスを詰め込むことで何度もファイルをrequireせずに済みパフォーマンスを向上させているわけですが、開発時のデバッグ&コードリーディングを行いたい場合にはしばしば邪魔になることがあります。

この記事ではその問題をなんとか解決するための方法を書きたいと思います。

設定方法

symfonyのcacheとautoloadの仕組みが分かってしまえばそんなに難しい話ではないのですが、意外と日本語情報もないのでご紹介したいと思います。

紹介したいといいつつ、実は公式ドキュメントに書いてあります。(最近気が付きました)

How to Optimize your Development Environment for Debugging (2.8version)

Symfonyはドキュメントが充実していてどんどん増えているのでよくチェックしておかないとですね。

やることとしては

  1. app/bootstrap.php.cache を読み込まずに、普通にautoloadを利用する
  2. 生成したcacheファイル(classes.php)を読み込まないの2つです。

app_dev.phpを以下のように編集するだけです。

変更前

<?php
// ...
$loader = require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';

$kernel = new AppKernel('dev', true);
$kernel->loadClassCache();
$request = Request::createFromGlobals();

変更後

<?php
// ...
// $loader = require_once __DIR__.'/../app/bootstrap.php.cache';
$loader = require_once __DIR__.'/../app/autoload.php';
require_once __DIR__.'/../app/AppKernel.php';

$kernel = new AppKernel('dev', true);
// $kernel->loadClassCache();
$request = Request::createFromGlobals();

bootstrap.php.cache の中身ですが、先頭でautoload.phpを読み込んでいて、その後に続けてSymfonyのコアクラスが書かれています。 変更後のコードに書き換えることで、通常のautoloadによってSymfonyのコアクラスがきちんとロードされるようになります。 また、loadClassCache()コメントアウトすることでclasses.phpを読み込まなくなります。

以上で設定完了です。簡単ですね。

これでコードが読みやすくなりました。Symfonyのコードを読むのはとても参考になるので是非チャレンジしてください。

Symfony3の場合

実はSymfony3の場合も既にドキュメントが書いてありました。 こちらを参考に設定してみてくだい。

How to Optimize your Development Environment for Debugging (current)

補足すると、Symfony3では auloload.phpbootstrap.php.cache から分離されました。

【参考】 extract autoloading out from bootstrap by Tobion · Pull Request #854 · symfony/symfony-standard · GitHub f:id:okapon_pon:20151205123050p:plain そして bootstrap.php.cache/var/ディレクトリ直下に配置されるようになり、app_dev.phpではデフォルトで読み込まないようになりました。

app.phpではパフォーマンス向上のためbootstrap.php.cache利用しています。 https://github.com/symfony/symfony-standard/blob/3.0/web/app.php#L9

まとめ

  • ちょっと、書き換えるだけでdebugしやすいようになります。
  • Symfonyは(英語の)ドキュメントが充実しているのでよく見ておくといいです。

最期に

まだ「[Symfony Advent Calender 2015](http://qiita.com/advent-calendar/2015/symfony」の枠で空いているところがあります。 この記事のように、日本語化されていないドキュメントのちょっとした紹介や入門記事でも構いません。 是非この機会にSymfonyについて記事を書いてみてください。

明日は @hidenorigoto さんのあの本の話ですね。とても楽しみです。