はじめに
データベースを使ってコンテンツを表示するページを作りたいと、友人(@digital24s さん)から相談されました。ウェブアプリでデータベースに接続する仕組を整理しました。
ウェブアプリでデータベースに接続する #JavaScript - Qiita
その続きです。
ウェブアプリでデータベースを操作する
友人も自分も Apache+PHP+MySQL のレンタルサーバを利用しています。PHP で作成したアプリを、ここで実行できるようにして、登録や削除などデータベースを操作できるようにしたいと思います。
バックエンドプログラムを用意する
前回の記事の server.php は slim フレームワークを使っていて、Apache+PHP のレンタルサーバで運用するのに mod_rewrite が有効でないといけません。ところが自分が利用しているレンタルサーバを確認すると、mode_rewirte が有効でありませんでした。mode_rewrite が使えないサーバで動作するよう、slim フレームワークを使わないで実装してみます。
$_SERVER['PATH_INFO'] で、呼出されたときのパスを取得できます。例えば
https://(ホスト)/path/server.php/actor/list
で呼出すると
/actor/list
が取得できます。
これを使って書換します。↓
<?php
$path = $_SERVER['PATH_INFO'] ?? "/";
if ($path === "/") {
header('Content-Type: text/html; charset=UTF-8');
readfile(__DIR__ . "/client.html");
exit;
}
if ($path === "/actor/list") {
// データベースに接続
$pdo = new PDO("mysql:host=(ホスト); dbname=sakila", "(ユーザ)", "(パスワード)");
// データを取得
$stmt = $pdo->query("SELECT actor_id, first_name, last_name, last_update FROM actor");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 結果を返す
header('Content-Type: application/json; charset=UTF-8');
print json_encode($rows, JSON_UNESCAPED_UNICODE);
exit;
}
PHP プログラムをドキュメントルートのディレクトリに配置して、そのディレクトリに以下の設定します。
# ファイルの指定がないときは server.php を呼出
DirectoryIndex server.php
フロントエンドプログラムを用意する
前回の記事の client.html を利用します。
(前略)
<button id="get">取得</button>
<ul id="list"></ul>
<div id="result"></div>
(後略)
document.querySelector('#get').addEventListener('click', () => {
// サーバから取得
fetch("server.php/actor/list")
server.php を呼出するようにします。↑
.then((res) => {
return res.json();
})
.then((res) => {
// 取得したデータを表示
const list = document.querySelector('#list');
list.innerHTML = "";
res.forEach((item) => {
const elem = document.createElement('li');
elem.innerHTML = `
<span>(${item.actor_id}) ${item.first_name} ${item.last_name}</span>
`;
list.appendChild(elem);
});
document.querySelector('#result').innerHTML = "Succeeded.";
})
.catch((err) => {
document.querySelector('#result').innerHTML = "Failed: " + err.message;
});
});
エラー発生したことを返すようにする
データベースの接続とデータの取得でエラー発生したとき、フロントエンドに通知できるようにしておきます。
if ($path === "/actor/list") {
try {
// データベースに接続
$pdo = new PDO("mysql:host=(ホスト); dbname=sakila", "(ユーザ)", "(パスワード)");
// データを取得
$stmt = $pdo->query("SELECT actor_id, first_name, last_name, last_update FROM actor");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 結果を返す
header('Content-Type: application/json; charset=UTF-8');
print json_encode($rows, JSON_UNESCAPED_UNICODE);
exit;
}
catch (Throwable $e) {
// エラー発生したことを返す
header('Content-Type: application/json; charset=UTF-8');
print json_encode([
'status' => 'error',
'message' => $e->getMessage()
], JSON_UNESCAPED_UNICODE);
exit;
}
}
エラーを通知する JSON の形式は「JSend」に準ずることにします。
JSend - JSONに緩いルールを適用して開発しやすく - NTT docomo Business Engineers' Blog
バックエンドプログラムが返す結果に status: 'error' が含まれることがあることに、フロントエンドプログラムも対応します。
(前略)
.then((res) => {
// 取得したデータを表示
const list = document.querySelector('#list');
list.innerHTML = "";
if (res.status === 'error') { // エラー発生が返されるのに対応
document.querySelector('#result').innerHTML = "Failed: " + res.message;
return;
}
res.forEach((item) => {
(後略)
結果を返す JSON データも JSend 仕様にしておきましょう。
(前略)
// 結果を返す
header('Content-Type: application/json; charset=UTF-8');
print json_encode([
'status' => 'success',
'data' => [
'rows' => $rows
]
], JSON_UNESCAPED_UNICODE);
exit;
}
(後略)
フロントエンドプログラムも合わせておきます。
(前略)
.then((res) => {
return res.json();
})
.then((res) => {
(中略)
res.data.rows.forEach((item) => {
(後略)
条件を指定して一覧できるようにする
上記のプログラムはテーブルに登録されている全てのデータを取得して表示しています。条件を指定して一致したデータを取得して表示するようにしたいと思います。
PHPのパラメータはこれで解決!値送信から取得まで解説 | 侍エンジニアブログ
まずフロントエンドプログラムを修正します。
(前略)
<input type="text" id="keyword" placeholder="キーワード">
<button id="find">検索</button>
(後略)
document.querySelector('#find').addEventListener('click', () => {
const keyword = document.querySelector('#keyword').value;
const params = new URLSearchParams({ keyword: keyword });
// サーバから取得
fetch("server.php/actor/find?" + params)
(後略)
パスを /actor/list から /actor/find にしました。? に続けてパラメータを指定します。↑
続いてバックエンドプログラムを修正します。
if ($path === "/actor/find") {
try {
// 指定されたパラメータを取得
$keyword = $_GET['keyword'] ?? '';
// データベースに接続
$pdo = new PDO("mysql:host=(ホスト); dbname=sakila", "(ユーザ)", "(パスワード)");
// データを取得
$stmt = $pdo->prepare("SELECT actor_id, first_name, last_name, last_update FROM actor "
. "WHERE first_name LIKE :keyword OR last_name LIKE :keyword");
$stmt->bindValue(':keyword', "%{$keyword}%", PDO::PARAM_STR);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
(後略)
登録と削除できるようにする
続いて、データを「登録」「削除」できるようにしたいと思います。
まずフロントエンドプログラムを修正します。
<form id="form">
<input type="text" name="actor_id" placeholder="Actor ID">
<input type="text" name="first_name" placeholder="First Name">
<input type="text" name="last_name" placeholder="Last Name">
</form>
<button id="regist">登録</button>
<button id="remove">削除</button>
document.querySelector('#regist').addEventListener('click', () => {
// サーバに送信
const data = new FormData(document.querySelector('#form'));
fetch("server.php/actor/regist", {
method: "POST",
body: data
})
.then((res) => {
return res.json();
})
.then((res) => {
if (res.status === 'error') {
document.querySelector('#result').innerHTML = "Failed: " + res.message;
return;
}
document.querySelector('#result').innerHTML = "Succeeded.: " + res.message;
})
.catch((err) => {
document.querySelector('#result').innerHTML = "Failed: " + err.message;
});
});
document.querySelector('#remove').addEventListener('click', () => {
// サーバに送信
const data = new FormData(document.querySelector('#form'));
fetch("server.php/actor/remove", {
method: "POST",
body: data
})
(後略)
パス /actor/regist で「新規登録」と「更新登録」します。「削除」は /actor/remove にします。登録内容は form タグでまとめることにします。POST メソッドにして、body に登録内容をセットします。↑
続いてバックエンドプログラムを修正します。
$method = $_SERVER['REQUEST_METHOD'];
if ($method == "POST" && $path === "/actor/regist") {
try {
// 指定されたパラメータを取得
$actorId = $_POST['actor_id'] ?? null;
$firstName = $_POST['first_name'] ?? '';
$lastName = $_POST['last_name'] ?? '';
// データベースに接続
$pdo = new PDO("mysql:host=(ホスト); dbname=sakila", "(ユーザ)", "(パスワード)");
// データを挿入/更新
$stmt = $pdo->prepare("INSERT INTO actor (actor_id, first_name, last_name, last_update) VALUES (:actor_id, :first_name, :last_name, NOW()) "
. "ON DUPLICATE KEY UPDATE first_name = VALUES(first_name), last_name = VALUES(last_name), last_update = NOW()");
$stmt->bindValue(':actor_id', $actorId, PDO::PARAM_INT);
$stmt->bindValue(':first_name', $firstName, PDO::PARAM_STR);
$stmt->bindValue(':last_name', $lastName, PDO::PARAM_STR);
$stmt->execute();
// 処理が成功したことを返す
header('Content-Type: application/json; charset=UTF-8');
print json_encode([
'status' => 'success',
'message' => 'regist succeeded.'
], JSON_UNESCAPED_UNICODE);
exit;
}
catch (Throwable $e) {
// 処理が失敗したことを返す
header('Content-Type: application/json; charset=UTF-8');
print json_encode([
'status' => 'error',
'message' => $e->getMessage()
], JSON_UNESCAPED_UNICODE);
exit;
}
}
if ($method == "POST" && $path === "/actor/remove") {
try {
// 指定されたパラメータを取得
$actorId = $_POST['actor_id'] ?? null;
// データベースに接続
$pdo = new PDO("mysql:host=(ホスト); dbname=sakila", "(ユーザ)", "(パスワード)");
// データを削除
$stmt = $pdo->prepare("DELETE FROM actor WHERE actor_id = :actor_id");
$stmt->bindValue(':actor_id', $actorId, PDO::PARAM_INT);
$stmt->execute();
// 処理が成功したことを返す
print json_encode([
'status' => 'success',
'message' => 'remove succeeded.'
], JSON_UNESCAPED_UNICODE);
exit;
}
(中略)
}
INSERT ~ ON DUPLICATE KEY UPDATE を使うことで「新規登録」と「更新登録」をまとめて処理できるようにします。↑
ウェブ API の仕様について考える
上記のコードは、バックエンドプログラムがフロントエンドプログラムに呼出されて結果を返しています。これは「ウェブ API」ですね。
上記の例では、パス /actor/find で URL に ?keyword=... でパラメータを指定して GET メソッドで呼出して、JSON データ { status: 'success', data: { rows: [{ actor_id:'9', ... }, { ... }] }} を返すようにしています。
この仕様は、アプリ開発者がアプリごとに決めるしかなさそうです。とはいえ、どう決めるのが望ましいのでしょうか。
- パスの構造
-
GET、POSTメソッドだけかPUT、PATCH、DELETEも使うか - HTTP ステータスを使うか
- JSON データの構造
調べてみると、指針はあるようです。
POST だけ?こんな馬鹿げた API デザインの議論を終わらせよう - Logto ブログ
REST APIとRPCによるアーキテクチャの違いをまとめてやんよ!!! - ときどきWEB
REST とは
「REST (Representational State Transfer) 」は、ウェブ API の設計に広く採用されている仕様の一つです。
RESTとは - IT用語辞典 e-Words
0からREST APIについて調べてみた #REST-API - Qiita
上記のコードで用意したウェブ API は、REST の仕様でないと言えそうです。REST の仕様に従った API にするにはどうしたらいいでしょうか。
REST APIのエンドポイント設計 - コグラフ株式会社 データアナリティクス事業部
RESTful APIのレスポンスデータ設計 #Java - Qiita
上記のコードの API を REST 仕様にすると、パス /actors で URL に ?keyword=... でパラメータを指定して GET メソッドで呼出して、成功したときはステータスコード 200 で JSON データ { data: { actors: [{ actor_id:'9', ... }, { ... }] }} を返し、失敗したときはステータスコード 500 で JSON データ { error: { message: '...' }} を返すなどになるでしょうか。
ただし「REST API」は、上記の要件を満たさないで、SOAP や RPC などを必要としない、JSON や XML と HTTP を用いた簡易なウェブ上のインタフェースを指すこともあります。その点で上記のコードの例も REST API と言えるかも知れません。
RPC とは
「RPC (Remote Procedure Call) 」は、ネットワーク越に別のコンピュータ上にあるプログラム(手続)を実行する技術です。
別のコンピュータの機能を呼出するという点で、ウェブ API も RPC の一種だと言えるでしょうか。
RPC(リモートプロシージャコール / 遠隔手続き呼び出し)とは - IT用語辞典 e-Words
そもそも RPC ってなんだ #RPC - Qiita
RPC は、さまざまな実装が可能です。そのうち XML-RPC や JSON-RPC は、ウェブサーバを中継して提供され、つまりウェブ API として実装されることもあると言えそうです。
RPC は REST と違って、呼出先のプロシージャ(手続)が呼出元に透けて見えるように実装されます。
上記のコードの API を RPC 仕様にすると、パス /findActors で JSON データ { params: { keyword: ... } } を指定して POST メソッドで呼出して、JSON データ { status: 'success', data: { rows: [{ actor_id:'9', ... }, { ... }] }} を返すなどになるでしょうか。
さらに JSON-RPC の仕様にすると、パス /api で JSON データ { jsonrpc: '1.0', method: 'findActors', params: { keyword: ... }, id: 9 } を指定して POST メソッドで呼出して、JSON データ { jsonrpc: '1.0', result: { rows: [{ actor_id:'9', ... }, { ... }], id: 9 }} を返すなどになるでしょう。