エンジニア初心者がPHP/Laravelを学ぶ 仕事

【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)
});
});
$('#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) }); });
 $('#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);
});
// 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); });
// 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;
}
}
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; } }
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"
}
{ errorId: null, errorMessage: null, data: "A,B,C\nあ,い,う\nえ,お,か\nき,く,け\n" }
{
   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)
});
});
$('#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) }); });
 $('#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;
}
}
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; } }
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;
        }
    }

-エンジニア初心者がPHP/Laravelを学ぶ, 仕事

S