仕事

【Laravel&Ajax】CSVダウンロード機能実装

2023年1月12日


最近仕事でCSVダウンロード機能を実装したんだけど、調べてみたらCSV生成方法が何個かあった。

今回は下記の方法で実装。

  • ブラウザに直接データを返却する方法
  • 変数に出力するデータを代入し、それを返却する方法

CSVデータの入ったJSONをAjaxに返却するには、後者のやり方になると思う。
Ajaxのdone内でBlobオブジェクトでファイルを作成する方法を採用する。

ユッケです。

バージョン

  • PHP version:7.4.*
  • Laravel version:8.0.*

成果物




上の写真のようにCSVダウンロードボタンをクリックすると、CSVファイルがダウンロードされれば成功!

UTF-8でBOM付きでダウンロードする。BOMは (Byte Order Mark)の略で「UTF-8 で文字を書いたよ」とPCに知らせる。逆にないと一部のブラウザやソフトで誤った文字コードで表示されて文字化けするときがある。

CSVファイルダウンロードの作成方法は色々あるけど、今回は2つの方法で実装

ブラウザに直接データを返却する方法

ファイルの保存が不要な場合は、「php://output」を使用。

フロント側のJavaScript

まずはボタンクリックしたらAjaxがHTTPリクエストをサーバーに送信するフロント側の処理を実装していく。

 $('#csvDownloadBtn').click(function() {
    // ボタンを非活性にする
  $(this).prop('disabled', true);
    // 今回はnullをpost送信
    let requestData = null;
    console.log('----- Request -----');
    console.log(requestData);
    console.log('-------------------');

    $.ajax({
      url: 'csvdownload',
      method: 'post',
      responseType: 'blob', // ←省略可能
      data: requestData
    }).done(function(response) {

      let dt = new Date();
      let y = dt.getFullYear();
      let m = ("00" + (dt.getMonth()+1)).slice(-2);
      let d = ("00" + (dt.getDate())).slice(-2);
      const result = y  + m +  d;

      const filename = `${result}_filename.csv`;
      let bom = new Uint8Array([0xEF, 0xBB, 0xBF]);

      const blob = new Blob([bom, response]);
      const link = document.createElement('a');
      const url = URL.createObjectURL(blob);
      link.href = url
      link.download = filename;
      document.body.appendChild(link);
      // オブジェクトURLを開放
      URL.revokeObjectURL(url);
      // disable解除
     $('#csvDownloadBtn').prop('disabled', false);

   }).fail(function(error) {

      console.log(error)
   });

});

リファクタリング

JSファイルを分けてすればなおさら良い。

// click csv downlaod button
const downloadToCsv = async function(requestData) {
  // calling api request function csv download
  const res = await apiRequestCsvOutput(requestData);
  if (res.status == 200) {
    if (res.errorId === null) {
      try {
        if (res.data === null || res.data === undefined || res.data === '') {
          throw new Error('Failed to download');
        }
        let bom = new Uint8Array([0xef, 0xbb, 0xbf]);
        let blob = new Blob([bom, res.data], { type: "text/csv" });
        let link = document.createElement("a");
        link.href = URL.createObjectURL(blob);
        link.download = fileName;
        link.click();
      } catch (error) {
        // failed to download
        console.log(error);
      } 
    } else {
      // show error modal
    }
  } else {
    // show error modal
  }
}

// API request function for csv download
const apiRequestCsvOutput = async function (data) {
  // API key
  let apiKey = 'ABC123456';

  // common function
  let res = await commonRequestApi(data, apiKey);

  return res;
};

const commonRequestApi = async function(data, apiKey) {

  try {

    let token = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
    let param = {
      sessionId: sessionId,
      data: data,
    };
    
    let options = {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-CSRF-TOKEN": token,
      },
      body: JSON.stringify(param),
    };
    
    const resCsv = await fetch(`http//localhost/mypage/${apiKey}`, options);
    
    if (!resCsv.ok) throw new Error('Problem downloading csv file');
    
    const data = await resCsv.blob();
    
    // return data
    return {
      status: 200,
      errorId: null,
      errorMessage: null,
      data: data,
    };
  } catch (err) {

    let message = "";
    if (res.status == 400 || res.status == 500) {
      message = "system error";
    }
    // return the data
    return {
      status: res.status,
      errorId: null,
      errorMessage: message,
      data: null,
    };
  }
};

// clcik csv download button
$('button#CsvDownloadBtn').click(function() {
  // disable the button
  $(this).prop('disabled', true);
  // send null
  let requestData = null;
  downloadToCsv(requestData);
  // enable the button
  $(this).prop('disabled', false);


});

 

URLオブジェクトに変換してアンカータグからダウンロード。

LaravelのController

public function csvdownload(Request $request)
{
    try {
        // テーブルからデータ取得
        $resultsData = CsvDataItem::all(); // ← [CsvDataItem]はModel

        // データが存在しない場合、エラーメッセージを返却
        if (!$resultsData ->count()) {

            $response = response()->json([
                'errorId' => null,
                'errorMessage' => 'データが存在しません',
                'data' => null
            ],200);
            return $response;

        } else {
            // データが存在する場合、取得結果をCSV変換し返却
            $callback = function () use ($resultsData) {

                // 出力するデータ修正
                $csvArr = [];
                $info = [];
                foreach ($resultsData as $result) {

                    $info['A'] = $result->A; // ←の[A]はデータベースのテーブルのカラム名;

                    $info['B'] = $result->B; 

                    $info['C'] = $result->C;

                    $csvArr[] = $info;
                }

                $stream = fopen('php://output', 'w'); 

                // 文字コードを変換して、文字化け回避
                stream_filter_prepend($stream, 'convert.iconv.utf-8/cp932//TRANSLIT', STREAM_FILTER_READ);

                // CSVファイルにヘッダーを追加
                $csvHeader = [
                    'A',
                    'B',
                    'C',
                ];
                fputcsv($stream, $csvHeader);

                foreach ($csvArr as $row) {
                    // 各列に行を追加(カラムに値)
                    fputcsv($stream, $row);
                }

                fclose($stream);

            };

            $filename = "filename".date('Ymd').".csv";

            $header = [
                'Content-Type' => 'application/octet-stream',
            ];

            return response()->streamDownload($callback, $filename, $header);
        }
    } catch(\PDOException $pdoe) {

        $response = response()->json([
            'errorId' => null,
            'errorMessage' => null,
            'data' => null
        ],500);
        return $response;
    }
}

簡単に解説すると、最初にModelを使用してデータベースのデータを取得する。データが存在する場合、foreachでループさせて$infoに入れて、それを$csv_arr配列にぶち込む。

変数に出力テキストを追加していく方式

{
   errorId: null,
   errorMessage: null,
   data: "A,B,C\nあ,い,う\nえ,お,か\nき,く,け\n"
}

データを上記のように返したい場合、変数に出力テキストを追加していく方式で実装可能。
データ量が多いとダウンロード失敗するので好ましくないやり方。

フロント側のJavaScript

 $('#csvDownloadBtn').click(function() {
    // ボタンを非活性にする
  $(this).prop('disabled', true);
    // 今回はnullをpost送信
    let requesthData = null;
    console.log('----- Request -----');
    console.log(requestData);
    console.log('-------------------');

    $.ajax({
      url: 'csvdownload',
      method: 'post',
      responseType: 'blob', // ←省略可能
      data: requestData
    }).done(function(response) {

      let dt = new Date();
      let y = dt.getFullYear();
      let m = ("00" + (dt.getMonth()+1)).slice(-2);
      let d = ("00" + (dt.getDate())).slice(-2);
      const result = y  + m +  d;

      const filename = `${result}_filename.csv`;
      let bom = new Uint8Array([0xEF, 0xBB, 0xBF]);

      const blob = new Blob([bom, response.data]);
      const link = document.createElement('a');
      const url = URL.createObjectURL(blob);
      link.href = url
      link.download = filename;
      document.body.appendChild(link);
      link.click();
      // オブジェクトURLを開放
      URL.revokeObjectURL(url);
      // disable解除
     $('#csvDownloadBtn').prop('disabled', false);

   }).fail(function(error) {

      console.log(error)
   });

});

フロントは「ブラウザに直接データを返却する方法」と同じ。

LaravelのController

public function csvdownload(Request $request)
{
      try {
          // テーブルからデータ取得               
           $resultsData = CsvDataItem::all(); // ← [CsvDataItem]はModel

            // データが存在しない場合、エラーメッセージを返却
            if (!$resultsData->count()) {

                $response = response()->json([
                    'errorId' => null,
                    'errorMessage' => 'データが存在しません',
                    'data' => null
                ],200);
                return $response;

            } else {
                // データが存在する場合、取得結果をCSV変換し返却

                $csvArr = [];
                $info = [];
                // CSVファイルにヘッダーを追加
                $csvHeader = [
                    'A',
                    'B',
                    'C',
                ];
                $csvArr[] = $csvHeader;
                foreach ($resultsData as $result) 

              $info['A'] = $result->A; // ←の[A]はデータベースのテーブルのカラム名; 
            $info['B'] = $result->B; 
             $info['C'] = $result->C; 
                    $csv_arr[] = $info;

                    $csvArr[] = $info;

                }
                // 変数に出力テキストを追加していく方式
                $csv = '';
                foreach($csvArr as $row) {
                if( is_array($row) ) {
                    $csv .= join(',', $row);
                } else {
                    $csv .= $row;
                }
                $csv .= "\n";
                }
                $charset = 'sjis-win';
                header("Content-Type: text/javascript; charset={$charset}");

                echo json_encode([
                    'errorId' => null,
                    'errorMessage' => null,
                    'data' => $csv,
                ]);
                exit();

            }
        } catch(\PDOException $pdoe) {

            $response = response()->json([
                'errorId' => null,
                'errorMessage' => null,
                'data' => null
            ],500);
            return $response;
        }
    }

-仕事