Help us understand the problem. What is going on with this article?

なぜか、うんざりするほど詰まった話(解決付)。 PHP → javascript → PHP の多次元配列jsonデータの受け渡し

CSVをアップロードして、データベースの情報を追記して表示し、DLできるようにする機能を作っていたのですが、追記する情報の中に、シングルクォート(')を含んでいる文字列(例 I'm Fine!)があるとDLできずにとてもとても詰まったので、ここにメモしておきます。

スクリーンショット 2020-05-21 20.31.26.png

環境

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


(:baby_tone2:以下、現象説明から解決まで:baby_tone2:)

現象説明

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になってしまうのを無事解消することができました!!

修正後のコード(javascript側)
$(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();
    });
});
修正後のコード(PHP側)
    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;
    }

:alien:振り返り:alien:

そもそものコードもちょっとおかしかったのですが、どん詰まり状態から開放されてとても嬉しい気分です。
直したコード的にはあっけなかったのですが、エンコードのデバッグはとてもわかりづらくて辛かった。。
Unicode文字列とかまで試したりして、結局は使いませんでしたが、勉強にはなりました!
POST値がencode済だったのは多分JSON.stringify()していたせいですかね、、
直せたけど、曖昧なままな部分もある。。でもとりあえず、よしとしておきます!

以上!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした