
最近仕事で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);
console.log('----- Request -----');
console.log(requestData);
console.log('-------------------');
responseType: 'blob', // ←省略可能
}).done(function(response) {
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.download = filename;
document.body.appendChild(link);
URL.revokeObjectURL(url);
$('#csvDownloadBtn').prop('disabled', false);
}).fail(function(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.errorId === null) {
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;
// API request function for csv download
const apiRequestCsvOutput = async function (data) {
let apiKey = 'ABC123456';
let res = await commonRequestApi(data, apiKey);
const commonRequestApi = async function(data, apiKey) {
let token = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
"Content-Type": "application/json",
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();
if (res.status == 400 || res.status == 500) {
message = "system error";
// clcik csv download button
$('button#CsvDownloadBtn').click(function() {
$(this).prop('disabled', true);
downloadToCsv(requestData);
$(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)
$resultsData = CsvDataItem::all(); // ← [CsvDataItem]はModel
// データが存在しない場合、エラーメッセージを返却
if (!$resultsData ->count()) {
$response = response()->json([
'errorMessage' => 'データが存在しません',
// データが存在する場合、取得結果をCSV変換し返却
$callback = function () use ($resultsData) {
foreach ($resultsData as $result) {
$info['A'] = $result->A; // ←の[A]はデータベースのテーブルのカラム名;
$stream = fopen('php://output', 'w');
stream_filter_prepend($stream, 'convert.iconv.utf-8/cp932//TRANSLIT', STREAM_FILTER_READ);
fputcsv($stream, $csvHeader);
foreach ($csvArr as $row) {
$filename = "filename".date('Ymd').".csv";
'Content-Type' => 'application/octet-stream',
return response()->streamDownload($callback, $filename, $header);
} catch(\PDOException $pdoe) {
$response = response()->json([
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配列にぶち込む。
変数に出力テキストを追加していく方式
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);
console.log('----- Request -----');
console.log(requestData);
console.log('-------------------');
responseType: 'blob', // ←省略可能
}).done(function(response) {
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.download = filename;
document.body.appendChild(link);
URL.revokeObjectURL(url);
$('#csvDownloadBtn').prop('disabled', false);
}).fail(function(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)
$resultsData = CsvDataItem::all(); // ← [CsvDataItem]はModel
// データが存在しない場合、エラーメッセージを返却
if (!$resultsData->count()) {
$response = response()->json([
'errorMessage' => 'データが存在しません',
// データが存在する場合、取得結果をCSV変換し返却
foreach ($resultsData as $result)
$info['A'] = $result->A; // ←の[A]はデータベースのテーブルのカラム名;
foreach($csvArr as $row) {
header("Content-Type: text/javascript; charset={$charset}");
} catch(\PDOException $pdoe) {
$response = response()->json([
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;
}
}