Unit 13 ยท E-commerce and Web-Related Components

13.2Shopping Cart

A cart holds the products a customer intends to buy. Guests need a cart too, so we store it in the session as product_id => quantity.

includes/cart.php (helper functions):

PHP
<?php
function cart_add(int $id, int $qty = 1): void {
    if ($id < 1 || $qty < 1) return;              // ignore bad ids and quantities
    $_SESSION['cart'][$id] = ($_SESSION['cart'][$id] ?? 0) + $qty;
}
function cart_update(int $id, int $qty): void {
    if ($qty <= 0) { unset($_SESSION['cart'][$id]); }
    else { $_SESSION['cart'][$id] = $qty; }
}
function cart_remove(int $id): void { unset($_SESSION['cart'][$id]); }
function cart_count(): int { return array_sum($_SESSION['cart'] ?? []); }

// Build full cart details from the DATABASE (never trust browser prices)
function cart_items(PDO $pdo): array {
    $cart = $_SESSION['cart'] ?? [];
    if (!$cart) return ['items' => [], 'total' => 0];

    $ids = array_keys($cart);
    $marks = implode(',', array_fill(0, count($ids), '?'));
    $stmt = $pdo->prepare('SELECT id, name, price, stock, image FROM products WHERE id IN (' . $marks . ')');
    $stmt->execute($ids);

    $items = []; $total = 0;
    foreach ($stmt->fetchAll() as $p) {
        $qty = min($cart[$p['id']], $p['stock']);        // cannot exceed stock
        if ($qty < 1) continue;                          // out of stock: leave it out
        $p['qty'] = $qty;
        $p['line'] = $qty * $p['price'];
        $total += $p['line'];
        $items[] = $p;
    }
    return ['items' => $items, 'total' => $total];
}

add_to_cart.php (a normal form POST version):

PHP
<?php
session_start();
require 'includes/cart.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    cart_add((int)($_POST['product_id'] ?? 0), (int)($_POST['qty'] ?? 1));
}
header('Location: cart.php');
exit;

api/cart_add.php (AJAX version returning JSON so the cart badge updates without reload):

PHP
<?php
session_start();
require '../includes/cart.php';
header('Content-Type: application/json');

$id  = (int)($_POST['product_id'] ?? 0);
$qty = max(1, (int)($_POST['qty'] ?? 1));
cart_add($id, $qty);
echo json_encode(['success' => true, 'count' => cart_count()]);
JavaScript
document.querySelectorAll('.add-btn').forEach(btn => {
  btn.addEventListener('click', async () => {
    const res = await fetch('api/cart_add.php', {
      method: 'POST',
      body: new URLSearchParams({ product_id: btn.dataset.id, qty: 1 })
    });
    const data = await res.json();
    document.getElementById('cartBadge').textContent = data.count;
  });
});

(The button is <button class='add-btn' data-id='5'>Add to cart</button>.)

cart.php shows the table with update and remove forms:

PHP
<?php
session_start();
require 'config/db.php';
require 'includes/cart.php';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (isset($_POST['remove'])) cart_remove((int)$_POST['remove']);
    if (isset($_POST['qty']))    foreach ($_POST['qty'] as $id => $q) cart_update((int)$id, (int)$q);
    header('Location: cart.php'); exit;
}
$cart = cart_items($pdo);
?>
<table class='table'>
  <tr><th>Product</th><th>Price</th><th>Qty</th><th>Subtotal</th><th></th></tr>
  <?php foreach ($cart['items'] as $it): ?>
  <tr>
    <td><?= htmlspecialchars($it['name']) ?></td>
    <td><?= number_format($it['price'], 2) ?></td>
    <td><form method='post'>
          <input type='number' min='0' max='<?= $it['stock'] ?>' name='qty[<?= $it['id'] ?>]' value='<?= $it['qty'] ?>'>
          <button>Update</button></form></td>
    <td><?= number_format($it['line'], 2) ?></td>
    <td><form method='post'><button name='remove' value='<?= $it['id'] ?>'>Remove</button></form></td>
  </tr>
  <?php endforeach; ?>
  <tr><td colspan='3'><strong>Total</strong></td><td colspan='2'><?= number_format($cart['total'], 2) ?></td></tr>
</table>
<a href='checkout.php' class='btn btn-primary'>Checkout</a>

Checkout: save the order safely with a transaction (checkout.php, POST handler):

PHP
<?php
session_start();
require 'config/db.php'; require 'includes/cart.php';
if (!isset($_SESSION['user_id'])) { header('Location: login.php?next=checkout.php'); exit; }

$cart = cart_items($pdo);
if (!$cart['items']) { header('Location: cart.php'); exit; }

try {
    $pdo->beginTransaction();

    $pdo->prepare('INSERT INTO orders (user_id, total) VALUES (?, ?)')
        ->execute([$_SESSION['user_id'], $cart['total']]);
    $orderId = $pdo->lastInsertId();

    $item  = $pdo->prepare('INSERT INTO order_items (order_id, product_id, qty, unit_price) VALUES (?, ?, ?, ?)');
    $stock = $pdo->prepare('UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?');

    foreach ($cart['items'] as $it) {
        $stock->execute([$it['qty'], $it['id'], $it['qty']]);
        if ($stock->rowCount() === 0) throw new Exception('Out of stock: ' . $it['name']);
        $item->execute([$orderId, $it['id'], $it['qty'], $it['price']]);
    }
    $pdo->commit();
    unset($_SESSION['cart']);
    header('Location: order_success.php?id=' . $orderId); exit;
} catch (Exception $e) {
    $pdo->rollBack();
    $_SESSION['flash'] = $e->getMessage();
    header('Location: cart.php'); exit;
}

Optional enhancement: logged-in users can have the cart saved in a cart_items database table so it follows them across devices; merge the session cart on login.