解説コードを読んでみよう
このサンプルでわかること
- 画面(HTML + jQuery)と、データの保存(PHP)を分けて作るアプリの組み立て方
- ajax で「読み込み・追加・編集・削除」を行う流れ
- あとから追加された要素のボタンにも、クリック処理をつける方法
このアプリは、もともと Android アプリ(HTML で作った画面をアプリにする方法)として作りました。アプリの中身は普通の HTML なので、上のデモのようにブラウザでも動きます。
使っているファイル
- app.htmlアプリの画面(jQuery Mobile で作ったスマホ向けの見た目)
- data_select.phpメモの一覧を返す
- data_insert.phpメモを追加する
- data_edit.php編集用の入力欄を返す
- data_update.phpメモを更新する
- data_delete.phpメモを削除する
- memo_store.phpメモを読み書きする関数と、エスケープ用の関数
h()
0. 保存の仕組み(セッション)
見本なので、データベースの代わりにセッションに保存しています。セッションは、サーバーが見ている人ごとに用意する一時的な保存場所です。読み書きの処理は memo_store.php の関数にまとめ、data_*.php はそれを呼ぶだけにしています。
<?php
// メモを読み書きする関数(data_*.php から読み込んで使う)
// 見本なので、データベースの代わりに「セッション」に保存します
// (セッション=サーバーが、見ている人ごとに用意してくれる一時的な保存場所)
const MEMO_MAX = 20; // 1人が持てるメモの数(多くなったら古いものから消す)
date_default_timezone_set('Asia/Tokyo'); // 日時は日本時間で記録する
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
function h($s): string
{
return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
}
// メモの一覧(初めて開いたときは、見本のメモを2件入れておく)
function memo_all(): array
{
if (!isset($_SESSION['memos']) || !is_array($_SESSION['memos'])) {
$_SESSION['memos'] = [];
$_SESSION['memo_id'] = 0;
memo_add('はじめてのメモ', '「編集」で書き換え、「削除」で消せます。');
memo_add('買い物リスト', '牛乳・たまご・食パン');
}
return $_SESSION['memos'];
}
// 番号でメモを1件取り出す(なければ null)
function memo_get(int $id): ?array
{
return memo_all()[$id] ?? null;
}
// メモを追加して、追加したメモを返す
function memo_add(string $title, string $content): array
{
memo_all();
$id = ++$_SESSION['memo_id']; // 番号は 1, 2, 3… と自動で増やす
$_SESSION['memos'][$id] = [
'id' => $id,
'title' => $title,
'content' => $content,
'date' => date('Y-m-d H:i'),
];
while (count($_SESSION['memos']) > MEMO_MAX) {
unset($_SESSION['memos'][array_key_first($_SESSION['memos'])]);
}
return $_SESSION['memos'][$id];
}
// メモを書き換えて、書き換えたメモを返す(なければ null)
function memo_update(int $id, string $title, string $content): ?array
{
if (memo_get($id) === null) {
return null;
}
$_SESSION['memos'][$id]['title'] = $title;
$_SESSION['memos'][$id]['content'] = $content;
return $_SESSION['memos'][$id];
}
// メモを消す
function memo_delete(int $id): void
{
memo_all();
unset($_SESSION['memos'][$id]);
}
// 送られてきたタイトルと内容を確かめる(空・長すぎるものは null)
function memo_input(): ?array
{
$title = trim((string)($_POST['title'] ?? ''));
$content = trim((string)($_POST['content'] ?? ''));
if ($title === '' || $content === '' || mb_strlen($title) > 100 || mb_strlen($content) > 1000) {
return null;
}
return ['title' => $title, 'content' => $content];
}
// 書き込みの処理は POST で送られたときだけ受け付ける
function require_post(): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
}
// メモ1件を表示する HTML
function memo_html(array $m): string
{
return '<div id="' . h($m['id']) . '">'
. '<h3>' . h($m['title']) . '</h3>'
. '<p>' . h($m['date']) . ' ' . nl2br(h($m['content']))
. '<button class="del ui-btn ui-icon-delete ui-btn-icon-left">削除</button>'
. '<button class="edt ui-btn ui-icon-edit ui-btn-icon-left">編集</button></p>'
. '</div>';
}
// メモ1件を編集する入力欄の HTML
function memo_edit_html(array $m): string
{
return '<div id="' . h($m['id']) . '" class="editing">'
. '<label>タイトル<input type="text" class="title" maxlength="100" value="' . h($m['title']) . '"></label>'
. '<label>内容<textarea class="content" rows="3" maxlength="1000">' . h($m['content']) . '</textarea></label>'
. '<button class="upd ui-btn ui-icon-check ui-btn-icon-left">更新する</button>'
. '</div>';
}
メモの内容を画面に出すときは、必ず h()(htmlspecialchars)でエスケープしています。件名に <script> などを書かれても、ただの文字として表示され、プログラムとして動かないようにするためです。
1. 画面を開いたら一覧を読み込む
app.html は、開くとすぐ $.get() で data_select.php を呼び、返ってきた HTML(メモの一覧)を #all の中に入れます。
<?php
// メモの一覧を HTML で返す
require_once __DIR__ . '/memo_store.php';
foreach (memo_all() as $memo) {
echo memo_html($memo);
}
2. メモを追加する
入力が空でないか確かめてから、件名と本文を data_insert.php に送ります。PHP 側は保存したあと、追加したメモの HTML を返すので、それを一覧の最後に append()(付け足し)します。
<?php
// メモを1件追加して、追加したメモの HTML を返す
require_once __DIR__ . '/memo_store.php';
require_post();
$input = memo_input();
if ($input === null) {
http_response_code(400); // 空や長すぎる入力は受け付けない
exit;
}
echo memo_html(memo_add($input['title'], $input['content']));
PHP 側でも、空の入力や長すぎる入力をはじいています。JavaScript のチェックは、ブラウザの開発ツールなどで簡単に飛ばせてしまうため、サーバー側のチェックが本当の守りになります。
3. 編集・更新・削除する
一覧のボタンはあとから追加されるので、$('#all').on('click', 'button.del', ...) のように「親の要素」にクリック処理をつけます。こうすると、あとから増えたボタンにも反応します。どのメモのボタンかは、ボタンを囲む <div id="番号"> の id で見分けます。
<?php
// 編集用の入力欄の HTML を返す
require_once __DIR__ . '/memo_store.php';
require_post();
$memo = memo_get((int)($_POST['id'] ?? 0));
if ($memo === null) {
http_response_code(404);
exit;
}
echo memo_edit_html($memo);
<?php
// メモを更新して、更新後のメモの HTML を返す
require_once __DIR__ . '/memo_store.php';
require_post();
$input = memo_input();
$memo = $input === null ? null : memo_update((int)($_POST['id'] ?? 0), $input['title'], $input['content']);
if ($memo === null) {
http_response_code(400);
exit;
}
echo memo_html($memo);
<?php
// メモを削除する
require_once __DIR__ . '/memo_store.php';
require_post();
memo_delete((int)($_POST['id'] ?? 0));
echo 'ok';
4. アプリの画面(app.html の全体)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MyNote(メモアプリ)</title>
<link rel="stylesheet" href="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" />
<script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
<style>
/* 見本ページの枠(iframe)の中で、きれいに見せるための調整 */
.ui-page { min-height: 0 !important; }
#all > div { border-bottom: 1px solid #ddd; padding-bottom: 8px; }
#all h3 { margin: 14px 0 4px; }
#all p { word-break: break-all; }
.editing { padding-top: 10px; }
.editing label { display: block; margin: 6px 0; font-size: 14px; }
.editing input, .editing textarea {
display: block; width: 100%; box-sizing: border-box; margin-top: 4px;
font-size: 16px; padding: 6px 8px; border: 1px solid #ccc; border-radius: 4px;
}
</style>
</head>
<body>
<script type="text/javascript">
$(function(){
// 開いたら、メモの一覧を読み込む
$.get('data_select.php', function (data) {
$('#all').html(data);
});
// 「ページを作る」:メモを追加する
$('#post').click(function(){
if ($('button').hasClass('upd')) {
alert('その他のページの編集が完了していません。');
} else if ($('#title').val() === '' || $('#content').val() === '') {
alert('空白箇所があります。');
} else {
var title = $('#title').val();
var content = $('#content').val();
$.ajax({
url: 'data_insert.php',
type: 'POST',
data: { title: title, content: content },
timeout: 10000,
dataType: 'text'
}).done(function (data) {
$('#title').val('');
$('#content').val('');
$('#all').append(data);
});
}
});
// 一覧のボタンはあとから増えるので、親の #all にクリック処理をつける
$('#all').on('click', 'button.del', function(){
// 「削除」
var id = $(this).parent().parent().attr('id');
if (window.confirm('No.' + id + ' 本当にこのページを削除しますか?')) {
$.ajax({
url: 'data_delete.php',
type: 'POST',
data: { id: id },
timeout: 10000,
dataType: 'text'
}).done(function () {
$('#' + id).remove();
});
}
}).on('click', 'button.edt', function(){
// 「編集」:入力欄に入れ替える
if ($('button').hasClass('upd')) {
alert('その他のページの編集が完了していません。');
} else {
var id = $(this).parent().parent().attr('id');
$.ajax({
url: 'data_edit.php',
type: 'POST',
data: { id: id },
timeout: 10000,
dataType: 'text'
}).done(function (data) {
$('#' + id).replaceWith(data);
});
}
}).on('click', 'button.upd', function(){
// 「更新する」:書き換えた内容を送る
if ($('.title').val() === '' || $('.content').val() === '') {
alert('空白箇所があります。');
} else {
var title = $('.title').val();
var content = $('.content').val();
var id = $(this).parent().attr('id');
$.ajax({
url: 'data_update.php',
type: 'POST',
data: { title: title, content: content, id: id },
timeout: 10000,
dataType: 'text'
}).done(function (data) {
$('#' + id).replaceWith(data);
});
}
});
});
</script>
<div data-role="page">
<div data-role="header">
<h1>MyNote</h1>
</div>
<div data-role="content">
<div>
タイトル: <input id="title" maxlength="100"><br>
内容: <textarea id="content" rows="3" maxlength="1000"></textarea><br>
<button id="post">ページを作る</button>
</div>
<h2>ノート一覧</h2>
<div id="all"></div>
</div>
<div data-role="footer">
<p style="text-align:center">MyNote サンプル</p>
</div>
</div>
</body>
</html>
先頭の <style> の .ui-page { min-height: 0 !important; } は、このページの枠(iframe)の中で表示するための調整です。jQuery Mobile は画面の高さいっぱいに広がろうとするため、枠の高さと競い合って、スマホで画面が崩れていました。
データベースを使うときは
このページは見本なので、データベースを使わずに動かしています。本物のサービスにするときは、データを取り出す関数の中身だけをデータベースの読み書きに取り替えます。画面の作り方(HTML と JavaScript)は同じままで使えます。
例えば、次のテーブルを作り、memo_store.php の各関数の中で SQL を実行します。
CREATE TABLE sp_post_app (
id INT AUTO_INCREMENT PRIMARY KEY, -- 番号(自動で増える)
title VARCHAR(100) NOT NULL, -- 件名
content TEXT NOT NULL, -- 本文
date DATETIME DEFAULT CURRENT_TIMESTAMP -- 作成日時(自動で入る)
);
function memo_add(string $title, string $content): array
{
$stmt = db()->prepare('INSERT INTO sp_post_app (title, content) VALUES (:title, :content)');
$stmt->execute([':title' => $title, ':content' => $content]);
return memo_get((int)db()->lastInsertId());
}