Overview
A shopping cart has to survive something HTTP was never designed to do on its own: remember what a visitor picked while they keep browsing to completely different pages. That is exactly the problem $_SESSION solves — data written into it on one page load is still there on the next, for as long as that visitor's session cookie stays valid, with no database required at all. This project stores the cart as a plain associative array, $_SESSION['cart'], mapping a product id to a quantity, e.g. [101 => 2, 103 => 1].
Notice what is deliberately *not* stored in that array: a product's name or price. Only the id and quantity live in the session; the name and price are looked up from a catalog every time the cart is displayed. That is a small but important design decision — if the catalog price for a product ever changes, every cart in every session automatically reflects the new price the next time it is viewed, instead of quietly holding onto a stale price that was copied into the session at add-to-cart time. By the end of this tutorial you will have products.php (the catalog plus every cart function), index.php (a product listing with an add-to-cart button), and cart.php (a cart view with per-item quantity updates, removal, and a running total).
- A hard-coded product catalog as an associative array (
id => [name, price]). - A cart initialized once per session as
$_SESSION['cart'], an id-to-quantity map. - An
addToCart()function that increases the quantity if the product is already in the cart. updateQuantity()andremoveFromCart()functions used by the cart page.- A
cartTotal()function that loops the cart against the catalog to compute the grand total. - A product listing page and a cart page that both work purely off session state.
Prerequisites
- Sessions —
session_start()and reading/writing$_SESSIONacross multiple page loads. - Arrays — associative and nested arrays, plus functions like
array_sum()andisset(). - Functions — parameters, return values, and calling one small function from another.
- Forms and superglobals — reading
$_POST/$_GETto know which product and quantity a request refers to. - Loops —
foreachfor rendering cart rows and computing totals.
Project Structure
Every function that touches $_SESSION['cart'] lives in one file, products.php, included by both index.php and cart.php. Centralizing them like this means the exact shape of the cart array — an id mapped to a plain integer quantity — is really only an implementation detail of products.php; neither page needs to know or care about that shape directly, they only ever call addToCart(), updateQuantity(), removeFromCart(), or cartTotal(). index.php lists the catalog and handles adding an item; cart.php displays the current cart, with a small form per row for updating its quantity or removing it, plus the total.
Step 1: Define the Product Catalog
getProductCatalog() stands in for a products database table for the purposes of this tutorial — returning the array from a function rather than a bare global variable keeps it easy to swap for a real SELECT * FROM products query later without changing anything that calls it. Keying the array by product id, rather than using a plain numbered list, is what lets the cart store just an id and quantity and look everything else up by that same id.
<?php// products.php — a hard-coded catalog standing in for a "products" database// table. Keyed by product id so the cart can store just an id and quantity,// and this array is where "what does that id actually mean" gets looked up.function getProductCatalog(): array { return [ 101 => ['name' => 'Wireless Mouse', 'price' => 799.00], 102 => ['name' => 'Mechanical Keyboard', 'price' => 3499.00], 103 => ['name' => 'USB-C Hub', 'price' => 1299.00], 104 => ['name' => 'Laptop Stand', 'price' => 1899.00], ];}Step 2: Initialize the Session Cart
initCart() guarantees $_SESSION['cart'] always exists as an array before any other cart function tries to read or write it — every page that touches the cart calls this first, so no later function needs to repeat the same isset() check. On a visitor's very first visit there is nothing in their session yet, so it is initialized to an empty array; on every later request in the same session, this check simply does nothing, and the cart from earlier is left untouched.
<?phpsession_start();
// Every page that touches the cart calls this first — it guarantees// $_SESSION['cart'] always exists as an array, so no later code needs to// repeat an isset() check before reading from or writing to it.function initCart(): void { if (!isset($_SESSION['cart'])) { $_SESSION['cart'] = []; // Empty id => quantity map for a brand-new visitor }}Step 3: Add and Remove Items
addToCart() checks whether the product id is already a key in the cart array before deciding what to do: if it is, the existing quantity is increased rather than overwritten, so clicking "Add to Cart" on the same product twice results in a quantity of 2, not two separate entries for the same id. removeFromCart() uses unset(), PHP's way of deleting one specific key from an array entirely, rather than setting its quantity to 0 and leaving a stray entry behind. Add both functions to products.php, alongside initCart().
function addToCart(int $productId, int $quantity = 1): void { if (isset($_SESSION['cart'][$productId])) { // Already in the cart: increase the existing quantity instead of overwriting it, // so clicking "Add to Cart" twice results in a quantity of 2, not a second entry. $_SESSION['cart'][$productId] += $quantity; } else { $_SESSION['cart'][$productId] = $quantity; }}
function removeFromCart(int $productId): void { unset($_SESSION['cart'][$productId]); // unset() on an array key is how PHP deletes one entry}Step 4: Update Quantities and Calculate Totals
updateQuantity() treats a quantity of zero or less as a request to remove the item entirely, by delegating straight to removeFromCart() from Step 3 rather than storing an invalid quantity and having every other function defend against it. cartTotal() is where the cart array and the catalog array meet: it loops the cart's id-to-quantity pairs, looks each id up in getProductCatalog(), and multiplies price by quantity — the isset($catalog[$productId]) check defends against a product id that is still sitting in an old session but no longer exists in the catalog at all.
function updateQuantity(int $productId, int $quantity): void { if ($quantity <= 0) { // A quantity of zero (or less) means "remove it" rather than storing an invalid value. removeFromCart($productId); return; } $_SESSION['cart'][$productId] = $quantity;}
function cartTotal(): float { $catalog = getProductCatalog(); $total = 0.0;
foreach ($_SESSION['cart'] as $productId => $quantity) { if (isset($catalog[$productId])) { // Defend against a stale id no longer in the catalog $total += $catalog[$productId]['price'] * $quantity; } }
return $total;}
function cartItemCount(): int { return array_sum($_SESSION['cart']); // Sum of every quantity, e.g. 2 mice + 1 keyboard = 3}Step 5: Build the Product and Cart Pages
Both pages redirect back to themselves with header('Location: ...') and exit immediately after handling a POST request, a pattern often called Post/Redirect/Get: it means refreshing the page after adding an item, updating a quantity, or removing a row re-runs a harmless GET request instead of silently resubmitting the same form data a second time. cart.php uses a hidden remove field on its own small form to tell the same page apart from a quantity update — isset($_POST['remove']) checks whether that specific button, rather than the "Update" button, was the one actually clicked.
<?phprequire_once 'products.php';initCart();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['product_id'])) { addToCart((int) $_POST['product_id']); header('Location: index.php'); // Redirect after POST so refreshing the page never re-adds the item exit;}
$catalog = getProductCatalog();?><!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Shop</title></head><body> <h1>Products</h1> <p><a href="cart.php">View Cart (<?= cartItemCount() ?>)</a></p>
<?php foreach ($catalog as $id => $product): ?> <div> <strong><?= htmlspecialchars($product['name']) ?></strong> — Rs. <?= number_format($product['price'], 2) ?> <form action="" method="POST" style="display:inline"> <input type="hidden" name="product_id" value="<?= $id ?>"> <button type="submit">Add to Cart</button> </form> </div> <?php endforeach; ?></body></html><?phprequire_once 'products.php';initCart();
if ($_SERVER['REQUEST_METHOD'] === 'POST') { $productId = (int) ($_POST['product_id'] ?? 0);
if (isset($_POST['remove'])) { removeFromCart($productId); } elseif (isset($_POST['quantity'])) { updateQuantity($productId, (int) $_POST['quantity']); }
header('Location: cart.php'); // Redirect after POST, same reasoning as index.php exit;}
$catalog = getProductCatalog();?><!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Your Cart</title></head><body> <h1>Your Cart</h1>
<?php if (empty($_SESSION['cart'])): ?> <p>Your cart is empty. <a href="index.php">Continue shopping</a>.</p> <?php else: ?> <?php foreach ($_SESSION['cart'] as $productId => $quantity): ?> <?php if (!isset($catalog[$productId])) { continue; } // Skip an id no longer in the catalog ?> <?php $product = $catalog[$productId]; ?> <div> <strong><?= htmlspecialchars($product['name']) ?></strong> — Rs. <?= number_format($product['price'], 2) ?> ×
<form action="" method="POST" style="display:inline"> <input type="hidden" name="product_id" value="<?= $productId ?>"> <input type="number" name="quantity" value="<?= $quantity ?>" min="1" style="width:3em"> <button type="submit">Update</button> </form>
<form action="" method="POST" style="display:inline"> <input type="hidden" name="product_id" value="<?= $productId ?>"> <button type="submit" name="remove" value="1">Remove</button> </form> </div> <?php endforeach; ?>
<h2>Total: Rs. <?= number_format(cartTotal(), 2) ?></h2> <?php endif; ?></body></html>Complete Code
Here is products.php with every function from Steps 1-4 assembled together, plus the two page scripts that use it.
<?phpsession_start();
function getProductCatalog(): array { return [ 101 => ['name' => 'Wireless Mouse', 'price' => 799.00], 102 => ['name' => 'Mechanical Keyboard', 'price' => 3499.00], 103 => ['name' => 'USB-C Hub', 'price' => 1299.00], 104 => ['name' => 'Laptop Stand', 'price' => 1899.00], ];}
function initCart(): void { if (!isset($_SESSION['cart'])) { $_SESSION['cart'] = []; }}
function addToCart(int $productId, int $quantity = 1): void { if (isset($_SESSION['cart'][$productId])) { $_SESSION['cart'][$productId] += $quantity; } else { $_SESSION['cart'][$productId] = $quantity; }}
function removeFromCart(int $productId): void { unset($_SESSION['cart'][$productId]);}
function updateQuantity(int $productId, int $quantity): void { if ($quantity <= 0) { removeFromCart($productId); return; } $_SESSION['cart'][$productId] = $quantity;}
function cartTotal(): float { $catalog = getProductCatalog(); $total = 0.0;
foreach ($_SESSION['cart'] as $productId => $quantity) { if (isset($catalog[$productId])) { $total += $catalog[$productId]['price'] * $quantity; } }
return $total;}
function cartItemCount(): int { return array_sum($_SESSION['cart']);}<?phprequire_once 'products.php';initCart();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['product_id'])) { addToCart((int) $_POST['product_id']); header('Location: index.php'); exit;}
$catalog = getProductCatalog();?><!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Shop</title></head><body> <h1>Products</h1> <p><a href="cart.php">View Cart (<?= cartItemCount() ?>)</a></p>
<?php foreach ($catalog as $id => $product): ?> <div> <strong><?= htmlspecialchars($product['name']) ?></strong> — Rs. <?= number_format($product['price'], 2) ?> <form action="" method="POST" style="display:inline"> <input type="hidden" name="product_id" value="<?= $id ?>"> <button type="submit">Add to Cart</button> </form> </div> <?php endforeach; ?></body></html><?phprequire_once 'products.php';initCart();
if ($_SERVER['REQUEST_METHOD'] === 'POST') { $productId = (int) ($_POST['product_id'] ?? 0);
if (isset($_POST['remove'])) { removeFromCart($productId); } elseif (isset($_POST['quantity'])) { updateQuantity($productId, (int) $_POST['quantity']); }
header('Location: cart.php'); exit;}
$catalog = getProductCatalog();?><!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Your Cart</title></head><body> <h1>Your Cart</h1>
<?php if (empty($_SESSION['cart'])): ?> <p>Your cart is empty. <a href="index.php">Continue shopping</a>.</p> <?php else: ?> <?php foreach ($_SESSION['cart'] as $productId => $quantity): ?> <?php if (!isset($catalog[$productId])) { continue; } ?> <?php $product = $catalog[$productId]; ?> <div> <strong><?= htmlspecialchars($product['name']) ?></strong> — Rs. <?= number_format($product['price'], 2) ?> ×
<form action="" method="POST" style="display:inline"> <input type="hidden" name="product_id" value="<?= $productId ?>"> <input type="number" name="quantity" value="<?= $quantity ?>" min="1" style="width:3em"> <button type="submit">Update</button> </form>
<form action="" method="POST" style="display:inline"> <input type="hidden" name="product_id" value="<?= $productId ?>"> <button type="submit" name="remove" value="1">Remove</button> </form> </div> <?php endforeach; ?>
<h2>Total: Rs. <?= number_format(cartTotal(), 2) ?></h2> <?php endif; ?></body></html>Sample Run
Click Run to see what this code prints.
Extend This Project
- Persist the cart to a database table for logged-in users (combine with the Login System project) so it survives across devices, not just one browser session.
- Add stock validation so a quantity can never exceed how many units are actually available.
- Add a coupon code field that applies a percentage or flat discount inside
cartTotal(). - Add a checkout page that turns the cart into an order record and clears
$_SESSION['cart']afterward. - Replace the full-page redirects with a small amount of
fetch()-based JavaScript so add/update/remove happen without a page reload.
Summary
You built a shopping cart that keeps exactly one thing in the session — an id-to-quantity map — and derives everything else, from a product's name and price to the grand total, by looking that id up in the catalog every time it is needed. That separation between "what the visitor chose" (session state) and "what that choice actually means" (catalog data) is what keeps a cart correct even as prices change, and it is the same pattern you would reach for whether the catalog behind it is a hard-coded array like this one or a full products table in MySQL.