最近仕事で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; } }