中級 jQuery ajax PHP セッション

円グラフのプラグイン Circliful:進み具合をデータベースに保存する

レッスンの完了状態を ajax で PHP に送って保存します。ページを再読み込みしても、進み具合の円グラフが残ります。見本ではデータベースの代わりにセッションを使い、データベースでの書き方も紹介します。

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

DEMO 実際に動かしてみよう

はじめての表計算 全4回

  • 第1回 表を作ってみよう
  • 第2回 足し算と合計
  • 第3回 グラフを作る
  • 第4回 印刷のしかた

完了したレッスン:0 / 4回
進み具合:0%

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

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

  • ボタンを押したときに、ajax でサーバーの PHP へ状態を送る方法
  • PHP が返した JSON を受け取って、画面と円グラフを作り直す方法
  • 保存した状態を、ページを開いたときに PHP で読み込んで表示する方法

ひとつ前のサンプル「レッスンの進み具合を表示する」では、再読み込みすると完了状態が消えてしまいました。ここでは状態をサーバーに保存して、残るようにします。

いくつか「完了」にしてから、ページを再読み込みしてみてください。円グラフが同じ進み具合から始まります。

使っているファイル

  • lesson.phpデモのページ
  • save.phpajax で呼ばれ、完了状態を保存して JSON を返す
  • lesson_store.php完了状態を読み書きする関数
  • js/jquery.circliful.jsCircliful 本体
  • css/jquery.circliful.cssCircliful 用の CSS

1. 保存の仕組み(セッション)

見本なので、データベースの代わりにセッションに保存しています。セッションは、サーバーが見ている人ごとに用意する一時的な保存場所です。ほかの人の画面には影響せず、ブラウザを閉じたり時間がたったりすると消えます。

lesson_store.php PHP
<?php
// レッスンの完了状態を読み書きする関数
// 見本なので、データベースの代わりに「セッション」に保存します
// (セッション=サーバーが、見ている人ごとに用意してくれる一時的な保存場所)

const LESSON_COUNT = 4;

if (session_status() === PHP_SESSION_NONE) {
    session_start();
}

// 保存されている完了状態を読む(まだ何もなければ、全部「未完了」)
function lessons_load(): array
{
    $saved = $_SESSION['lessons'] ?? null;
    if (!is_array($saved) || count($saved) !== LESSON_COUNT) {
        $saved = array_fill(0, LESSON_COUNT, false);
    }
    return array_map('boolval', array_values($saved));
}

// $no 番目(0 から数える)のレッスンの完了状態を保存する
function lessons_save(int $no, bool $done): void
{
    $lessons = lessons_load();
    $lessons[$no] = $done;
    $_SESSION['lessons'] = $lessons;
}

// 全部「未完了」に戻す
function lessons_reset(): void
{
    $_SESSION['lessons'] = array_fill(0, LESSON_COUNT, false);
}

// 画面に必要な数字をまとめて返す
function lessons_summary(): array
{
    $done  = lessons_load();
    $count = count(array_filter($done));
    return [
        'done'  => $done,                                     // 例: [true, false, true, false]
        'count' => $count,                                    // 完了した数
        'ratio' => (int)round($count / LESSON_COUNT * 100),   // 進み具合(%)
    ];
}

$_SESSION に入れた値は、同じ人が次にページを開いたときにも読み出せます。使う前に session_start() を呼ぶ決まりです。

2. ページを開いたときに、保存済みの状態を表示する

ページの先頭で lessons_summary() を呼び、完了したボタンには最初から ON クラスと「完了済」の文字を付けておきます。

lesson.php(146〜150行目) PHP
<?php foreach ($titles as $i => $title): ?>
<?php   $on = $state['done'][$i]; ?>
      <li>第<?= $i + 1 ?>回 <?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?>
        <button type="button" class="btn switch<?= $on ? ' ON' : '' ?>" data-no="<?= $i ?>"><?= $on ? '完了済' : '未完了' ?></button></li>
<?php endforeach; ?>

円グラフも、保存済みの進み具合($state['ratio'])から始めます。

lesson.php(173〜176行目) PHP
  // ① 円グラフを作る(PHP が読み込んだ、保存済みの進み具合から始める)
  $("#circle").circliful({
    percent: <?= $state['ratio'] ?>
  });

3. ボタンを押したら、サーバーに送る

押したボタンの番号(data-no)と、新しい状態(完了なら 1、未完了なら 0)を $.post() で save.php に送ります。

lesson.php(178〜182行目) PHP
  // ② 完了ボタンを押したら、新しい状態を save.php に送る
  $("button.switch").on("click", function(){
    var done = $(this).hasClass("ON") ? 0 : 1;   // 今と反対の状態にする
    send({ action: "save", no: $(this).data("no"), done: done });
  });

4. save.php で保存して、今の状態を返す

受け取った番号が 0〜3 の整数かを filter_var() で確かめてから保存します。最後に、今の状態をまとめて JSON で返します。

save.php PHP
<?php
// ajax で受け取った完了状態を保存して、今の状態を JSON で返す
require_once __DIR__ . '/lesson_store.php';
header('Content-Type: application/json; charset=UTF-8');

$action = $_POST['action'] ?? '';

if ($action === 'save') {
    // レッスンの番号が 0〜3 の整数かどうか確かめる
    $no = filter_var($_POST['no'] ?? '', FILTER_VALIDATE_INT,
        ['options' => ['min_range' => 0, 'max_range' => LESSON_COUNT - 1]]);
    if ($no === false) {
        http_response_code(400);
        echo json_encode(['error' => 'レッスンの番号が正しくありません'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    lessons_save($no, ($_POST['done'] ?? '') === '1');
} elseif ($action === 'reset') {
    lessons_reset();
}

echo json_encode(lessons_summary(), JSON_UNESCAPED_UNICODE);

返ってくる JSON は、例えば次のような形です。

{"done":[true,false,true,false],"count":2,"ratio":50}

5. 返ってきた状態で、画面を作り直す

ボタンの見た目・円グラフ・数字を、サーバーが返した状態に合わせて作り直します。画面の表示と保存した中身がずれません。

lesson.php(189〜205行目) PHP
  // ④ サーバーに送り、返ってきた状態で画面を作り直す
  function send(data) {
    $.post("save.php", data, null, "json")
      .done(function(res){
        $("button.switch").each(function(i){
          $(this).toggleClass("ON", res.done[i]).text(res.done[i] ? "完了済" : "未完了");
        });
        $("#circle .circle").attr("stroke-dasharray", (res.ratio * 3.6) + ", 20000");
        $("#circle .number").text(res.ratio);
        $("#cmp").text(res.count);
        $("#ratio").text(res.ratio);
        $("#msg").text("保存しました");
      })
      .fail(function(){
        $("#msg").text("保存できませんでした。時間をおいて試してください。");
      });
  }

ブラウザから送られてくる値は、書き換えられているかもしれません。番号の範囲などは、必ず PHP 側でも確かめます。

データベースを使うときは

このページは見本なので、データベースを使わずに動かしています。本物のサービスにするときは、データを取り出す関数の中身だけをデータベースの読み書きに取り替えます。画面の作り方(HTML と JavaScript)は同じままで使えます。

例えば、会員ごとの完了状態を残すなら、次のようなテーブルを作り、lessons_save() の中で書き込みます。

テーブルを作る SQL(例) SQL
CREATE TABLE lesson_progress (
  user_id   INT NOT NULL,        -- 会員の番号
  lesson_no INT NOT NULL,        -- レッスンの番号(0〜3)
  done      TINYINT(1) NOT NULL, -- 完了なら 1
  PRIMARY KEY (user_id, lesson_no)
);
lesson_store.php(データベース版の例) PHP
function lessons_save(int $no, bool $done): void
{
    $stmt = db()->prepare(
        'INSERT INTO lesson_progress (user_id, lesson_no, done) VALUES (:user, :no, :done)
         ON DUPLICATE KEY UPDATE done = VALUES(done)'   // あれば書き換え、なければ追加
    );
    $stmt->execute([':user' => current_user_id(), ':no' => $no, ':done' => (int)$done]);
}
↑