CSVをアップロードして、データベースの情報を追記して表示し、DLできるようにする機能を作っていたのですが、追記する情報の中に、シングルクォート(')を含んでいる文字列(例 I'm Fine!)があるとDLできずにとてもとても詰まったので、ここにメモしておきます。
環境
PC: MacBook Air
Browser: Chrome
Version: Laravel Framework 5.8.16
参考文献
[PHP]preg_replaceで複数の文字列置換
【PHP】複数文字列の置換(str_replace)
【PHP】JSONがnullになるケース
Javascript|JSONでの、レコードの表現方法(配列、連想配列)→{}と[]の違い
【JavaScript】JSONのparseとstringifyメソッドの使い方
PHP: json_decode を使って Unicode エスケープシーケンスを UTF-8 の文字列に変換する
PHPでjson_encodeとjson_decodeを使ってみる
htmlspecialchars
array_walk_recursiveでクロージャを使う
JSON でのエスケープ処理 (JSONの値に""", "" を含める場合の処理)
PHP エスケープシーケンスのサンプル
JSONデータに改行コードを入れる方法
JSONについて調べてみた
How to deal with backslashes in json strings php
json_encode() escaping forward slashes
Posting JSON from hidden form field
how to use JSON.stringify and json_decode() properly
JSONのエスケープ
PHPとJavaScriptでHTMLエンティティを扱う時のおさらい
json_encode
json_encode can not escape single quotation marks?
How should I go about adding slashes to only single quotes and ignoring double quotes?
【PHP入門】正規表現で置換する方法
JSON.stringifyは、文字列化されるたびに二重引用符をエスケープします
How do I json_decode string with special chars (“ \ ” )
htmlspecialchars_decode
(以下、現象説明から解決まで)
現象説明
Laravelで作っている管理画面で、IDが載っているCSVを管理画面上でアップロードしたら、ajaxで、PHP側でそのIDをもとに紐づく情報をデータベースから取ってきて、CSVのデータにくっつけて、テーブルに表示し、CSVとしてDLもできるようにする機能を作ったのですが、
紐づく情報の中に、「'」(シングルクオート)が入っている場合に、JavaScriptのエラーが出てDLできない事象が起こりました。そして、Javascript側のエラーを解消すると、今度はPHP側でエラーが起こってしまう、という事態になりました。
まず、Javascript側でエラーになってしまっていた原因は、
そもそもの仕組みとして、DLボタンが押されたら、
scriptでformを生成してhiddenのvalue値にjson形式でデータを入れてsubmitさせる
という形だったのですが、
そのvalue値が、「'」(シングルクオート)があることにより、途中で途切れてしまう為、第一弾のエラーが起こってしまっておりました。
$(document).ready(function(){
//CSVダウンロード
function download_csv(){
// csvデータを作成
var csvData = '<?= json_encode($csv_data) ?>';
// ダウンロードを実行
$('<form target="_blank" action="【PHP側のURL】" method="POST">' +
'<?= csrf_field() ?>' +
'<input type="hidden" name="csv_data" value=\'' + JSON.stringify(csvData) + '\'>' +
'</form>').appendTo(document.body).submit();
}
$("#dl-btn").on('click', function(e) {
download_csv();
});
});
そこで、json_encodeを見直し、「'」(シングルクオート)をエスケープしたりすることで、javascriptのエラーを起こさずに、PHP側に遷移できるようになったのですが、
しかしながら、今度はエラー第二弾。
PHP側で、POSTで送られてきた上記CSVのデータをjson_decode()した後に、foreachを回していたのですが、
そのforeachの部分で、エラーが起きてしまいました。
デバッグしてみると、json_decode()した後のデータが入っているはずの$csv_datがNULLとなっており、
NULLなのにforeachを回そうとしてエラーになっておりました。
public function dlCsvConversion(Request $request)
{
assert(isset($_POST['csv_array']));
$csv_dat = json_decode(json_decode($_POST['csv_array'], true),true);
$csv = '';
foreach ($csv_dat as $value){
foreach ($value as $k => $val){
if ($k === count($value)-1) {
$csv.=$val;
} else {
$csv.=$val."\t";
}
}
$csv.="\r\n";
}
header('Content-Encoding: UTF-16LE');
header('Content-type: text/csv; charset=UTF-16LE');
header('Content-Disposition: attachment; filename="convertedCsv-'.date('Ymd').'.csv"');
header('Pragma: no-cache');
header('Expires: 0');
echo chr(255) . chr(254) . iconv("UTF-8", "UTF-16LE", $csv);
exit;
}
だけど、POST値はきちんと送られてきております。
なのに、json_decode()の結果がNULL。
検索してみると、json_decode()に失敗するとNULLとなってしまうことがわかりました。
そこで、色々と改善策を検索して試してみたのですが、
今回、json配列が[](鍵カッコ)で括られた、行列のみ多次元配列のデータだった為、key:valueのような形の{}で囲われたjson配列の記事はたくさんあったのですが、その違いのためかうまくいかない手法ばかりでした。
解決
ただ、試行錯誤しているうちに、
POST値がjson_encode()したはずなのに、なぜか「'」(シングルクオート)などがdecode済みの形で入ってきてしまっていたのを
javascript側を
json_encode(json_encode($csv_data, JSON_HEX_APOS))
上記のように最初に「'」(シングルクオート)だけ指定してjson_encodeし、さらに全体をjson_encodeし、JSON.stringify()は使わないようにすることで、
PHP側にencodeしたままの形で渡せるようになり、
さらに、今までは
""[[],[]]""
上記のように、javascript側で2回json_encode()していることで、「"」(ダブルクオーテーション)が入れ子になってしまっているのをjson_encode()を入れ子にして2回実施することで、解凍していたのですが、
それをpreg_replace()で最初と最後の「"」(ダブルクオーテーション)を取り除き、json_encode()を1回だけにしたところ、json_decode()がSyntax errorでNULLになってしまうのを無事解消することができました!!
$(document).ready(function(){
//CSVダウンロード
function download_csv(){
// csvデータを作成
var csvData = '<?= json_encode(json_encode($csv_array, JSON_HEX_APOS)) ?>';
// ダウンロードを実行
$('<form target="_blank" action="【PHP側のURL】" method="POST">' +
'<?= csrf_field() ?>' +
'<input type="hidden" name="csv_array" value=\'' + csvData + '\'>' +
'</form>').appendTo(document.body).submit();
}
$("#dl-btn").on('click', function(e) {
download_csv();
});
});
public function dlCsvConversion(Request $request)
{
assert(isset($_POST['csv_array']));
$search = array('/^"/','/"$/');
$replace = array('','');
$csv_data = json_decode(preg_replace($search, $replace, $_POST['csv_data']),true);
$csv = '';
foreach ($csv_data as $value){
foreach ($value as $k => $val){
if ($k === count($value)-1) {
$csv.=$val;
} else {
$csv.=$val."\t";
}
}
$csv.="\r\n";
}
header('Content-Encoding: UTF-16LE');
header('Content-type: text/csv; charset=UTF-16LE');
header('Content-Disposition: attachment; filename="paygentCsvConverted-'.date('Ymd').'.csv"');
header('Pragma: no-cache');
header('Expires: 0');
echo chr(255) . chr(254) . iconv("UTF-8", "UTF-16LE", $csv);
exit;
}
(振り返り)
そもそものコードもちょっとおかしかったのですが、どん詰まり状態から開放されてとても嬉しい気分です。
直したコード的にはあっけなかったのですが、エンコードのデバッグはとてもわかりづらくて辛かった。。
Unicode文字列とかまで試したりして、結局は使いませんでしたが、勉強にはなりました!
POST値がencode済だったのは多分JSON.stringify()していたせいですかね、、
直せたけど、曖昧なままな部分もある。。でもとりあえず、よしとしておきます!
以上!