読者です 読者をやめる 読者になる 読者になる

k-holyのPHPとか諸々メモ

Webで働くk-holyがPHP(スクリプト言語)とか諸々のことをメモしていきます。ソースコードはだいたいWindowsで動かしてます。

SilexでVolcanus_CsvとStreamedResponseを使ってCSV出力・Content-Dispositionヘッダの設定方法いろいろ

先日公開したCSV入出力用ライブラリ Volcanus_Csv ですが、実際に使わないと改善点も見えてこないので、あれこれやってみます。

今回は Silex で Volcanus_Csv のレスポンス出力を利用せず、通常のコントローラと同様に Response オブジェクトを利用してみました。

元々レスポンス出力機能を付けたのは、Symfony HttpFoundation 単体でファイルのダウンロード用レスポンスを生成する方法が分からなかったことが理由の一つなんですが、これについてはタイトルの StreamedResponse クラスを使うことで解決できました。
Response クラスを継承した StreamedResponse クラスは第1引数にレスポンスの内容そのものではなく、内容を出力するコールバックを受け付けるので、データベースやファイルの内容を読み込みつつ出力することが可能になります。

こんな感じで、Writer::flush() を実行する無名関数を渡します。

Opera の開発ツール Dragonfly でレスポンスを確認した結果 (Writer::buildResponseHeaders())

Content-Lengthヘッダの値は、受信したファイルのバイト数と同じ値が送出されていることを確認しました。
(X-XSS-Protection や X-Content-Type-Options: nosniff はソースでは付与されていないので、高セキュリティを謳っている Gehirn RS2 のWebサーバ側で付与されているものと思います)

StreamedResponse の第3引数ではレスポンスヘッダを指定できますので、サンプルではレスポンスヘッダを配列で返す Writer::buildResponseHeaders() を利用しています。
Content-Type は "application/octet-stream" が、Content-Disposition は "attachement; filename="responseFilenameで指定したファイル名"" が、Content-Length は SplFileObject に書き込まれたバイト数がそのまま設定されます。

よく問題となる Content-Dispositionヘッダ における日本語ファイル名については、Volcanus_Csvでは現状、SJIS-winに変換してそのまま出力してます。なのでOperaでは文字化けしてます…。
(業務上のお客さんはほぼ日本のIEユーザーなんで…IE7も多いし…)

Content-Dispositionヘッダのブラウザ対応は、こちらの記事 BK通信 - ブラウザのバッドノウハウ コンテンツ編 がちょっと古いですが分かりやすいです。
ブラウザに限らない内容では SuikaWiki の Content-Disposition が詳しいです。

不都合な場合は、以下のように自分で設定することも可能です。

Opera の開発ツール Dragonfly でレスポンスを確認した結果 (UTF-8で%エンコーディング)

Symfony\Component\HttpFoundation\ResponseHeaderBag に makeDisposition() メソッドがありますので、それを併用することもできます。

ResponseHeaderBag::makeDisposition() ではマルチバイトのファイル名は RFC 2231 (MIME Parameter Value and Encoded Word Extensions: Character Sets, Languages, and Continuations) に則ってUTF-8としてパーセントエンコーディングされます。
第3引数の $filenameFallback の使い方がよく分からないのですが、コードを読んだ限りでは、ファイル名に非ASCII文字を含む場合はこの引数にASCIIのみの文字列を指定すると良いようです。
上記サンプルでは、RFC 2047 形式でBASE64エンコードしたファイル名を指定しています。

Opera の開発ツール Dragonfly でレスポンスを確認した結果 (ResponseHeaderBag::makeDisposition()で RFC 2231 + RFC 2047)

以上、SilexでStreamedResponseを使ってみた的な内容になりましたが、より多くのブラウザに対応できるよう、日本語ファイル名の扱いは更に改善する予定です。
IE7にも対応しつつというのが無理そうなので、設定で切替える方式にするしかないかも…)

[追記]
Content-Dispositionヘッダですが、恥ずかしいことに「attachment」を「attachement」とtypoしてたのを今朝直しました。
https://github.com/k-holy/Volcanus_Csv/commit/3d39cb388d7253bbf72ed1702cd75a67881ba74c

よもや実用されている方はいないと思いますが、公開後すぐダウンロードしてみたという方はご注意ください…。