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()` and `removeFromCart()` 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 `$_SESSION` across multiple page loads.
- Arrays — associative and nested arrays, plus functions like `array_sum()` and `isset()`.
- Functions — parameters, return values, and calling one small function from another.
- Forms and superglobals — reading `$_POST`/`$_GET` to know which product and quantity a request refers to.
- Loops — `foreach` for 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.