時折データベースの中身をファイル(csvなど)として出力したいという要件があります。そういった時は往々にして画面に表示しきれないくらいのデータ量のデータを出力することになります。この処理を素朴に書いた場合、PHPやマシンの設定と環境によっては容易にメモリが足りなくなります。メモリの問題を解決したとしても、コードや相手のブラウザによってはクライアント側のタイムアウトの問題に直面することがあります(あんまりにもあんまりな処理だとPHP側の実行時間タイムアウトまで起きますが本記事では割愛)。一時的にファイルに書き出し、書き出したファイルを送信する方法もありますがI/Oが恐ろしいことになりやすいです。これらの問題を避けるためにはクライアントに向かってデータを書き出し続けるストリームによるダウンロードを使うと良いです。
PHPにはクライアントへデータを出力するための通信口をファイル的に扱える仕組みがあります。ファイル名をphp://outputとするとファイル的に出力を扱えます。
PHP: php:// – Manual
大体次の様にファイル書き込み同様に扱います。
file_put_contents('php://output', data)
Laravelだとおおよそデータベースを基にするので次の様にクエリを準備、chunk機能を使ってその中でストリームに書き込む方法が考えられます。chunkは結果の分割機能でデータベースから得られるクエリ結果のサイズが多くてもメモリを溢れさせないためによく使います。
データベース:クエリビルダ 6.x Laravel#結果の分割
public function downloadCsv($query, $header = null, $chunkSize = 10000)
{
return Response::streamDownload(
function () use ($query, $header, $chunkSize) {
// バイナリ書き込みモードで出力をオープン
$stream = fopen('php://output', 'wb');
// BOM付きUTF-8にしてExcelで開いても文字化けしないようにする
file_put_contents('php://output', "\xEF\xBB\xBF");
// あらかじめCSVの一行目(ヘッダ行)に書き込みたい配列を$this->header()に用意
// 書き込みたいヘッダがあるならストリームに出力
is_array($this->header()) ? fputcsv($stream, $this->header()) : null;
// あらかじめCSVにしたいSQLを$this->queryに用意
// クエリをchunkを通して投げて、投げた結果を都度presenterに通してストリームに乗せる
$this->query->chunk($chunkSize, function (Collection $chunk) use ($stream) {
$chunk->each(function ($item) use ($stream) {
// クエリ結果をCSVとして出力するためにいい感じに整形して配列にするメソッド$this->presenter($item)をあらかじめ用意
// PHPには配列をCSV形式として適切に書き込むための関数fputcsvがあります
fputcsv($stream, $this->presenter($item));
});
});
// 出力をクローズ
fclose($stream);
},
$filename,
[
'Content-Type' => 'application/octet-stream',
]
);
}