matsudada技術ブログ

日々の雑念と備忘録

Laravel 5.7でCSVをダウンロードするときに内容が画面に表示されてしまう

Laravel 5.7でCSVをダウンロードするときに内容が画面に表示されてしまう

CSVファイルをダウンロードする機能の作成を依頼していて、
質問されたが原因が全然分からなかった。

環境

現象

一定までのサイズならCSVファイルがダウンロードされるが、
一定のサイズを超えると内容が画面に表示される。
とりあえずメモリリークしてね?とは思ったが、今回の問題には関係ないので触れないでおいた。

問題のコード

public function csvDownload(Request $request)
{
    $headers = [
                'Content-Type' => 'text/csv',
                'Content-Disposition' => 'attachment; filename="users.csv"'
               ];
    $stream = csvDownload($request); // 検索してstreamに書き込む

    return \Response::make(stream_get_contents($stream), 200, $headers);
}

原因

下記の参考ページを発見して知ったが、こんな仕様があるらしい。

最後に、Httpヘッダーの作成までに時間がたくさんかかってしまった場合、そもそもファイルのダウンロードに失敗してしまいます。
これは、ブラウザがしばらくレスポンスを待ってもHttpヘッダーが返ってこない場合、デフォルトのヘッダーを使ってブラウザにデータを吐き出してしまうという仕様によるものです。

対応

参考ページに乗っているコードを一部改変し使用した。
変更点

  • BOMの付け方が元のやり方では文字列として出力されるため変更
  • ネストが浅くなるのでchunkからcursorに変更
use \Symfony\Component\HttpFoundation\StreamedResponse;

public function csvDownload(Request $request)
{
    $headers = [
                'Content-Type' => 'text/csv',
                'Content-Disposition' => 'attachment; filename="users.csv"'
               ];
     
    return new StreamedResponse(
        function () {
            $stream = fopen('php://output', 'w');
            // ExcelでUTF-8と認識させるためにBOMを付ける(変更部分)
            fwrite($stream, pack('C*', 0xEF, 0xBB, 0xBF));
            
            // chunkではなくcursorを使用(変更部分)
            $cursor = \DB::table("users")->orderBy("id")->cursor();
            foreach ($cursor as $user) {
                fputcsv($stream, [$user->id, $user->name]);
            }
            fclose($stream);
        },
        200,
        $headers
    );
}