PHPを使った在庫テーブルの更新とページネーション

学習目標

  • データベースに登録された在庫データを一覧表示する
  • ページネーション(ページ分割) を実装して、10件ずつ表示する
  • 各行の在庫数をフォームで更新できるようにする
  • 更新後にリロードしても二重送信されないようにする(PRGパターン)
  • Bootstrapを使って見やすいUIに整える

完成イメージ

フォルダ構成とファイル名

php-stock/
├── common.php   # DB接続とエスケープ関数
├── stock.php    # 在庫一覧とページネーション、更新フォーム
├── update.php   # 更新処理(PRGパターン)
└── seed.php     # 初期データ投入(30件)

データベース準備

データベース:testdbに以下のSQLでテーブルを作成します。

CREATE TABLE items (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    stock INT DEFAULT 0
);

初期データ投入(seed.php)

学習用に30件のダミーデータを入れます。
毎回リセットしてIDを1から振り直す仕様にしています。

<?php
require 'common.php';

// 既存データ削除
//TRUNCATE TABLE はAUTO_INCREMENTもリセットされるので、IDが毎回1からスタートします。
//本番システムではこ使いません。
$pdo->exec("TRUNCATE TABLE items");

for ($i = 1; $i <= 30; $i++) {
    $stmt = $pdo->prepare("INSERT INTO items (name, stock) VALUES (?, ?)");
    $stmt->execute(["商品{$i}", rand(0, 20)]);
}

echo "データ投入完了(既存データは削除されました)";

共通ファイル(common.php)

PDOで接続を作り、全ページで使える h() 関数を定義。

<?php
// DB接続(例外モードON)
$pdo = new PDO(
    'mysql:host=localhost;dbname=testdb;charset=utf8',
    'root',
    '',
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);

// HTMLエスケープ関数
function h($str)
{
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

まずは在庫一覧(stock.php)

  1. SELECT文で、itemsデータを取得。HTMLのテーブルで一覧表示
  2. 各行にフォームを設置 → update.php に送信
  3. 更新後は ?updated=1 付きで戻ってきてメッセージ表示します。
<?php
require 'common.php';

// データ取得 ORDER BY idを入れておくことで、順番が崩れない
$stmt = $pdo->prepare("SELECT * FROM items ORDER BY id");
$stmt->execute();
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>在庫管理</title>
    <!--  Bootstrap CDN -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>

<body class="bg-light">

    <div class="container py-4">
        <h1 class="mb-4 text-center">在庫管理システム</h1>
        <?php if (isset($_GET['updated']) && $_GET['updated'] == 1): ?>
            <div class="alert alert-success alert-dismissible fade show" role="alert">
                在庫を更新しました。
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="閉じる"></button>
            </div>
        <?php endif; ?>

        <table class="table table-striped table-hover shadow-sm">
            <thead class="table-dark">
                <tr>
                    <th style="width:10%;">ID</th>
                    <th style="width:50%;">商品名</th>
                    <th style="width:20%;">在庫</th>
                    <th style="width:20%;">更新</th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($items as $item): ?>
                    <tr>
                        <td><?= h($item['id']) ?></td>
                        <td><?= h($item['name']) ?></td>
                        <td>
                            <input type="number" form="form-<?= $item['id'] ?>" name="stock"
                                value="<?= $item['stock'] ?>" min="0"
                                class="form-control form-control-sm" style="max-width: 80px;">
                        </td>
                        <td>
                            <form id="form-<?= $item['id'] ?>" action="update.php" method="post">
                                <input type="hidden" name="id" value="<?= $item['id'] ?>">
                                <button type="submit" class="btn btn-primary btn-sm w-100">更新</button>
                            </form>
                        </td>
                    </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
</body>

</html>

更新処理(update.php)

POSTデータを受け取り、UPDATE文で反映。完了後は元のページに戻る。

<?php
require 'common.php';

$id = (int)($_POST['id'] ?? 0);
$stock = (int)($_POST['stock'] ?? 0);

$stmt = $pdo->prepare("UPDATE items SET stock = ? WHERE id = ?");
$stmt->execute([$stock, $id]);

header("Location: stock.php?updated=1");
exit;

ページネーションを追加してみよう!

今は全件表示なので、件数が多いとスクロールが長くなります。SQL文のLIMITOFFSET を使って10件ずつ表示にすると実用的です。またupdate.php のリダイレクト時に page を保持すると、更新後も同じページに戻れます。

1.ページの表示件数の設定

$perPage = 10;

2.次のページの表示開始位置

$page = max(1, (int)($_GET['page'] ?? 1));
$offset = ($page - 1) * $perPage;
  • $_GET['page'] ?? 1デフォルトで1ページ目を表示するための書き方
  • max(1, …)はページ番号が 0 や負の数になったときに強制的に 1 にする。
    例:?page=-3 と書かれてもページ1に戻す
  • 1ページ目 → OFFSET 0
  • 2ページ目 → OFFSET 10
  • 3ページ目 → OFFSET 20
  • 4ページ目 → OFFSET 30
  • OFFSETは「何件目からデータを取るか」を指定するものです。

3.全体で「何件あるか」を調べる

$stmt = $pdo->query("SELECT COUNT(*) FROM items");
$total = $stmt->fetchColumn();
$totalPages = ceil($total / $perPage);
  • SELECT COUNT(*)テーブル内の行数を数えるSQL
    → 例:30件あれば「30」が返る
  • fetchColumn() でその数値だけを取り出して $total に入れる
  • 総ページ数は ceil()(小数点切り上げ)で計算
    → 30件 ÷ 10件 = 3ページ
    → 31件 ÷ 10件 = 3.1 → 切り上げで4ページ
  • 「何ページ必要か」を事前に計算しないと、ページリンクが作れない

4.LIMIT / OFFSET でデータ取得

$stmt = $pdo->prepare("SELECT * FROM items ORDER BY id LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);

  • SELECT * FROM items ORDER BY id LIMIT :limit OFFSET :offset
  • LIMIT = 取る件数(例:10件)
  • OFFSET = どこから取るか(例:11件目から)
  • bindValue() を使うことで、変数の値を安全にSQLに渡せる→ SQLインジェクション対策

bindValue と ? プレースホルダの違い

  • プレースホルダは?を使って位置だけ指定するので、順番が大事です。($stmt->execute([$limit, $offset])
  • 名前付きプレースホルダ(:limit, :offset)は順番を気にせず渡せる
  • bindValue を使うと 型を指定できるPDO::PARAM_INT整数型)
  • LIMIT / OFFSET は整数以外が入るとSQLエラーになるので PDO::PARAM_INT を指定するのが安全です

HTMLにページネーション部分を追加

        <!-- ページネーション -->
        <nav aria-label="ページネーション">
            <ul class="pagination justify-content-center">
                <?php for ($i = 1; $i <= $totalPages; $i++): ?>
                    <li class="page-item <?= $i === $page ? 'active' : '' ?>">
                        <a class="page-link" href="?page=<?= $i ?>"><?= $i ?></a>
                    </li>
                <?php endfor; ?>
            </ul>
        </nav>
  • ページ番号の生成for ($i = 1; $i <= $totalPages; $i++)
    → 1から $totalPages まで繰り返し、リンクを作成します。
  • 現在ページの強調表示
    • <li class="page-item <?= $i === $page ? 'active' : '' ?>">
    • $i === $page なら 'active' を出力、違えば空文字を出力
    • 出力結果は次のようになる
      • 現在ページなら<li class="page-item active">
      • それ以外のページなら<li class="page-item">
      • Bootstrap の .active クラスが付くと背景色が変わり、「今いるページ」が強調
  • ページリンクの生成
    • <a class="page-link" href="?page=<?= $i ?>"><?= $i ?></a>
      → クリックすると ?page=1 のようにページ番号を GET パラメータで渡す。
  • 件数が変わっても自動調整
    • $totalPages = ceil($total / $perPage) なので、
      データ件数が増減してもページ数が自動で変わる。

更新処理(update.php)にも追加

$page = $_GET['page'] ?? 1; // 現在ページを保持
header("Location: stock.php?page={$page}&updated=1");
exit;

全体を通してみると
<?php
require 'common.php';

$perPage = 10;
$page = max(1, (int)($_GET['page'] ?? 1));
$offset = ($page - 1) * $perPage;

// 件数を取得するSQLを実行
$stmt = $pdo->query("SELECT COUNT(*) FROM items");
// 結果セットの1列目(COUNTの結果)だけを取り出す
$total = $stmt->fetchColumn();

// 総ページ数を計算(小数点切り上げ)
$totalPages = ceil($total / $perPage);

// データ取得
$stmt = $pdo->prepare("SELECT * FROM items ORDER BY id LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>在庫管理</title>
    <!--  Bootstrap CDN -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>

<body class="bg-light">

    <div class="container py-4">
        <h1 class="mb-4 text-center">在庫管理システム</h1>
        <?php if (isset($_GET['updated']) && $_GET['updated'] == 1): ?>
            <div class="alert alert-success alert-dismissible fade show" role="alert">
                在庫を更新しました。
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="閉じる"></button>
            </div>
        <?php endif; ?>

        <table class="table table-striped table-hover shadow-sm">
            <thead class="table-dark">
                <tr>
                    <th style="width:10%;">ID</th>
                    <th style="width:50%;">商品名</th>
                    <th style="width:20%;">在庫</th>
                    <th style="width:20%;">更新</th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($items as $item): ?>
                    <tr>
                        <td><?= h($item['id']) ?></td>
                        <td><?= h($item['name']) ?></td>
                        <td>
                            <input type="number" form="form-<?= $item['id'] ?>" name="stock"
                                value="<?= $item['stock'] ?>" min="0"
                                class="form-control form-control-sm" style="max-width: 80px;">
                        </td>
                        <td>
                            <form id="form-<?= $item['id'] ?>" action="update.php" method="post">
                                <input type="hidden" name="id" value="<?= $item['id'] ?>">
                                <button type="submit" class="btn btn-primary btn-sm w-100">更新</button>
                            </form>
                        </td>
                    </tr>
                <?php endforeach; ?>
            </tbody>
        </table>

        <!-- ページネーション -->
        <nav aria-label="ページネーション">
            <ul class="pagination justify-content-center">
                <?php for ($i = 1; $i <= $totalPages; $i++): ?>
                    <li class="page-item <?= $i === $page ? 'active' : '' ?>">
                        <a class="page-link" href="?page=<?= $i ?>"><?= $i ?></a>
                    </li>
                <?php endfor; ?>
            </ul>
        </nav>
    </div>
</body>

</html>

ステップ課題1:何件あるかを表示してみよう!

  • SELECT COUNT(*) FROM items の結果を $total に入れる
  • その件数を 画面に表示 してみる。
  • また全30件中 11〜20件目を表示しています と表示してみよう
解答例
<?php
$stmt = $pdo->query("SELECT COUNT(*) FROM items");
$total = $stmt->fetchColumn();

echo "現在、商品は {$total} 件あります。";
$start = $offset + 1;
$end = min($offset + $perPage, $total);

echo "全 {$total} 件中 {$start}〜{$end} 件を表示中";