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

【PHP】Ajaxを使ってJSON形式で配列データを取得し、Webページ上に表示させる最低限の方法

PHPと任意のデータベースでシステムを構築する際、非同期通信を行うためにAjaxを使う機会は非常に多くなっています。そして、今日ではPHPファイル上でデータベースから受け取った値をJSON形式にして返すのがセオリー化してきています。しかし、多くのサイトはそこまででとどまっているのが多く、配列を返す場合にどうやってWebページ上に表示させるかが詳しく載っていなかったためPG駆け出しの頃にかなり苦労しましたので、備忘録を兼ねてまとめてみました。

※一昔前のように、複数の戻り値に対し、間に;や@など、出力データ上に支障のないデリミタを挟んで一本の文字列化し、その戻り値をsplitメソッドで分割し配列化させる、というやり方もあるにはありますけど、流石に方法が古い上、処理が非常に遅くなるので省略します。

§1:jQuery編

まずは基本のおさらい

プルダウンメニューで選択した地区によって、集合場所と集合時刻を表示させる、簡潔なプログラムとなります。これの仕組みを見てみます。

HTML
<select id="sel">
     <option> --場所を選ぶ-- </option>
     <option value="kyoto">京都</option>
     <option value="osaka">大阪</option>
     <option value="kobe">神戸</option>
</select>
<div id="mes">
   集合場所は<span id="pos"><!-- ここに集合場所が入る --></span>、
   集合時刻は<span id="time"><!-- ここに集合時刻が入る --></span>となります。
</div>

JavaScript(jQuery)

javascript(jQuery)ファイルの記述は下のようになっています。取得データについてはPHPファイルの部分でも説明しますが、連想配列のプロパティがそのままjsonオブジェクトのプロパティとなっているので、そのまま引き出すだけで簡単に取得できます。あとはその取得した値を、html上のIDの値とJQuery上のセレクタの値を合わせてあげるだけでオーケーです。

JQuery
let optval;
$(function(){
    $('#sel').on("change",function(){
        optval = $(this).val(); //選択したメニューの値
        $.post({
            url: 'ajax_getData.php',
            data:{
                'opt': optval
            },
            dataType: 'json', //必須。json形式で返すように設定
        }).done(function(data){
           //連想配列のプロパティがそのままjsonオブジェクトのプロパティとなっている
           $("#pos").text(data.position); //取得した集合場所の値を、html上のID値がposの箇所に代入。
           $("#time").text(data.ap_time); //取得した集合時刻の値を、html上のID値がtimeの箇所に代入。
        }).fail(function(XMLHttpRequest, textStatus, errorThrown){
            alert(errorThrown);
        })
    })
})

jsファイルにおける補足と注意点

1. $.post()

Ajaxは$.ajax()とするページが多いですが、$.post()とする方が表記を簡略化できます(get形式なら$.get()で。ただし、jQuery1.18以上でないと、転送URLが文字化けして正しく送れないので、その場合は普通に$.ajaxと記述してタイプ指定してください)。また、successとerrorはjQuery1.18以上非推奨の古い記述法なので使わない方が望ましいです(入れ子構造となった場合、記述と解読が困難になります)。

補足:$.ajax$.postまたは$.getは厳密には異なり、前者だと非同期通信を実行してくれるので、遅延処理が可能なのに対し、後者だと遅延処理を行うことができません(普通に値を返すだけなら問題ない部分ではあるのですが)。

2. dataType: 'json' ※必須

$.post()メソッドの引数には

dataType:'json'

と記述しないと、スクリプトが文字列(string型)と勝手に認識してしまい、jsonオブジェクトを受け取ることができないので忘れないでください。

3. fail(function(XMLHttpRequest, textStatus, errorThrown){ … })

fail()メソッドには無名関数を記述し、その引数に対して形式的に(jqXHRオブジェクト, エラーの型を返す文字列, 例外発生オブジェクト)の3つを受け取るため、上記のように記述することが多いですが、変数名は特に指定はないようで、極端な話、(a,b,c)とするだけでもエラーを取得できます(要は引数を代入するだけなので)。…が、慣例的な記述をそのまま活用した方がいいでしょう(何がなんだか訳がわからなくなります)。

PHPプログラム

PHPプログラムはそこまで難しく考える必要はなく、一般的なPHPプログラムと同様に、postで外部から値を取得、変数を検索条件にかけ、一致するインデックスを割り出し、取得した値をechoで返しているだけです。この際、返す変数を配列(オブジェクト)化することで、javascriptで引き出しやすくしています。

ajax_getData.php
<?php
    header("Content-Type: application/json; charset=UTF-8"); //ヘッダー情報の明記。必須。
    $ary_sel_obj = []; //配列宣言
    $opt = filter_input(INPUT_POST,"opt"); //変数の出力。jQueryで指定したキー値optを用いる。
    //リスト情報(今回は配列にしているが、オブジェクトでも同様のJSON形式で受け取ることができる)
    $ary_lists = [
                    "kyoto" => [
                                   "position" => "四条河原町駅",
                                   "ap_time" => "8:30",
                               ],
                    "osaka" => [
                                    "position" => "梅田駅",
                                    "ap_time" => "9:00",
                               ],
                    "kobe" => [
                                    "position" => "三宮駅",
                                    "ap_time" => "9:30",
                              ],
                 ];
     if( isset($ary_lists)) $ary_sel_obj = $ary_lists[$opt]; //連想配列のプロパティから値を取得
    echo json_encode($ary_sel_obj); //jsonオブジェクト化。必須。配列でない場合は、敢えてjson化する必要はない
    exit; //処理の終了

補足と注意点

1. header("Content-type: application/json; charset=UTF-8") ※必須

phpプログラムではjson形式で値を返すために、ヘッダ情報は忘れずに記述すること。この記述を行わないとPHPファイルがデフォルトの形式(text/html) と認識してしまい、jsonデータを返すことができません。

※ヘッダ情報の補足説明

Content-Type: application/jsonというのはjsonにおけるMIMEタイプで、RFC4627(現在はRFC8259)のルールに則っています。また、charset=UTF-8と記述して、文字エンコーディングをutf-8として指定するのも、jsonはUTF-8環境下でのみ正しく動作することを、公式サイトが明記しているため。それからヘッダ情報は必ず変数出力の前に記述すること。(これはjsonファイルを扱うためというより、PHPプログラム記述の常識で、公式マニュアルにもheader()関数において「通常の HTML タグまたは PHP からの出力にかかわらず、すべての実際の 出力の前にコールする必要がある」と記載されています)。よって、場所としては変数出力前ならどこでもよく、プログラムの途中に記述しているページもありますが、PHPプログラム記述の慣習として、先頭に記述するのが望ましいとは思います。

2. json_encode()  ※必須

json形式で返したい配列及びオブジェクト変数は必ずPHPのjson_encode()関数を使って返してください。オブジェクトをそのままechoで返そうとすると、jsonに転換されていないのでレスポンスエラーとなります。そして、このjson_encode()関数においてもUTF-8形式でしか使用できないことが、PHPの公式マニュアルに明記されています。なお、値を返すのはecho の代わりにprintを使っても構いません(そもそもechoが、printのエイリアス関数)。

なお、配列ないしはオブジェクト以外の変数を返すのならば、敢えてjson化しなくても値を返すことはできますが、その際はヘッダもコメントアウトしてください。もし、ヘッダを記述した場合は、変数が配列でない場合でも、json_encode()で返してください。

3. デバッグに注意

PHPプログラム上でnoticeやwarning等の警告文(notice、warningといったメッセージ)がブラウザ上に表示されていないか(またはエラーログから確認する手もあります)確認してください。エラーなどがそのままで放置されていて、正しく値を取得できていない可能性があります。また、テスト出力用のechoやvar_dump()などをそのまま放置しておくと正しくデータを取得できない(というより、最初のechoやvar_dump()の値を戻り値として返してしまうので)ので、それも見直します。

4. exit;

変数を返した後はPHPプログラムの動作は不要になるので、exitで動作を終了しておくといいようです(要検証ですが、Ajaxが入れ子となったときのメモリ使用量と処理速度が大きく変わります)。

戻り値が二次元配列の場合

表示ボタンによって、リスト情報の一覧を一度に表示させるプログラムとなります。二次元配列になっている場合の注意点として、連想配列で返されたjsonオブジェクトはインデックスを持っていないため、$.eachイベント関数でループさせ、クラスセレクタのインデックス番号を明示させてデータを出力していく必要があります(処理の高速性を考慮するとforの方がいいかも)。

HTML
<button type="button" id="btn_display">表示</button>
<article class="lists">
    <div class="mes">京都の集合場所は<span class="pos"></span>、集合時刻は<span class="time"></span>です。</div>
    <div class="mes">大阪の集合場所は<span class="pos"></span>、集合時刻は<span class="time"></span>です。</div>
    <div class="mes">神戸の集合場所は<span class="pos"></span>、集合時刻は<span class="time"></span>です。</div>
</article>
JQuery
$(function(){
    $('.lists').css("display","none"); //リストを隠蔽
    $('#btn_display').on("click",function(){
        $.post({
            url: 'ajax_getLists.php',
        }).done(function(datas){
           $('.lists').css("display","block"); //リストを表示
           //二次元配列になっているのでループさせる
           var i = 0; //インデックス用
          $.each(datas,function(key,item){
              //クラスセレクタのインデックスを明示してあげないと、同じ場所を何度も上書きしてしまうことになる。
              $(".pos").eq(i).text(item.position); 
              $(".time").eq(i).text(item.ap_time);
              i++; //インデックス用のインクリメント
          })
        }).fail(function(XMLHttpRequest, textStatus, error){
            alert(error);
        })
    })
})

ajax_getLists.php
<?php
    header("Content-Type: application/json; charset=utf-8"); //ヘッダー情報の設定
    $ary_sel_obj = array(); //配列宣言
    $opt = filter_input(INPUT_POST,"opt"); //変数の出力
    //リスト情報
    $ary_lists = array(
                    "kyoto" => array(
                                   "position" => "四条河原町駅",
                                   "ap_time" => "8:30" 
                                 ),
                    "osaka" => array(
                                    "position" => "梅田駅",
                                    "ap_time" => "9:00"
                                 ),
                    "kobe" => array(
                                    "position" => "三宮駅",
                                    "ap_time" => "9:30"
                  )
    );
    echo json_encode($ary_lists); //jsonオブジェクト化。
    exit;

応用編(データベースとの連携)

これらを踏まえ、実際の現場で必要となるのはデータベースや外部ファイルなどと連携させる場合です。そこで郵便番号を入力すると、住所が自動的に表示される仕組みを実装してみました(実際は、郵便番号は必ずしもユニークではないので、複数表示できるようにするのが望ましいですが、あくまで今回のテスト用サンプルなので…)

データベースのテーブル情報は以下の通り(今回は使用DBMSは問わない)

sql
create table master_zip (
                       zip_code char(7) primary key,
                       pref_name char(5),
                       city_name char(10),
                       town_name char(20),
                       area_name char(20)
 );
insert into master_zip values
                       ("1690075","東京都","新宿区","","高田馬場"),
                       ("2130001","神奈川県","川崎市","高津区","溝の口"),
                       ("2470056","神奈川県","鎌倉市","","大船"),
                       ("2500408","神奈川県","足柄下郡","箱根町","強羅")
;
HTML
<label>郵便番号</label>
<input type="text" id="code">
<div id="mes">住所:<span id="place"></span></div>

JQuery
$(function(){
    let area;
    $('#code').on("mouseleave input",function(){
        code = $(this).val();
        //postで転送する
        $.post({
            url: 'ajax_getData.php',
            data:{
            'code': code
            },
            dataType: 'json',
        }).done(function(data){
            console.log(data);
            //連想配列のプロパティがそのままオブジェクト化されているので、それを活用する
            $.each(data,function(key,item){
                area = isValue(item.pref_name) + isValue(item.city_name) + isValue(item.town_name) + isValue(item.area_name);
            })
           $("#place").text(area);
        }).fail(function(XMLHttpRequest, textStatus, error){
            alert(error);
        })
        })

        isValue = function(val){
            if(val == undefined){
                val = "";
            }
            return val;
        }
})
ajax_getData.php
<?php
    header("Content-Type: application/json; charset=utf-8"); //ヘッダー情報の設定。必須。
    $row = array(); //配列宣言
    $data = array(); //配列宣言
    $code = func_escape(filter_input(INPUT_POST,"code")); //変数の出力
    //SQLの準備
    $sql = "select 
                zip_code,
                pref_name,
                city_name,
                town_name,
                area_name 
            from 
                master_zip
            where 
                zip_code = ?";
    //データベースの接続
    $dbh = db_connect(); //接続用の任意関数(今回はPDOを用い、使用DBMSの詳細は省略する)
    $sth = $dbh -> prepare($sql);
    $sth -> bindValue(1,$code,PDO::PARAM_STR);
    $sth -> execute();
    //ループさせて、データを取得する。
    $data = $sth -> fetchAll(PDO::FETCH_ASSOC); 
    echo json_encode($data); //jsonオブジェクト化。必須。
    exit;

    //エスケープ処理用の関数
    function func_escape($word){
        return htmlspecialchars($word,ENT_QUOTES);
    }

補足:doneとfailではなくて、thenを使う

thenを使うと、doneの処理とfailの処理を振り分けることができます。当時からあるにはありましたが、まだ一般的に使われていませんでした。普及したのは遅延処理のdeffered()promise()が一般的に使用されてからです。

js
let optval;
$(function(){
    $('#sel').on("change",function(){
        optval = $(this).val(); //選択したメニューの値
        $.post({
            url: 'ajax_getData.php',
            data:{
                'opt': optval
            },
            dataType: 'json', //必須。json形式で返すように設定
        }).then(
           function(data){
               //連想配列のプロパティがそのままjsonオブジェクトのプロパティとなっている
               $("#pos").text(data.position); //取得した集合場所の値を、html上のID値がposの箇所に代入。
               $("#time").text(data.ap_time); //取得した集合時刻の値を、html上のID値がtimeの箇所に代入。
           }
           ,function(XMLHttpRequest, textStatus, errorThrown){
               alert(errorThrown);
           }
        )
    })
})

§2:JavaScript編

fetchAPIを使用

fetchAPIを利用すれば、JavaScriptだけでけっこう高度なことができるようになっており、特に高速性が要求されるWEBアプリの世界ではセオリー化しているようです(ですが、色々と記述が煩雑なのでPC閲覧メインのWebシステム上でならjQueryでいいとは思います)。

ちなみに、このfetchAPIを利用した場合も、jsonで配列データを返す場合についての記述があまりなかったので、要点をまとめておきます。

また、jQuery利用者が極力飲み込みやすいように、かなりjQueryに似せた形で書いてます。

html
<body>
<select id="sel">
     <option> --場所を選ぶ-- </option>
     <option value="kyoto">京都</option>
     <option value="osaka">大阪</option>
     <option value="kobe">神戸</option>
</select>
<div id="mes">
   集合場所は<span id="pos"><!-- ここに集合場所が入る --></span>、
   集合時刻は<span id="time"><!-- ここに集合時刻が入る --></span>となります。
</div>
<script>
const url = "ajax_getData.php";
let selid = document.getElementById('sel');

selid.addEventListener('change',function(){
    let optval = selid.options[selid.selectedIndex].value; //プルダウンの値取得
    //パラメータを渡す
    let param = new URLSearchParams(); //URLSearchParamsインターフェースについては、後述で補足
    param.append("opt",optval);
    //Ajax処理
    fetch(url,{
        method: "post",
        body: param, //jQueryのdataプロパティのように、PHPにデータを引き渡す処理
    }).then(response => {
        if(response.ok){
            let promise = response.json(); //jsonを格納
            //Promiseから値を呼び出す(※Promiseについては後述で補足)
            promise.then(data =>{
                document.getElementById('pos').innerHTML = data.position;
                document.getElementById('time').innerHTML = data.ap_time;
            })
        }else{
            alert("リクエストに失敗しました");
        }
    });
})
</script>
</body>

注意点

1.new URLSearchParams();

JavaScriptのfetchにおけるbodyプロパティとjQueryの$.ajaxメソッドのdataプロパティとの一番の違いは引数に複数のパラメータを渡すことができないということです。そのため、PHPへのデータの引き渡しに関しては、オブジェクトに随時パラメータを追加する、という方法を採ることになります。

URLSearchParamsはURLにパラメータを渡すことができるインターフェースです。そして、それを利用してプロトタイプを生成し、それに対しappendメソッドで、PHPに渡したいデータを記述していくことになります。また、detaプロパティに転送する値は、パラメータを追加したプロトタイプの変数値になります。

※インターフェースとプロトタイプの関係は、クラスとインスタンスの関係と似たようなものだと覚えておけばいいでしょう(厳密には違います)。

参考記事
URLSearchParamsによる簡単URL操作

2.promiseについて

Promiseとは、簡単に言えば「預かり所」です。つまり、非同期通信を行った結果を成功、失敗にかかわらず、データも含めたコールバックをPromiseというオブジェクトに格納してくれます。なので、console.logコマンドで確認すると、そのデータを確認することができます。

ですが、注意しなければいけないのは他のオブジェクトと違って、実体のオブジェクトとして持っているわけではないので、普通にfor( obj of objects)構文を実行してループしてみても、何もデータを取得できません。まずは、Promiseされたオブジェクトに対しjson()メソッドでjsonオブジェクトを取得してから、thenメソッドを実行してください。そうすれば、非同期通信が成功した場合、各プロパティから値を取得することができます。

thenメソッドで取得したのが二次元配列の場合には前述のfor(obj of objects)構文が役立ちます。

参考記事
Promiseについて0から勉強してみた

BRSF
職業、PG・SE・DBエンジニア。オープン環境のwebプログラムをメインにシステム構築担当。使用言語はPHP(cakePHP、Laravel含)jQuery、JavaScript、ExcelVBA、Perl、Ruby、Python。現在Vue、React、Angular強化中。
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
ユーザーは見つかりませんでした