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.