PHPと任意のデータベースでシステムを構築する際、非同期通信を行うためにAjaxを使う機会は非常に多くなっています。そして、昨今ではPHPファイル上でデータベースから受け取った値をJSON形式にして返すのがセオリー化してきています。しかし、多くのサイトはそこまででとどまっているのが多く、配列を返す場合にどうやってWebページ上に表示させるかが詳しく載っていなかったためPG駆け出しの頃にかなり苦労しましたので、備忘録を兼ねてまとめてみました。
- §1jQuery
- $2fetchAPI
- $3axios
この順番で紹介していきますが、サーバーサイド側の処理など、基本はjQueryの部分で説明を入れています。
※一昔前のように、複数の戻り値に対し、間に;や@など、出力データ上に支障のないデリミタを挟んで一本の文字列化し、その戻り値をsplitメソッドで分割し配列化させる、というやり方もあるにはありますけど、流石に方法が古い上、処理が非常に遅くなるので省略します(とはいえ、WEBスクレイピングはこの技術を応用していますが…)。
§1:jQuery編
まずは基本のおさらい
プルダウンメニューで選択した地区によって、集合場所と集合時刻を表示させる、簡潔なプログラムとなります。これの仕組みを見てみます。
<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上のセレクタの値を合わせてあげるだけでオーケーです。
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で引き出しやすくしています。
<?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タイプで、RFCのルールに則っています。また、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
の方がいいかも)。
<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>
$(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);
})
})
})
<?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は問わない)
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","神奈川県","足柄下郡","箱根町","強羅")
;
<label>郵便番号</label>
<input type="text" id="code">
<div id="mes">住所:<span id="place"></span></div>
$(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;
}
})
<?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()
が一般的に使用されてからです。
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:fetchAPI編
fetchAPI
を利用すれば、JavaScriptだけでけっこう高度なことができるようになっており、特に高速性が要求されるWEBアプリの世界ではセオリー化しているようです(ですが、色々と記述が煩雑なのでPC閲覧メインのWebシステム上でならjQueryでもいいとは思います)。また、後で述べるaxiosのようにライブラリ呼出も不要です。
ちなみに、このfetchAPIを利用した場合も、jsonで配列データを返す場合についての記述があまりなかったので、要点をまとめておきます。
また、jQuery利用者が極力飲み込みやすいように、かなりjQueryに似せた形で書いてます。
<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から勉強してみた
jsonを利用して、一度に複数のデータを投入する方法
送信データをjsonにしてしまい、ヘッダ情報もすでにパラメータに埋め込む、昨今で最も主流の方法です。
param = {
method: 'POST',
headers:{ "Content-Type":"application/json"},
body: JSON.stringify({hoge:'hoge',fuga:'fuga'})
}
fetch(url,param).then((res)=> return res.json())
.then((res)=>{
//resに値が返ってくるので、それを用いる
})
§3:axios編
今日、一番手軽なのはこのaxios(アクシオス)というライブラリを用いたもので、ReactやVue.jsなどでAjax制御する際には欠かせません。ですが、このaxiosはJavaScript単品に使用することもできますし、記述と取得した値の制御がjQueryぐらい単純です(つまり、jQueryとFetchAPIのいいとこどり)。
ただ、注意点としてはpostとgetでパラメータの記述が全く異なるので、二通りで制御が必要な場合はちょっと面倒かもしれません。
また、axiosは外部APIとしても使用できます。
POSTの場合
POSTの場合、パラメータはFetchAPIと同じようにプロトタイプを作ってから、オブジェクトを代入していきます。しかし、レスポンスはjQUERYのようにパラメータをそのまま受け取ることができるので、後の処理が非常に楽です。
<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 src="https://unpkg.com/axios/dist/axios.min.js"><!-- Axiosを呼び出す--></script>
<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)
axios.post(url,param)
.then(function(res){
data = res.data;
document.getElementById('pos').innerHTML = data.position;
document.getElementById('time').innerHTML = data.ap_time;
})
//エラー制御
.catch(function(error){
console.log(error)
});
})
</script>
</body>
GETの場合
これをgetで制御しようとした場合、以下の記述となります(こっちがスタンダードな制御方法です)。POST制御の際とは異なり、今度はAxios側に用意されたパラメータにプロパティと値を代入して送信します。
<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 src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
const url = "ajax_getData.php";
let selid = document.getElementById('sel');
selid.addEventListener('change',function(){
let optval = selid.options[selid.selectedIndex].value; //プルダウンの値取得
axios.get(url,{ params:{opt: optval}})
.then(function(res){
data = res.data;
document.getElementById('pos').innerHTML = data.position;
document.getElementById('time').innerHTML = data.ap_time;
})
//エラー制御
.catch(function(error){
console.log(error)
});
})
</script>
</body>
当然、PHP側のfilter_input関数の引数もGETに変わります。わかっているとは思いますが念のため…
//前略
$opt = filter_input(INPUT_GET,"opt"); //取得タイプをGETに変更
//後略
参考にしたページ
付録(昨今の流れを受けて)
1: jsonでパラメータを送る
今までの記述はあくまでヘッダ情報をPHP上に記述していましたが、昨今ではローカル上にヘッダ情報を埋め込んでしまい、jsonとしてパラメータを送り、サーバーサイドでは受け取ったjsonを操作する、そんな手法が一般化しています。その場合の記述は以下のようになります。
例はfetchAPIを用いた場合です。
const param = {
method: "POST",
headers:{
"Content-Type":"application/json; charset=utf-8"
},
body: JSON.stringify({hoge: 'hoge',fuga: 'fuga'})
}
//fetchAPIの第二引数にパラメータを代入
fetch(url,param).then((res)=>{
return res.json();
})
.then((json)=>{
})
PHP側での記述
パラメータにヘッダ情報を埋め込んだため、ヘッダ情報は不要となります。また変数の取得も以下のように記述が大幅に変更となります。CORS対策も適宜、しておいた方が無難です。
//CORS対策
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: *');
//json取得
$buff = file_get_contents('php://input'); //サーバ上のパラメータを受け取る
$json = json_decode($buff,true); //jsonをデコードし、配列として格納する
$data1 = '';
$data2 = '';
//配列化しているため、一度issetで確認しないとnoticeが発生する
if(isset($json['hoge'])) $data1 = $json['hoge'];
if(isset($json['fuga'])) $data2 = $json['fuga'];
2:ローカル上に非同期データを展開させる
通常の方法だと、レスポンス結果でしか、取得した値を操作できません。ですが、Fetchならびにaxiosのデータをローカル上に展開したいときもあると思います。そんなときは新たに導入されたasyncとawaitを使用します。イベントリスナー上の関数にasyncを用いて、取得用の関数にawaitで制御すれば、問題なくデータを取得できます。
//非同期通信でデータをやりとりする関数
const getData = async ()=>{
const data = await fetch(...)
return data
}//イベントリスナー上の関数にasyncを用いる
window.addEventListener('DOMContentLoaded',async ()=>{
data = await getData() //非同期通信データの取得
})
3:画像データを送る
JavaScriptには自在に画像を操作できるcanvasというライブラリがあります。このcanvasで加工したデータをサーバ上に保存したい場合もあると思います。そのときは、以下の方法でデータを送れます。
const cvs = document.querySelector("#canvas") //id名canvas上のcanvasタグ
canvas = cvs.toDataURL("image/jpeg",0.9) //pngの場合はjpegを変える
あとは同様にjsonで送るだけです。…が、そのままだと画像変換できません。
(注)ヘッダ情報を除去すること
base64形式にして、json上に画像を送った場合は必ずヘッダ情報が付与されるのですが、付与されたヘッダ情報がサーバ上でデコードする際の邪魔になります。したがって、サーバ上で画像に戻す場合は、必ずヘッダ情報を除去してください。
PHPで画像を保存したい場合、以下のようになります。
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: *');
$buff = file_get_contents('php://input');
$json = json_decode($buff,true);
(isset($json['data'])) $data = $json['data'];
$data = str_replace('data:image/jpeg;base64,', '', $data); //デコードに邪魔なヘッダ情報を除去
$image = base64_decode($data); //画像にデコードする
file_put_contents($save_path,$image,LOCK_EX); //画像を保存したい場合
サーバサイドの処理はどれも同じなので、PythonやRubyの場合でも、上記の手順を踏むことになります。