2013年6月6日木曜日

SwiftMailerでsubjectが文字化け

Symfony2でアプリケーションからメールを送信する際、標準のSwift Mailerを使うのが一般的でしょう。 PHPメンターズさんの記事「Practical Symfony #15: Swift Mailerによる日本語メールの作成」がとても参考になります。

私もこの記事のコードを参考にしてsubject, from, 本文に日本語を使ったメールが問題なく送信できることを確認していたのですが、この送信ロジックを複数のコントローラ内に散乱させてしまっていたので、サービスにして共通化しようとリファクタリングを始めました。

mimeが効かない

services:
    my_mailer:
        class:     MyBundle\Service\Mailer
        arguments:
            - @mailer
            - @templating
            - @translator
            - @logger
とサービスを定義して、Switf_Mailerをコンストラクタ内で注入して、sendメソッドでは$this->mailer->send()で送信。コントローラ内のロジックをサービスに集約する、ごくごく普通のリファクタリングをしたつもりでした。

ところが、このリファクタリングを入れてからsubjectのmimeエンコーディングがされなくなり、いわゆる「文字化け」が発生するようになりました。

原因はわかっているものの

mimeが効いていないので、おそらくは
Swift::init(function () {
    Swift_DependencyContainer::getInstance()
        ->register('mime.qpheaderencoder')
        ->asAliasOf('mime.base64headerencoder');

    Swift_Preferences::getInstance()->setCharset('iso-2022-jp');
});
の設定がうまく反映されていないんだろうという見当はすぐつきました。ただ、なぜサービス内に移しただけで反映しないのかが釈然としません。

シングルトン

さきほどの設定部分のコードを眺めていてわかるのは、
  • Swift::init()はstaticなメソッドである。
  • Swift_DependencyContainerとSwift_Preferencesはシングルトンである。
ここまで来て、ようやく理解できました。私はメールの送信のメソッド内に上記設定を書いていました(PHPメンターズさんの記事ではアプリケーションのバンドルに配置するのを推奨しているにもかかわらず)。Swift::init()はSwift_Mailerのオブジェジェクトが生成される前に実行できていないといけなのですが、サービスで定義してコンストラクタでSwift_Mailerを注入するようになってからSwift::init()を呼んだ時点ではすでにSwift_Mailerのオブジェクトは生成されているため、mimeの設定が反映されない状態になっていました。

解決

mimeの設定をSwiht::init()で行うことを公式ページでも推奨しているのですが、別にsend()毎に実行してもよいのではないか?ということで、
Swift_DependencyContainer::getInstance()
    ->register('mime.qpheaderencoder')
    ->asAliasOf('mime.base64headerencoder');

Swift_Preferences::getInstance()->setCharset('iso-2022-jp');
(snip)
とSwift::init()内から外に出して解決しました。mimeの設定コストがどのくらいなのか計測していませんが、バッチで大量に配信するものでなく、ウェブアプリケーションからなんらかのクライアントのリクエストからキックされてメールを送る用途では、気にしないでいいのではないでしょうか。 このやり方であれば、メール毎にmime設定のオンオフができます(する機会があるかは疑問ですが)

SwiftMailerネタはまだあります

メール本文のテキストの折り返しが意図しないところで行われる挙動にもだいぶ悩まされました。いずれ記事にします。

2013年6月4日火曜日

Symfony2.1から2.2へのアップデート

Symfony2.3.0のリリースおめでとうございます

2.3系はSymfony2はじめてのLTS(Long Term Support Release)で、今後3年間のメンテナンス期間が設定されています。 LTSは魅力的なのですが、βのころからコミットを追いかけていたわけではない私は、まだ公開サービスを2.3に移行するには早いと判断して、 ひとまず2.1のプロジェクトを2.2にアップデートしてみようとやってみました。

基本的な作業は

  1. composerでcreate-project
  2. composer.jsonにプロジェクト独自のパッケージを追加して、composer updateを通す
  3. 既存プロジェクトのvendor参照先をできあがったvendorに変更する
  4. app/config/config*.ymlの差分を確認、反映
  5. app/AppKernel.phpの差分を確認、反映

ここまでやって、php app/consoleを実行すればデバッグ環境なのでapp/cache以下がごそっと作成されます。 Symfonyをお使いの方であれば、php app/consoleでヘルプが表示されればひと安心!という気持ち、わかっていただけるかと思います。

すんなり移行できると思いきや

ところが、エラーハンドリングのカスタマイズをしていたところでエラー発生。 src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.phpを継承してクラスを作成して、それをconfig.ymlの
twig:
    exception_controller: Foo\Bundle\Controller\ExceptionController::showAction
    ...
で指定していたのですが、showActionのプロトタイプが変わっているようでした。

githubの差分を見てみたところ、ContainerAwareを継承しないController、つまりController as a Serviceに変更されていました。

新版のshowActionにプロトタイプをあわせて、いままでContainerAwareを継承していたため使えた$this->get('xxxx')をすべてコンストラクタで注入するようにして動作するようになりました。ここいらの設定は普通のサービスと変わりません。

ライブラリやフレームワークをアップデートしてアタリがある箇所ってたいてい、既存のインターフェースをimplementsしてたり、クラスをextendsしていたりすることが多いですね。

まとめ

  • Symfony2.2以降、標準のExceptionControllerはController as a serviceとなり、リクエストを注入しレスポンスを返すサービスになった