PHPで作る在庫と連動するショッピングカート

学習目標

  • データベースの商品データを一覧表示する
  • 在庫数が0の商品は購入できないようにする
  • カートに入れると在庫数が減る(UPDATE文)
  • セッションを使ってユーザーごとのカートを保持する
  • カート画面で商品名・数量・価格・合計金額を表示する

完成イメージ

同一商品のカートに入れるボタンをクリックすると在庫が減って、カートの中身が増える 小計 合計が変わる

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

php-cart/
├── common.php        # DB接続・セッション開始・エスケープ関数
├── index.php         # 商品一覧(購入ボタン付き)
├── add_to_cart.php   # 在庫減算とカート追加処理(PRGパターン)
├── cart.php          # カート表示
└── seed.php          # 初期データ投入(果物・野菜20件)

データベース準備

データベース:testdbに以下のSQLでテーブルを作成します。INSERT INTOでカートデータも挿入しておきます。

CREATE TABLE products (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    price INT NOT NULL,
    stock INT DEFAULT 0
);
INSERT INTO products (name, price, stock) VALUES
('りんご', 120, 5),
('みかん', 90, 8),
('ぶどう', 300, 3),
('いちご', 400, 6),
('バナナ', 150, 10),
('もも', 350, 2),
('スイカ', 1200, 1),
('パイナップル', 800, 2),
('メロン', 1500, 1),
('キウイ', 180, 4),
('にんじん', 90, 8),
('じゃがいも', 70, 10),
('たまねぎ', 80, 9),
('牛乳(1L)', 200, 6),
('オレンジジュース(1L)', 250, 5);

共通ファイル(common.php)

  • PDOで接続を作り、全ページで使える h() 関数を定義。
  • $_SESSIONを使うので、session_start()を忘れないように
<?php
session_start();//忘れないように

// DB接続
$pdo = new PDO(
    'mysql:host=localhost;dbname=testdb;charset=utf8',
    'root',
    '',
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);

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

商品一覧(index.php)

  • LIMIT / OFFSET でページ分割
  • 在庫が0ならボタンを無効化(売り切れと表示)
<?php
require 'common.php';

// 商品を全部取得
$stmt = $pdo->query("SELECT * FROM products ORDER BY id");
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>商品一覧</title>
    <link rel="stylesheet" href="style.css">
</head>
<body class="container">
    <h1>商品一覧</h1>

    <?php if (isset($_GET['error'])): ?>
        <div class="alert alert-danger">在庫切れです</div>
    <?php endif; ?>

    <table class="table">
            <tr>
                <th>ID</th><th>商品名</th><th>価格</th><th>在庫</th><th></th>
            </tr>
        <tbody>
        <?php foreach ($items as $item): ?>
            <tr>
                <td><?= h($item['id']) ?></td>
                <td><?= h($item['name']) ?></td>
                <td><?= h($item['price']) ?>円</td>
                <td><?= h($item['stock']) ?></td>
                <td>
                    <?php if ($item['stock'] > 0): ?>
                        <form action="add_to_cart.php" method="post">
                            <input type="hidden" name="id" value="<?= $item['id'] ?>">
                            <button type="submit" class="btn">カートに入れる</button>
                        </form>
                    <?php else: ?>
                        <button class="btn" disabled>売り切れ</button>
                    <?php endif; ?>
                </td>
            </tr>
        <?php endforeach; ?>
        </tbody>
    </table>

    <a href="cart.php" class="btn">カートを見る</a>
</body>
</html>

在庫減算&カート追加(add_to_cart.php)

  1. 商品IDを受け取る
  2. 在庫を1減らす(在庫が残っている場合だけ)
  3. 更新できなければエラー画面へ
  4. カート配列に商品を追加(数量を増やす)
  5. cart.php に移動
<?php
require 'common.php';

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

$stmt = $pdo->prepare("UPDATE products SET stock = stock - 1 WHERE id = ? AND stock > 0");
$stmt->execute([$id]);

if ($stmt->rowCount() === 0) {
    // 在庫切れなら index.php に戻す
    header("Location: index.php?error=1");
    exit;
}

// セッションに追加
$_SESSION['cart'][$id] = ($_SESSION['cart'][$id] ?? 0) + 1;

header("Location: cart.php");
exit;

1. 共通ファイルの読み込み

require 'common.php';
  • DB接続セッション開始をまとめた common.php を読み込みます。
  • これで $pdo(データベース接続オブジェクト)や $_SESSION が使えるようになります。

2. 商品IDを受け取る

$id = (int)($_POST['id'] ?? 0);
  • index.php の「カートに入れる」ボタンから送られてきた id を受け取ります。
  • (int) で数値に変換 → 不正な入力を防ぐ(例:文字列が入っても0になる)。

3. 在庫を1減らす

$stmt = $pdo->prepare("UPDATE products SET stock = stock - 1 WHERE id = ? AND stock > 0");
$stmt->execute([$id]);
  • stock = stock - 1 で在庫を1つ減らします。
  • AND stock > 0 を条件にして「在庫が残っているときだけ」更新するようにしています。
  • これで「在庫切れなのに減ってしまう」ことを防ぎます。

4. 更新できたか確認

if ($stmt->rowCount() === 0) {
    header("Location: index.php?error=1");
    exit;
}
  • rowCount() は実際に更新できた件数を返します。
  • 0件だった場合 → 在庫がなかった、または商品IDが不正だった。
  • そのときは index.php?error=1 にリダイレクトしてエラーメッセージを表示します。

5. カートに追加

$_SESSION['cart'][$id] = ($_SESSION['cart'][$id] ?? 0) + 1;
  • $_SESSION['cart'] に「商品ID → 数量」の形で保存します。
  • すでに入っている商品なら数量を+1、なければ0からスタートして+1。
  • 例:
    • 初めて追加 → [3 => 1]
    • もう1回追加 → [3 => 2]

カート一覧(cart.php)

  • セッションからIDを取得してDBからまとめてSELECT
  • 小計と合計を計算して表示
  • 配列 → 加工(キー取り出し、型変換)→ 文字列化 → 再び配列に変換して使いやすく整形
<?php
require 'common.php';

$cart = $_SESSION['cart'] ?? [];
$total = 0;

// カートが空なら終了
$ids = array_map('intval', array_keys($cart));
if (empty($ids)) {
    echo "<p>カートは空です</p><p><a href='index.php'>商品一覧へ</a></p>";
    exit;
}

// 商品データをまとめて取得(安全版)
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM products WHERE id IN ($placeholders)");
$stmt->execute($ids);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);

// id をキーにした連想配列に整形
$itemsById = array_column($items, null, 'id');

?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>カート</title>
    <link rel="stylesheet" href="style.css">
</head>
<body class="container py-4">
    <h1>カートの中身</h1>

    <table class="table">
        <tr>
            <th>商品名</th><th>数量</th><th>単価</th><th>小計</th>
        </tr>

        <?php foreach ($cart as $id => $qty): ?>
            <?php if (!isset($itemsById[$id])) continue; ?>
            <?php 
                $p = $itemsById[$id];
                $subtotal = $p['price'] * $qty;
                $total += $subtotal;
            ?>
            <tr>
                <td><?= h($p['name']) ?></td>
                <td><?= $qty ?></td>
                <td><?= $p['price'] ?>円</td>
                <td><?= $subtotal ?>円</td>
            </tr>
        <?php endforeach; ?>
    </table>

    <p class="bold">合計:<?= $total ?>円</p>

    <a href="index.php" class="btn">商品一覧へ戻る</a>
</body>
</html>

1. セッションからカート情報を取り出す

$cart = $_SESSION['cart'] ?? [];
  • $_SESSION['cart'] に「カートの中身」が保存されています。
  • もしまだ何も入っていなければ空の配列 [] を使います。
  • 例:
    • 何も入っていない → []
    • りんごを2個、バナナを1個 → [1 => 2, 5 => 1]
      (ID1の商品を2個、ID5の商品を1個)

2. カートが空かどうかチェック

$ids = array_map('intval', array_keys($cart));
if (empty($ids)) {
    echo "<p>カートは空です</p><p><a href='index.php'>商品一覧へ</a></p>";
    exit;
}
  • array_keys($cart) で「商品ID」だけを取り出す。
    例: [1 => 2, 5 => 1][1, 5]
  • array_map('intval', ...) で文字列かもしれないIDを「整数」に変換。
  • empty($ids) でIDが1つもなければ「カートは空です」と表示して終了します。

3. 商品データをまとめて取得

$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM products WHERE id IN ($placeholders)");
$stmt->execute($ids);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
  • カートに入っているIDの一覧を使って、必要な商品のデータをまとめて取り出します。
  • WHERE id IN (...) を使うことで、
    例: [2, 5, 7]SELECT * FROM products WHERE id IN (2,5,7)
  • prepare? を使うことで安全にSQLを実行できます。

4. IDで取り出しやすい形に変換

$itemsById = array_column($items, null, 'id');
  • 取得した商品データを「IDをキーにした配列」に変換します。
  • こうすると $itemsById[2] のように「商品IDから商品情報を直接取り出す」ことができます。

5. カートの中身を表示する

foreach ($cart as $id => $qty) {
    $p = $itemsById[$id];
    $subtotal = $p['price'] * $qty;
    $total += $subtotal;
}
  • foreach ($cart as $id => $qty)
    → カートの中身を「商品ID」と「数量」で1つずつ取り出します。
  • $p = $itemsById[$id]
    → 商品IDから商品の名前や価格を取り出します。
  • $subtotal = $p['price'] * $qty
    → 単価 × 数量 で小計を計算します。
  • $total += $subtotal
    → 小計を合計金額に足し込んでいきます。

ステップアップ課題

カート一覧に「カートを空にする」ボタンをつけてみよう

ヒント:unset($_SESSION['cart']); でカート情報だけ削除(セッション全体は残す)しましょう。一番簡単なのは、reset.phpを作成し、unsetを使うことです。そのあと、cart.php の一番下にボタンを追加して、リンク先を reset.phpへ戻るように設定しておきます。

解答例
<?php
require 'common.php';

// カート内容を取得(セッションに保存されている id => 数量 の配列)
$cart = $_SESSION['cart'] ?? [];

// 在庫を戻す処理
if (!empty($cart)) {
    // UPDATE文を事前に準備(? がプレースホルダ)
    // stock = stock + ? で  カートにいれた個数だけ在庫を増やす
    // id = ? で「特定の商品だけ更新」する条件
    $stmt = $pdo->prepare("UPDATE products SET stock = stock + ? WHERE id = ?");

    // カートの中身を1つずつ取り出して処理
    foreach ($cart as $id => $qty) {
        // $qty → カートに入っていた個数
        // $id  → 商品ID
        // 例えば、商品ID=3を2個カートに入れていた場合
        //     stock = stock + 2 WHERE id = 3
        $stmt->execute([$qty, $id]);
    }
}

// カートを空にする(セッションから cart のみ削除)
unset($_SESSION['cart']);

// 商品一覧へリダイレクト
header("Location: index.php");
exit;
<p>合計:<?= $total ?>円</p>
<p><a href="index.php" class="btn">商品一覧へ戻る</a></p>

<a href="reset.php" class="btn btn-danger"
   onclick="return confirm('カートを空にしますか?');">カートを空にする</a>
style.css
/* 全体の基本設定 */
body {
    background-color: #f9f9f9;
    line-height: 1.6;
    color: #333;
}

/* 見出し */
h1 {
    font-size: 1.8rem;
    font-weight: bold;
    margin-bottom: 1.5rem;
    text-align: center;
}


.container{
    max-width: 700px;
    margin: auto;
}
/* テーブルのデザイン */
.table {
    background-color: #fff;
    border-radius: 6px;
    overflow: hidden;
    max-width: 100%;
}

.table th {
    background-color: #343a40;
    color: #fff;
    text-align: center;
    padding: 0.5rem 1rem;
}

.table td {
    vertical-align: middle;
    padding: 0.5rem 1rem;
    text-align: center;
}
.table td:nth-child(2){
    text-align: left;
    width: 300px;
}
.table td:nth-child(3){
    text-align: right;
}


/* カート合計の強調 */
.fw-bold {
    font-size: 1.2rem;
    color: #d9534f;
}

/* アラートの調整 */
.alert {
    max-width: 500px;
    margin: 1rem auto;
    text-align: center;
}

/* ページ全体のコンテナ調整 */
.container {
    max-width: 800px;
}

/* ボタンの共通デザイン(Bootstrapに追加上書き) */
.btn {
    border-radius: 20px;
    padding: 0.4rem 1.2rem;
    font-size: 0.9rem;
}