中級 jQuery Mobile ajax PHP セッション

スマホ用メモアプリ(追加・編集・削除)

スマホアプリの画面(jQuery Mobile)から ajax でサーバーの PHP を呼び出し、メモを追加・編集・削除するアプリです。画面は HTML、データの保存はサーバー、という役割分担がわかります。

このサンプルは2017〜2018年ごろに作成したものです。2026年に PHP 8.3 で動くように書き直し、さらにデータベースがなくても動く見本に作り変えました。データベースを使うときの書き方は、解説の最後に載せています。

DEMO 実際に動かしてみよう

スマホアプリの画面を、そのままブラウザで動かしています。タイトルと内容を入れて「ページを作る」を押すと、メモがサーバーに保存されます。「編集」「削除」も試してみてください。

※ メモは、見ている人ごとにサーバーへ一時的に保存されます。ほかの人には見えません。ブラウザを閉じたり、時間がたったりすると消えます(最大20件)。

解説コードを読んでみよう

このサンプルでわかること

  • 画面(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 はそれを呼ぶだけにしています。

memo_store.php 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 の中に入れます。

data_select.php PHP
<?php
// メモの一覧を HTML で返す
require_once __DIR__ . '/memo_store.php';

foreach (memo_all() as $memo) {
    echo memo_html($memo);
}

2. メモを追加する

入力が空でないか確かめてから、件名と本文を data_insert.php に送ります。PHP 側は保存したあと、追加したメモの HTML を返すので、それを一覧の最後に append()(付け足し)します。

data_insert.php PHP
<?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 で見分けます。

data_edit.php PHP
<?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);
data_update.php PHP
<?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);
data_delete.php PHP
<?php
// メモを削除する
require_once __DIR__ . '/memo_store.php';
require_post();

memo_delete((int)($_POST['id'] ?? 0));
echo 'ok';

4. アプリの画面(app.html の全体)

app.html 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 を実行します。

テーブルを作る SQL(例) 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   -- 作成日時(自動で入る)
);
memo_store.php(データベース版の例) PHP
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());
}
↑