💾

[Laravel] Laravelで行うCSVダウンロード(Laravel8対応)

2022/01/17に公開

実案件で、データをファイルとしてダウンロードする要件が時々あります。
サーバーに無駄な負荷をかけない実装を紹介します。

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