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

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

2014年個人的Symfony振り返り、そして 2015年・・・

この記事はSymfony Advent Calender 2014 25日目最終日の記事になります。

昨日は qcmatsuokaさんの「Symfony2 カスタムバリデーションの作成」でした。

カスタムバリデータ作成は一応公式ドキュメントに書かれてはいるものの意外と良い記事がなくて苦戦しがちなところだと思います。 私も以前苦戦した記憶があります。

わりと詰まりがちなところですので、やってみました記事が出てくるのはいいですね。

emailのシステム制約の実装は実践的な内容ですし、Symfony入門者にとって嬉しい内容だと思いました。ご丁寧にスクショまでついています。

本題

さて本題です。タイトルから想像がつきますが、残念ながら技術的な内容ではありません。技術的内容を期待されていた方ごめんなさい。

本当は「レガシー 続・フラットなPHPからSymfonyへ」という誰得(?)な内容でブログを書こうと思っていたのですが、時間がなくて内容を変えました。

ごめんなさい。機会を作ってまた書こうと思います。

個人的2014Symfony振り返り

ここからは私個人の主観による2014年Symfonyの所感と、Symfonyコミュニティ発展のため小ネタも含めて書いていきたいと思います。 入門者向けの情報も書いてますので、最期まで読んでみてください。

その1 今年はSymfonyユーザーが増えてきているのを実感した

1-1

まず、今年のSymfony Advent Calender 2014はほとんどの人が2周目登録することもなく全枠埋まりました。

しかも皆さん期日通り記事をアップされていて、クオリティーもすごく高くてこういう日本語情報が欲しかった! という記事がたくさん集まりました。

良質な記事を書く人が増えたのは純粋に嬉しいですね。

Symfony Advent Calendar 2012 は本当に大変で、同じ人が何周もしてました(笑)

1-2

2つめは、Symfony2を採用している会社が増えてきて、勉強会に参加される中でも利用者の割合が増えてきました。

2013年ぐらいまでは勉強会を開催しても、あまり周りでSymfony2を使っているということを聞いたことがないですが、最近はちらほら聞くようになりました。

これまでSymfony勉強会は決まった人が前に出てはなすところがありましたが、今年は今まで発表したことない人の発表が多かったです。 よくよく考えたら、私も今年初めて発表した人の一人でした。

あの @t_wada さんがSymfonyを使っているというのは今年一番の衝撃だったかも知れません。

勉強会では様々な会社の事例・取り組みであったり、開発ノウハウ、Symfonyを使った開発スタイルなどリアルな情報を交換できたことはとてもよかったです。

Symfonyの使い方で悩んでいる、という方がもしいらっしゃいましたら勉強会へ参加されることをオススメします!

勉強会の開催情報を逃したくない方はこちらへ → 日本Symfonyユーザー会 | Doorkeeper

その2 Symfony入門者の増加を感じた

日本Symfonyユーザー会 メーリングリスト を見ていてもそうですが、最近Syomfony入門者の方が増えているのか、これまでほとんど動いていなかった日本Symfonyユーザー会のメーリングリストでしたが、最近はぼちぼちの頻度で質問が投稿されています。

メーリングリストや、#symfony_ja というハッシュタグをつけてtwitterに投稿すれば誰かしら回答してくれると思います。

入門者の方は是非活用してみてください。

ただし、ここで注意。

「今後はSymfonyに関する質問は 日本語版のstack overflow を使うこと」を推奨することになりましたのでそちらで質問されるのがよいと思います。

日本語版のstack overflow Symfony2タグ

その3Symfonyもくもく会が開催されるようになった

今年からと言っても、11月からですがSymfonyもくもく会というものが開催されるようになりました。

いつも勉強会に参加されている人から、Symfonyをほとんど触ったことのない人達まで参加しているゆるい会です。半分ぐらいはSymfony触ったことないという方でした。

もくもく会と称しつつ作業半分、議論半分ぐらいで、もくもくやってる時間の方が少ないかもしれません。

今のところ月1ぐらいのペースで開催していて、おそらく1月も開催すると思います。

こちらも参加者の募集はDoorkeeperでやっています。 日本Symfonyユーザー会 | Doorkeeper

2015年は?

2015年は 次期LTSであるSymfony2.7が5月に、そしてメジャーバージョンアップである Symfony3.0が11月にリリース予定となっています。

The Release Process (Contributing to Symfony)

来年は動きのある年となりそうで、今後のSymfonyの発展が楽しみですね。

来年の抱負

  • Symfony勉強会 #11も開催したいとろ。LTSや3.0も出るので、ワークショップ形式でもいいかも。
  • 次回も発表する
  • そろそろ日本語ドキュメントのFormの章の翻訳を終わらせたい(最近ユーザー会への質問も多い。。)
  • 来年もコミュニティへの貢献
  • Symfony温泉!(これ重要!!)

以上、Symfony Advent Calendar 2014 でした。

ここまでSymfony Advent Calendar 2014のバトンを繋いでこられた皆さま。本当にお疲れ様でした!!

今年も残すところあと僅かですね。それでは皆さま良いお年をお過ごしくださいませ!!

「BEAR.Sundayから学ぶテストプラクティス」という記事を書きました

タイトル通りなのですが、BEAR.Sunday Advent Calendar 2014 の21日目の記事として書きました。

BEAR.Sundayから学ぶテストプラクティス - Qiita

よろしければ読んでみてください。

これを書こうと思った経緯なども時間のあるときに書きたいなー

SymfonyのDIを利用してMail送信クラスをインテグレーションテストする:コード編

【追記】

新しく書いたこちらの記事を参考にしたほうが良いです。 Symfonyでメールのインテグレーションテストを行う - プログラミング - THIS IS IT !

追記終わり

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

昨日は ohsawa0515さんの「StofDoctrineExtensionsBundle について使ったり調べてみた」でした。

今年のSymfony Advent Calender 2014は盛り上がってますね。既に全枠埋まっています。 一昨年、枠が埋まらず何周も記事を書いてた方がたくさんいらっしゃった事を考えると感動ものです(笑)

追記

この記事の内容より新しく書いた以下の記事を参考にすることをおすすめします。

okapon-pon.hatenablog.com

追記終わり


さて本題

SymfonyのDIを利用してMail送信クラスをインテグレーションテストする

前置き

書いていたらあまりに超大作になってしまったので前編を作りました。興味がある方は読んでみてください。

SymfonyDIを利用してMail送信クラスをインテグレーションテストする:前置き編 - プログラミング - THIS IS IT !

本稿で取り上げる内容

サンプルコード

サンプルコードはgithubにアップしています。

GitHub - okapon/mail-test-sample: Symfony2でメール送信クラスをテストするコードサンプル

以下の環境で動作確認をしています。

Symfony 2.6.1
php 5.5.18
phpunit 3.7.38

github からチェックアウトして下記コマンドを実行することでテストを実行できます。 といっても実行されるテストは1つだけですが。

$ composer install
$ ./vendor/phpunit/phpunit/phpunit.php -c app

コードの解説

プロダクトコード

まずプロダクトコードですが、Symfonyの公式ドキュメント「サービスコンテナ」の章で、NewsletterManager サービスの作り方が紹介されているので、これに習ってMailService作っている方が多いのではないでしょうか。

サービスコンテナ | Symfony2日本語ドキュメント

これと似たコードですが、今回は以下のようなプロダクトコードを考えてみます。ユーザー登録を行った際に送信される、メールの持ち主確認を行うアクティベーションメールを送るものを想定してます。

<?php

namespace AppBundle\Service;

use AppBundle\Entity\User;

class MailService
{
    protected $mailer;
    protected $twig;

    /**
     * Constructor
     *
     * @param \Swift_Mailer $mailer
     * @param \Twig_Environment $twig
     */
    public function __construct(\Swift_Mailer $mailer, \Twig_Environment $twig)
    {
        $this->mailer = $mailer;
        $this->twig = $twig;
    }

    public function sendRegistrationMail(User $user)
    {
        // 認証URLを作成する、ここではサンプルコードなので固定で記述しています
        $activationUrl = 'http://example.com/activation?code=abcdefg';

        $body = $this->render('AppBundle:Mail:register.txt.twig', [
            'user' => $user,
            'activationUrl' => $activationUrl,
        ]);
        $subject = '[xxx サービス] ご登録ありがとうございます';
        $fromEmail = 'info@example.com';
        $userEmail = $user->getEmail();

        $message = \Swift_Message::newInstance()
            ->setFrom($fromEmail)
            ->setTo($userEmail)
            ->setSubject($subject)
            ->setBody($body)
        ;

        return $this->sendMessage($message);
    }

    /**
     * sendMessage
     *
     * @param \Swift_Message $message
     * @return void
     */
    protected function sendMessage(\Swift_Message $message)
    {
        $this->mailer->send($message);
    }

    /**
     * render
     *
     * @param string $template
     * @param array $vars
     * @return string
     */
    protected function render($template, array $vars = [])
    {
        return $this->twig->loadTemplate($template)->render($vars);
    }
}

このコードに対するテストコードを書いていこうと思いますが、ここで問題となるのがtestモードでテストを実行した場合にはSwiftMailerは、デフォルトではメール送信処理を行いません。

【参考】 開発中におけるメール送信の扱い方#メール送信を無効にする | Symfony2日本語ドキュメント

かといってテストコードを実行するたびに外部にメールを送信してしまうのはよい作法とはいえまません。 実際にメール送信は行いたくはない、けれどもメールの確認はしたい。

この問題を解決するためのヒントはSymfonyのドキュメントにあります。

英語のドキュメントでまだ日本語に翻訳されていませんが、ファンクショナルテスト中でメール送信を確認する方法が紹介されています。

How to Test that an Email is Sent in a functional Test(英語)

How to Test that an Email is Sent in a Functional Test (Symfony Docs)

しかしメールのテストをするために、$client = static::createClient();して、コントローラーのテストまでしないといけないというのは少し面倒です。

そこで今回の記事の本題である、SymfonyDIを使って少し工夫することで、上記のようなメールのテストを行う方法を紹介したいと思います。

テストコード

1 パラメーターを使ってMailServiceクラスを指定するようにします。

※サービスコンテナのコンフィギュレーションはymlで記述するものとします。

service.yml

parameters:
    app.mail_service.class: AppBundle\Service\MailService

services:
    app.mail_service:
        class: %app.mail_service.class%
        arguments:
            - @mailer
            - @twig

2 テストコードだけで読み込まれるservice_test.ymlを用意して、上記のMailServiceを置き換える

service_test.yml

parameters:
    app.mail_service.class: AppBundle\Tests\Mock\Service\TestMailService

3 service_test.ymlを読み込む

<?php

namespace AppBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

/**
 * This is the class that loads and manages your bundle configuration
 *
 * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
 */
class AppExtension extends Extension
{
    /**
     * {@inheritdoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
        $loader->load('services.yml');

        // test環境だけファイルを読み込む
        $env = $container->getParameter("kernel.environment");
        if ($env === 'test') {
            $loader->load('services_test.yml');
        }
    }
}

4 置き換えたクラスでメールの確認ができるように拡張する

<?php

namespace AppBundle\Tests\Mock\Service;

use AppBundle\Service\MailService;
/**
 * TestMailService.
 *
 * 送信メールの内容をテストするため、Swift_Messageを内部に保持し
 * 後から取りだしすことをできるようにしたテスト用のクラス
 *
 */
class TestMailService extends MailService
{
    /**
     * @var Swift_Message[]
     */
    protected $messages = [];

    /**
     * {@inheritDoc}
     */
    public function sendMessage(\Swift_Message $message)
    {
        $this->messages[] = $message;  // ここがポイント

        return parent::sendMessage($message);
    }

    /**
     * @return Swift_Message[]
     */
    public function getMessages()
    {
        return $this->messages;
    }
}

ポイントはsendMessage()をオーバーライドしてクラスの内部変数にSwift_messageインスタンスを保持していることです。 また、そのインスタンスを取得できるようgetMessagesで取得してます。

テストコードは以下のようになりました。

<?php
namespace AppBundle\Service;

use Phake;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use AppBundle\Entity\User;
class MailServiceIntegrationTest extends WebTestCase
{
    protected $mailService;
    public function setUp()
    {
        $kernel = self::createKernel();
        $kernel->boot();
        $container = $kernel->getContainer();
        $this->mailService = $container->get('app.mail_service');
    }

    /**
     * @test
     */
    public function sendRegistrationMail()
    {
        $fromEmail = 'info@example.com';
        $userEmail = 'user.mail@example.com';
        $userName = '田中 太郎';
        $activationUrl = 'http://example.com/activation?code=abcdefg';
        $user = Phake::mock(User::class);
        Phake::when($user)->getUsername()->thenReturn($userName);
        Phake::when($user)->getEmail()->thenReturn($userEmail);

        // test (メール送信)
        $this->mailService->sendRegistrationMail($user);

        // 送信されたメッセージのインスタンスを取得
        $messages = $this->mailService->getMessages();
        $this->assertCount(1, $messages);

        // 1通目だけテスト
        $message = $messages[0];
        $this->assertInstanceOf('Swift_Message', $message);

        $expected = [
            'from' => $fromEmail,
            'to' => $userEmail,
            'subject' => '[xxx サービス] ご登録ありがとうございます',
            'body' => [
                $userName,
                $activationUrl,
            ]
        ];

        $this->verifyMail($message, $expected);
    }

    protected function verifyMail(\Swift_Message $message, array $expected)
    {
        // From & To アドレスを検証
        $this->assertEquals($expected['from'], key($message->getFrom()));
        $this->assertEquals($expected['to'], key($message->getTo()));
        // Subject
        $this->assertEquals($expected['subject'], $message->getSubject());
        // Body
        foreach ($expected['body'] as $body) {
            $this->assertContains($body, $message->getBody());
        }
    }
}

$this->message = $container->get('app.mail_service')で取得されるインスタンスは、AppBundle\Tests\Mock\Service\TestMailServiceに置き換わっています。

$messages = $this->mailService->getMessages(); で送信されたSwift_Messageインスタンスを取り出し想定通りのメッセージとなっているか確認します。

ここでは、Fromアドレス、Toアドレス、件名の完全一致、本文に「名前」「アクティベーションURL」が含まれているかを検証しています。

場合によっては本文を全文一致で検証してもいいですが、必要な情報が出力されていればいいとの判断です。文面がかわることもありますし。

本文を全て確認したい場合には、必殺のvar_dump($message->getBody())も使えますし、最終的にブラウザテストも必要なのでまた別に確認したりします。

※ 当然のようにPhakeを利用してますが、PhakeはPHPのモッキングライブラリです。 Welcome to Phake - PHP Mocking Framework’s documentation! — Phake - PHP Mocking Framework 1.0.3 documentation

まとめ

  • DIとクラスのパラメーター化を利用することで、テスト対象のクラスを容易に置き換えることができました。
  • このようにテストしにくい対象クラスを継承して、内部状態を検査するような手法はテストコードを書く技術としてしばしば利用されています。(実際Symfony2のテストコードでみかけます)
  • メールのテストで面倒なブラウザ操作を何度も行う必要がなくなります(※最低限の動作確認は必要です)

SymfonyDIを利用してMail送信クラスをインテグレーションテストする:前置き編

コードの解説を行っている続編はこちら

http://okapon-pon.hatenablog.com/entry/symfony2-mail-test-2

前置き

皆さんテスト書いてますか?

TDDでバリバリ開発しています!という人は少ないのではないかと思います。

TDDをなかなか実践できない問題の1つにテストを書くメリットを感じ辛いというのがあるのではないかと思います。

確かに効果を感じ辛い部分もありますが、逆にすごく効果的に機能する場合もあります。

今回はメリットを感じやすいメールのインテグレーションテストを紹介したいと思います。

Mailのインテグレーションテストを行うメリット

普通にブラウザテストを行う場合、メールを送信するためにユーザー登録フォームに入力したり、申し込みフォームを入力したりと、面倒な操作を行わないといけないケースが大半だと思います。

ユーザー登録など1度しか行うことのできない操作の場合だと登録処理が完了した後、テストのためにDBのデータを削除して再度実行!なんて非常に面倒です。

操作するのが面倒なのでデバッグ用のコントローラーでも用意して、そこからメール送信のメソッドを呼び出すこともできますが、毎回そういうのを書くのも面倒です。

そこで是非やっていただきたいのが、登場するのがメールのインテグレーションテストです!

この記事の続編に書いてある方法を実践すると以下のメリットがあります。

  • ブラウザを開かなくても、メール送信ロジックの確認ができる(とはいえ開発時の話ですので、最終的にはブラウザテストも行うべきでしょう)
  • フォームの入力など面倒な操作が必要ないので、素早く何度でも確認できる

テストについて

タイトル通り扱うのはインテグレーションテストであり、ユニットテストではありません

このテストの目的は、ある引数を渡したら想定されたメールが送信されるか確認するためのテストを行いたいのです。 (ただし、続編記事で書いている通り実際にはメールを送らず代替手段で確認を行います)

なぜユニットテストではないのか?

  • どんなテンプレートを呼ぼうとしているか
  • どんな引数を渡しているか

は今回確認したいことではありません。

テンプレートの置き場所やファイル名が間違っていた。

変数は渡してたけど、Twig中での表記が間違っていて動作しなかった

では意味がないですよね。

テストで確認したいことは、

  • 実際にテンプレートが読み込まれ、レンダリングが行われること
  • 想定した宛先に想定した件名・本文でメールが送信されていること

だからです。

※ 決してユニットテストが不要と言っているわけではありません。 テストの目的が異なるだけです。

End-toEnd Testすれば?と思う方もいるかもしれませんが、ここではコントローラーのテストを行いたい訳ではないのでやりません。

コードの解説を行っている続編はこちら

http://okapon-pon.hatenablog.com/entry/symfony2-mail-test-2

「Blackfire」というPHPプロファイラーが出たので使ってみた(最速レビュー)

Blackfireとは?

SensioLabsからリリースされた、PHPのプロファイリングツールです。 プロファイリング結果をブラウザ上で簡単に見ることができるようになります。

Blackfireのデモ動画

以前は「SensioLabsProfiler」と呼ばれており、サービスを出すとだけ告知されていたのですがようやく利用できるようになりました。

現在はbeta版として提供されていて、DebianRed HatベースのLinuxMac OSが対応しています。

利用するためには、Blackfileの「Join the beta」からSensioLabsConnectに登録する必要があります。

アーキテクチャ(ざっくり)

  • CompanionというChorome拡張を使ってプロファイリングを実行
  • ProbeというPHP extensionでアプリケーションのプロファイルを計測
  • blackfire-agentでプロファイル結果を集めてBlackfireサーバー側にデータを送信
  • プロファイラーで集めた結果をブラウザで確認する

セットアップ

インストール方法は、マニュアルのGetting Started に丁寧に書かれています。 こちらを見ることをおすすめします。

私はMacにインストールしたので、以下Macにインストールする場合の話をします。

Chrome拡張のインストール

利用可能なブラウザはChromeだけのようです。 Googole Chromeをインストールしてない人はまずインストールしてください。

Chrome 拡張機能の「Blackfire Companion」をインストールします。

blackfire-agentのインストール

手順はMacだと以下

$ brew tap blackfireio/homebrew-blackfire
$ brew install blackfire-agent

DebianRed Hat 向けのリポジトリもありますので各環境に合わせてインストールしてください。

続いてServer IdとServer Tokenを登録します。

以下のコマンドを実行します。

$ blackfire-agent -register

Server IdとServer Tokenは、Getting started のページに記載されているので、それを入力します。

f:id:okapon_pon:20141103203024p:plain

現在、1アカウントにつき1つのAgentしか登録できないようです。 ただし、Blackfire Accountページに「Register an Agent」ボタンがあるので将来的には複数agentを登録できるようになるのでしょう。

f:id:okapon_pon:20141103234655p:plain

blackfire-agent が自動起動するように登録します。

$ ln -sfv /usr/local/opt/blackfire-agent/*.plist ~/Library/LaunchAgents

今回は手動で起動

$ launchctl load -w ~/Library/LaunchAgents/homebrew.mxcl.blackfire-agent.plist

Probeのインストール

Probeのインストールは環境によってはちょっと面倒ですし、場合によっては利用さえできない可能性があります。

  • 現時点で、LInux環境ではPHP5.6に未対応
  • PHPのビルド時に、--enable-debug つけて有効化していると利用不可
  • XDebugXHProf を利用していると干渉するかも(確認していない)

「homebrew-phpを利用していれば特に問題ないよ」とは書かれてました。

ちなみに、私は--enable-debugとしてPHPをビルドしていたので、PHPをインストールし直しました。

ドキュメントにも書かれていますが、自前でビルドしたPHPMac標準のPHPを使う場合には、以下のコマンドを実行します。

$ brew install blackfire-php55 --without-homebrew-php

homebrew-phpでインストールしている場合には「--without-homebrew-php」を消すと付けずに実行します。

一応モジュールのインストールを確認

$ php -m | grep blackfire
blackfire

使い方

試しにSymfonyフレームワークのプロファイルを取ってみます。

プロファイリングしたいページにアクセスし「Blackfire Companion」の拡張ボタンをクリック。続いてprofileボタンをクリックします。

f:id:okapon_pon:20141103231921p:plain

プロファイリングの進行状況はページの上部に表示され、完了するとプロファイリング結果のページヘのリンクが表示されます。

f:id:okapon_pon:20141103233249p:plain

プロファイリング結果(profile-slots)のページへアクセスすると、コールグラフなどが表示されます。

f:id:okapon_pon:20141103233426p:plain

左側のメソッドと、右側のコールグラフが連動していて、右側のグラフをクリックするとその部分の詳細を見ていくことができます。 xhprofのコールグラフはあくまで画像として出力されるだけなので、リンクになっていて該当メソッドの場所にジャンプできるのは便利ですね。

f:id:okapon_pon:20141103233526p:plain

実行時間、ディスクI/O、使用メモリーの順で並び替えたり、コストの高いメソッドを抽出したりとボトルネックとなっている部分を探しやすいようになっています。

f:id:okapon_pon:20141104000951p:plain

既存のプロファイラーだと実行時間ぐらいしか見ることができなかったので、これはありがたいですね。

もう少し使いこんでみようと思います。