[Laravel] Laravelで行うCSVダウンロード(Laravel8対応)
実案件で、データをファイルとしてダウンロードする要件が時々あります。
サーバーに無駄な負荷をかけない実装を紹介します。
CSVダウンロードについては下記がよくまとまっています。
ここを参考に実装しました。
Laravel 6.x でCSV出力を実装する(レスポンスストリームにそのまま返す版) - hrendoh's tech memo
実装例
コードのアウトライン
LaravelにはstreamDownload()というメソッドがあります。
ストリームダウンロード
特定の操作の文字列レスポンスを、操作の内容をディスクに書き込まずにダウンロード可能なレスポンスへ変換したい場合もあるでしょう。このシナリオでは、streamDownloadメソッドを使用します。このメソッドは、コールバック、ファイル名、およびオプションのヘッダ配列を引数に取ります。
HTTPレスポンス 8.x Laravel
streamDownload()を使うことでphpストリームを扱った操作を行えます。
コールバック関数の中でcursol()メソッドを使います。
そうすることでストリームに1行ずつデータを流して出力することができるようになり、結果メモリの節約になります。
streamDownloadはコールバック関数、ファイル名、レスポンスヘッダーを引数に指定します。
コールバック関数の中身に実際のCSV出力処理を書いていきます。
PHPはC言語の影響を大きく受けたプログラム言語であり、ファイルにアクセスする処理もC言語と同じような仕組みを採用しています。そのためPHPでファイルにアクセスする際に利用する抽象的なインターフェースを使用しており、それがストリームです。
ファイルストリームって何?PHPのfopenについて機能と使い方を解説 | ウェブカツ公式BLOG
// コントローラーの1メソッドとして実装
public function download()
{
// コールバック関数に1行ずつ書き込んでいく処理を記述
$callback = function () use ($引数) {
// 出力バッファをopen
$stream = fopen('php://output', 'w');
// 文字コードをShift-JISに変換
stream_filter_prepend($stream, 'convert.iconv.utf-8/cp932//TRANSLIT');
// ヘッダー行
fputcsv($stream, [
'ID',
]);
// データ
$companies = Company::orderBy('id', 'desc');
// 2行目以降の出力
// cursor()メソッドで1レコードずつストリームに流す処理を実現できる。
foreach ($companies->cursor() as $company) {
fputcsv($stream, [
$company->id,
]);
}
fclose($stream);
};
// 保存するファイル名
$filename = sprintf('company-%s.txt', date('Ymd'));
// ファイルダウンロードさせるために、ヘッダー出力を調整
$header = [
'Content-Type' => 'application/octet-stream',
];
return response()->streamDownload($callback, $filename, $header);
}
好ましくない実装例
Laravelはデータの取得が簡単なのでついつい全件取得したくなりますが、そうしてしまうとメモリが足りなくなります。
許容量以上のサイズをメモリ確保しようとすることでエラーとなるのです。
下記にそのケースが発生しうるコードを記載しました。
CSVダウンロードでは往々にして画面に表示しきれないデータ量の出力を要求されます。
件数が多くなった場合に備えたコードを書くことが重要です。
// コントローラーの1メソッドとして実装
public function download()
{
$rows = [];
// ヘッダー行
array_push(
$rows,
implode(',', [
'ID',
])
);
// データ
$companies = Company::orderBy('id', 'desc')->get();
foreach ($companies as $company) {
array_push(
$rows,
implode(',', [
$company->id,
])
);
}
return implode('\n', $rows);
}
Discussion