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となり、リクエストを注入しレスポンスを返すサービスになった

2011年11月8日火曜日

Zend Framerowkでデフォルトと異なるビュースクリプトでレンダリングする

何回使っても覚えられずにいつもgoogleにお世話になってしまいます。 forwardと違って表示しようとするスクリプトのactionは実行されません。
return $this->getHelper('viewRenderer')->setNoController()->setScriptAction("controller/action");

2011年10月11日火曜日

短縮リンク先取得であらためて、KISSの原則について考えた

Twitterの短縮がリンクがすべて t.co/... になってからもう数ヶ月たちますが、関連アプリケーションを開発している人たち以外にはほとんど意識する必要はないでしょう。

いっぽう、Twitterクライアントを開発している人たちは、t.co/...の短縮リンクを展開するにはどうしたらいいか、必ず直面する課題だと思います。bit.lyにあるようなAPIは提供されていないのです。

bit.lyをいままで使っていた私は、APIがないから展開できないと考えていたのです。しかし、よくよく考えてみると実際にt.coのリンクにアクセスして、リダイレクトをたどっていけば最終的には求めるURLにたどり着けるはずです。

開発の現場でスケジュールに追われていると、このような発想の転換はなかなかできません。後になって冷静に考えると、問題を解決しようとしてかえって問題を複雑にとらえてしまっていることが多々あります。

詰まってしまったら、まず以下を意識すると解決の糸口になるかもしれません。

  • 大きな問題は細かく分割する
  • 難しくかんがえない。自分だけだとなかなか気づかないことも多いので、第三者のアドバイスをあおぐのも手です

(おまけ)短縮リンク取得(Zend Framework版)

        $config = array(
            'adapter'   => 'Zend_Http_Client_Adapter_Curl',
        );
        $client = new Zend_Http_Client(短縮リンクのURL, $config);
        $client->setMethod(Zend_Http_Client::GET);
        $response = $client->request();
        if ($response->getStatus() == "200") {
            $url = $client->getUri(true);
        }

2011年9月18日日曜日

phpのエラーハンドリング

エラーを適切にキャッチし、記録し、対策することはどんな言語を使っていても重要です。 phpでこれらの処理を行うのによく使う関数やディレクティブを並べてみました。


関数

error_reporting
出力する PHP エラーの種類を設定する

ディレクティブ

error_reporting
error_reporting()関数同様の機能。ただし演算子の記述に制限あり
display_errors
エラーをHTML出力の一部として画面に出力するかどうかを定義する
log_errors
エラーメッセージを、サーバーのエラーログまたはerror_logに記録するかどうかを指定する
error_log
スクリプトエラーが記録されるファイル名を指定する

それぞれのおすすめ設定

error_reporting

E_NOTICEもログ出力しておくとバグに気づくことがあります。

display_errors

開発環境ではtrue、本番環境ではfalse(これは必須)

log_errors

開発環境、本番環境問わずtrue

error_log

省略するとapacheのエラーログに記録されます。アプリケーション個別のエラーログファイル名を決めて指定することを推奨。

まとめ

  • 本番環境、開発環境問わずerror_reportingにはE_NOTICEも記録する
  • 本番環境ではdisplay_errorsは必ずfalseを指定。開発環境ではお好みですが、trueを設定しておくと画面に表示されるので対応漏れを減らすことができます。
  • エラーログはアプリケーションごとにファイルを分ける。これにより、監視するときもアプリケーション単位で行うことができます。

注意

error_reportingに設定するE_ALLの値がphpのバージョンで変わります。 このせいでphpのバージョンアップをしたとたんに大量のエラーログに見舞われることがあります。これについてはまた別の機会に。

2011年9月2日金曜日

phpでメモリ使用量に気を使う意味

phpはそのスクリプトで使用するメモリの上限を memory_limit というphp.iniのディレクティブで設定できますが、このデフォルト値はphpのバージョンによって変わります。
PHP 5.2.0より後では"128M"、PHP 5.2.0 より前は "8M"、PHP 5.2.0 では "16M"
となっており、php 5.2.0より前ではかなり少ない値となっています。 このメモリ制限のしくみは、便利でもあり、不便に思うこともあります。

リクエストを処理してレスポンスを返すウェブアプリケーションではメモリを大量に使用することはまれです。memory_limitののおかげでバグを仕込んでしまったループでメモリが消費されてエラーで気づいて助かったことがあります。

いっぽう、バッチ処理のようなスクリプトでは、内容にもよりますがメモリ使用量に気をつけていないと、途中で「PHP Fatal error: Allowed memory size of **** bytes exhausted」が発生して、中断してしまい悲惨です。

これを避けるために、memory_get_usage()でメモリ使用量を調査するデバッグコードを仕込んでおくのは良い習慣といえるでしょう。そして、もしPHP Fatal Error:...が発生してしまったら、まずロジックに問題がないか調べ、それからmemory_limitの値の変更を検討します。

そして、明示的にmemory_limitを指定しておけばphpのバージョンアップによる影響も避けられます(ini_setで設定できるのでスクリプトごとに変えられます)

2011年8月29日月曜日

phpの出力制御

header()の前には出力はできない(はず)

phpのマニュアルのheader()関数の説明に
<html>
<?php
/* これはエラーとなります。この上に出力があることに注目してください。
* それはheader()のコールより前であるということになります */
header('Location: http://www.example.com/');
?>
とあります。たしかにおぼろげに
Cannot modify header information - headers already sent by ...
こんなワーニングを見た記憶があります。
このワーニングを再現しようと思い上記のコードをテストサーバに配置しアクセスしてみたのですが、ワーニングも出ずにきちんとリダイレクトされてしまいました。なぜ!? 納得がいくまで調べていたら数日が経過してしまいました…

調査

サーバからのレスポンスをヘッダを含めて確認したかったので、いろいろ手段はありますが今回は Burp Suite を使いました。
レスポンスはこのようになっていました。
HTTP/1.1 302 Found
Date: Mon, 29 Aug 2011 **:**:** GMT
Server: Apache/2.2.16 (Ubuntu)
X-Powered-By: PHP/5.3.3-1ubuntu9.5
Location: http://www.example.com/
Vary: Accept-Encoding
Content-Length: 7
Content-Type: text/html

<html>
<html>を先に出力しているのに、きちんとheader()関数が有効で、Location:ヘッダが出力されていました。これは明らかに、

出力バッファリング

ですよね。でも、出力制御関数は一切使っていないのに…となると、答えはもう、php.iniしかありません。
テストした環境(ubuntu server 10.10, php 5.3.3)のphp.iniを見てみると…
output_buffering = 4096
見つけました。この設定があるおかげで、header()の前に出力があってもheader()が有効でした。ためしに、
<html>
<?php
echo str_repeat(" ", 4096);
header('Location: http://www.example.com/');
?>
このように余計な文字列を出力し Content-Length: が4096以上になると、その時点でバッファに溜め込んだ出力がヘッダとともに出力されてしまうので、次の header("Location:"... でワーニングが発生し、リダイレクトもされません。

以上をふまえて注意

header()を使うときは
  • header()とコンテンツの出力の順序に気をつける。これはMVCの分離ができていれば容易にできるはずです。
もしくは
  • output_bufferingの設定を明示的に行う。php.ini以外に.htaccessやhttpd.confでも可能です。
header()がまともに動かなかったり動いたり、出力制御が結果的にややこしくしている原因かもしれません。しかし、phpの出力制御の特性を理解していればトラブルもすぐ解決できることでしょう。