学習目標
- データベースに登録された在庫データを一覧表示する
- ページネーション(ページ分割) を実装して、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)
- SELECT文で、itemsデータを取得。HTMLのテーブルで一覧表示
- 各行にフォームを設置 →
update.phpに送信 - 更新後は
?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文のLIMIT と OFFSET を使って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} 件を表示中";
