Simple Shopping Cart In PHP MySQL (FREE Download)

Welcome to a tutorial on how to create a simple shopping cart with PHP and MySQL. Yes, there are a ton of shopping cart scripts in the world, but they are also bloated with features that people rarely even use. So let us walk through a basic shopping cart, without using any third-party frameworks – Read on!

 

 

TABLE OF CONTENTS

 

DOWNLOAD & NOTES

Here is the download link to the example code, so you don’t have to copy-paste everything.

 

EXAMPLE CODE DOWNLOAD

Source code on GitHub Gist

Just click on “download zip” or do a git clone. I have released it under the MIT license, so feel free to build on top of it or use it in your own project.

 

SORRY FOR THE ADS...

But someone has to pay the bills, and sponsors are paying for it. I insist on not turning Code Boxx into a "paid scripts" business, and I don't "block people with Adblock". Every little bit of support helps.

Buy Me A Coffee Code Boxx eBooks

 

PHP MYSQL SHOPPING CART

All right, let us now get started with building a shopping cart.

 

 

 

PART 1) THE DATABASE

1A) PRODUCTS TABLE

1-database.sql
-- (A) PRODUCTS TABLE
CREATE TABLE `products` (
  `pid` bigint(20) NOT NULL,
  `name` varchar(255) NOT NULL,
  `image` varchar(255) NOT NULL,
  `price` decimal(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

ALTER TABLE `products`
  ADD PRIMARY KEY (`pid`);

ALTER TABLE `products`
  MODIFY `pid` bigint(20) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;

Let us start by addressing the elephant in the room, we need a table to store the products.

  • pid Product ID, primary key.
  • name Product name.
  • image Product image.
  • price Product price.

 

1B) ORDERS TABLE

1-database.sql
-- (B) ORDERS TABLE
CREATE TABLE `orders` (
  `oid` bigint(20) NOT NULL,
  `date` datetime NOT NULL DEFAULT current_timestamp(),
  `name` varchar(255) NOT NULL,
  `email` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
ALTER TABLE `orders`
  ADD PRIMARY KEY (`oid`),
  ADD KEY `name` (`name`),
  ADD KEY `email` (`email`),
  ADD KEY `date` (`date`);
 
ALTER TABLE `orders`
  MODIFY `oid` bigint(20) NOT NULL AUTO_INCREMENT;

Then, a table to hold the order details – The customer’s name, email, order date, and whatever is required.

  • oid Order ID, primary key.
  • date Date of purchase.
  • name Customer’s name.
  • email Customer’s email.

 

 

1C) ORDER ITEMS TABLE

1-database.sql
-- (C) ORDERS ITEMS TABLE
CREATE TABLE `orders_items` (
  `oid` bigint(20) NOT NULL,
  `name` varchar(255) NOT NULL,
  `price` decimal(10,2) NOT NULL,
  `qty` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
ALTER TABLE `orders_items`
  ADD PRIMARY KEY (`oid`,`name`);

Lastly, another table to hold all the order items.

  • oid Order ID, primary and foreign key.
  • name Product name, primary and foreign key.
  • price Product price (per item).
  • qty Quantity.

Some of you sharp code ninjas may be wondering – Why not just use the product ID as the foreign key? Isn’t the name and price redundant? Consider what happens when a product is updated or deleted in the future… Yep, keeping the product name and price at the time of checkout is “data integrity”, not “redundant”.

 

PART 2) SHOPPING CART LIBRARY

2-lib-cart.php
<?php
class Cart {
  // (A) CONSTRUCTOR - CONNECT TO DATABASE
  private $pdo = null;
  private $stmt = null;
  public $lastID = null;
  public $error = "";
  function __construct() {
    $this->pdo = new PDO(
      "mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=".DB_CHARSET,
      DB_USER, DB_PASSWORD, [
      PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
      PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
    ]);
  }
 
  // (B) DESTRUCTOR - CLOSE DATABASE CONNECTION
  function __destruct() {
    if ($this->stmt !== null) { $this->stmt = null; }
    if ($this->pdo !== null) { $this->pdo = null; }
  }
 
  // (C) HELPER - EXECUTE SQL QUERY
  function exec ($sql, $data=null) : void {
    $this->stmt = $this->pdo->prepare($sql);
    $this->stmt->execute($data);
  }
 
  // (D) GET PRODUCTS
  function getProducts ($cart=false) {
    // (D1) GET CART ITEMS - EMPTY
    if ($cart && $_SESSION["cartC"]==0) { return null; }
 
    // (D2) GET ALL PRODUCTS OR ITEMS IN CART?
    $sql = "SELECT * FROM `products`";
    $data = null;
    if ($cart) {
      $sql .= " WHERE `pid` IN (";
      $sql .= str_repeat("?,", count($_SESSION["cartI"]) - 1) . "?";
      $sql .= ")";
      $data = array_keys($_SESSION["cartI"]);
    }
 
    // (D3) FETCH PRODUCTS
    $this->exec($sql, $data);
    unset($sql); $data = [];
    while ($r = $this->stmt->fetch()) { $data[$r["pid"]] = $r; }
 
    // (D4) REMOVE ILLEGAL CART ITEMS
    if ($cart) { foreach ($_SESSION["cartI"] as $id=>$qty) {
      if (isset($data[$id])) { $data[$id]["qty"] = $qty; }
      else {
        $_SESSION["cartC"] -= $_SESSION["cartI"][$id];
        unset($_SESSION["cartI"][$id]);
      }
    }}
 
    // (D5) DONE
    return count($data)!=0 ? $data : null ;
  }
 
  // (E) CHECKOUT
  function checkout ($name, $email) {
    // (E1) GET ITEMS + CHECK
    $items = $this->getProducts(true);
    if ($_SESSION["cartC"] == 0) {
      $this->error = "Cart is empty";
      return false;
    }
 
     // (E2) AUTO-COMMIT OFF
    $this->pdo->beginTransaction();
 
    // (E3) MAIN ORDER
    $this->exec("INSERT INTO `orders` (`name`, `email`) VALUES (?,?)", [$name, $email]);
    $oid = $this->pdo->lastInsertId();
 
    // (E4) ORDER ITEMS
    $sql = "INSERT INTO `orders_items` (`oid`, `name`, `price`, `qty`) VALUES ";
    $sql .= str_repeat("(?,?,?,?),", count($_SESSION["cartI"]));
    $sql = substr($sql, 0, -1) . ";";
    $data = [];
    foreach ($_SESSION["cartI"] as $pid=>$qty) {
      array_push($data, $oid, $items[$pid]["name"], $items[$pid]["price"], $qty);
    }
    $this->exec($sql, $data);
 
    // (E5) COMMIT + EMPTY CART
    $this->pdo->commit();
    $_SESSION["cartI"] = [];
    $_SESSION["cartC"] = 0;
    return true;
  }
}
 
// (F) DATABASE SETTINGS - CHANGE TO YOUR OWN!
define("DB_HOST", "localhost");
define("DB_NAME", "test");
define("DB_CHARSET", "utf8mb4");
define("DB_USER", "root");
define("DB_PASSWORD", "");
 
// (G) NEW CART OBJECT
$_CART = new Cart();

With the database in place, we can now build a library to work with it. This looks intimidating, but keep calm and look closely.

  • (A, B, G) When $_CART = new Cart() is created, the constructor connects to the database. The destructor closes the connection.
  • (C) exec() A helper function to run an SQL query.
  • (D, E) There are only 2 “actual shopping cart” functions.
    • getProducts() Get all products from the database, or the current cart items.
    • checkout() Self-explanatory. Checkout the current cart.
  • (F) Remember to change the database settings to your own.

 

 

PART 3) CART AJAX HANDLER

3-ajax-cart.php
<?php
if (isset($_POST["req"])) {
  // (A) INIT SHOPPING CART
  session_start();
  if (!isset($_SESSION["cartI"])) {
    $_SESSION["cartI"] = []; // cart items
    $_SESSION["cartC"] = 0; // total quantity
  }
 
  // (B) UPDATE CART COUNT
  function ccount () {
    $_SESSION["cartC"] = 0;
    if (count($_SESSION["cartI"])!=0) {
      foreach ($_SESSION["cartI"] as $id=>$qty) { $_SESSION["cartC"] += $qty; }
    }
  }
 
  // (C) STANDARD SYSTEM RESPONSE
  function respond ($status=1, $msg="OK") {
    echo json_encode(["status"=>$status, "msg"=>$msg, "count" => $_SESSION["cartC"]]);
  }
 
  // (D) CART ACTIONS
  switch ($_POST["req"]) {
    // (D1) GET COUNT
    case "count": respond(); break;
 
    // (D2) ADD / CHANGE QUANTITY / REMOVE
    // send id only to add item
    // send id and qty to set quantity
    // send id and 0 qty to remove item
    case "set":
      $max = 99; // max allowed quantity per item
      $item = &$_SESSION["cartI"][$_POST["id"]];
      if (isset($_POST["qty"])) { $item = $_POST["qty"]; }
      else { if (isset($item)) { $item++; } else { $item = 1; } }
      if ($item<=0) { unset($_SESSION["cartI"][$_POST["id"]]); }
      if ($item > $max) { $item = $max; }
      ccount(); respond(); break;
 
    // (D3) NUKE
    case "nuke":
      $_SESSION["cartI"] = [];
      $_SESSION["cartC"] = 0;
      respond(); break;
 
    // (D4) GET ALL ITEMS IN CART
    case "get":
      // (D4-1) CART IS EMPTY
      if ($_SESSION["cartC"]==0) { respond(1, null); break; }
 
      // (D4-2) GET ITEMS IN CART
      require "2-lib-cart.php";
      respond(1, $_CART->getProducts(true)); break;
 
    // (D5) CHECKOUT
    case "checkout":
      require "2-lib-cart.php";
      if ($_CART->checkout($_POST["name"], $_POST["email"])) { respond(); }
      else { respond(0, $_CART->error); }
      break;
  }
}

Next, we have another piece of system foundation that drives the shopping cart. How it works is actually dreadfully simple:

  • (A) We keep the shopping cart in the PHP session.
    • $_SESSION["cartI"] contains the cart items, in the format of PRODUCT ID => QUANTITY.
    • $_SESSION["cartC"] contains the total quantity.
  • (D) We send $_POST["req"] and the required parameters to perform an action. For example, $_POST = ["req"=>"add", "pid"=>123] will add product ID 123 to the cart.
Request Description & Parameters
count Get the total quantity in the cart.
set
  • id only – Add item to the cart.
  • id and qty – Set item quantity in the cart.
  • id and qty=0 – Remove item from the cart.
nuke Empty the entire cart.
get Get all items in the cart.
checkout Checkout the current cart.

  • name Customer’s name.
  • email Customer’s email.

 

 

PART 4) SHOPPING PAGE

4A) HTML SHOPPING PAGE

4a-shop.php
<!-- (A) PRODUCTS + SHOPPING CART -->
<div id="wrap">
  <!-- (A1) HEADER -->
  <div id="head">
    <div id="iCart" onclick="cart.show()">
      My Cart <span id="cCart">0</span>
    </div>
  </div>
 
  <!-- (A2) PRODUCTS -->
  <div id="products"><?php
    require "2-lib-cart.php";
    $products = $_CART->getProducts();
    foreach ($products as $i=>$p) { ?>
    <div class="pCell">
      <img class="pImg" src="<?=$p["image"]?>">
      <div class="pName"><?=$p["name"]?></div>
      <div class="pPrice">$<?=$p["price"]?></div>
      <input class="pAdd button" type="button" value="Add To Cart" onclick="cart.add(<?=$i?>)">
    </div>
    <?php } ?>
  </div>
 
  <!-- (A3) CART ITEMS -->
  <div id="wCart">
    <span id="wCartClose" class="button" onclick="cart.toggle(cart.hWCart, false)">&#8678;</span>
    <h2>SHOPPING CART</h2>
    <div id="cart"></div>
  </div>
</div>
 
<!-- (B) CHECKOUT FORM -->
<div id="checkout"><form onsubmit="return cart.checkout()">
  <div id="coClose" class="button" onclick="cart.toggle(cart.hCO, false)">X</div>
  <label>Name</label>
  <input type="text" id="coName" required value="Jon Doe">
  <label>Email</label>
  <input type="email" id="coEmail" required value="jon@doe.com">
  <input class="button" type="submit" value="Checkout">
</form></div>

The system foundations are complete, and all we need is to build the shopping page. The essential parts are:

  • (A2) <div id="products"> We use the cart library to get all the products and draw the HTML here.
  • (A3) <div id="cart"> The shopping cart items.
  • (B) <div id="checkout"> The checkout form.

 

4B) SHOPPING CART JAVASCRIPT

4b-cart.js
var cart = {
  // (A) HELPER - AJAX FETCH
  ajax : (data, after) => {
    // (A1) FORM DATA
    let form = new FormData();
    for (let [k, v] of Object.entries(data)) { form.append(k, v); }
 
    // (A2) FETCH
    fetch("3-ajax-cart.php", { method:"POST", body:form })
    .then(res => res.json())
    .then(res => {
      if (res.status==1) { after(res); }
      else { alert(res.msg); }
    })
    .catch(err => console.error(err));
  },
 
  // (B) HELPER - TOGGLE HTML SECTIONS
  toggle : (target, show) => {
    if (show) { target.className = "show"; }
    else { target.className = ""; }
  },
 
  // (C) INITIALIZE
  hCart : null, // html cart
  hWCart : null, // html cart wrapper
  hCCart : null, // html cart count
  hCO : null, // html checkout wrapper
  init : () => {
    // (C1) GET HTML ELEMENTS
    cart.hCart = document.getElementById("cart");
    cart.hWCart = document.getElementById("wCart");
    cart.hCCart = document.getElementById("cCart");
    cart.hCO = document.getElementById("checkout");
 
    // (C2) UPDATE CART COUNT
    cart.ajax({ req : "count" }, res => cart.count(res.count));
  },
 
  // (D) UPDATE CART COUNT
  count : qty => {
    cart.hCCart.innerHTML = qty;
    if (cart.hWCart.classList.contains("show")) { cart.show(); }
  },
 
  // (E) SHOW CART ITEMS
  show : () => {
    // (E1) RESET
    cart.toggle(cart.hWCart, true);
    cart.hCart.innerHTML = "";
 
    // (E2) LOAD & GENERATE HTML
    cart.ajax({ req : "get" }, items => {
      // (E2-1) ITEMS IN CART
      items = items.msg;
 
      // (E2-2) CART IS EMPTY
      if (items==null) {
        cart.hCart.innerHTML = `<div class="cCell empty">Cart is empty</div>`;
      }
 
      // (E2-3) DRAW CART ITEMS
      else {
        let row, subtotal, total = 0;
        for (let [id, item] of Object.entries(items)) {
          // CALCULATE SUBTOTAL
          subtotal = item["price"] * item["qty"];
          total += subtotal;
 
          // ITEM ROW
          row = document.createElement("div");
          row.className = "cCell";
          row.innerHTML = `<input class="cQty" type="number" value="${item["qty"]}" 
          min="0" max="99" onchange="cart.change(${id}, this.value)">
          <div class="cInfo">
            <div class="cName">${item["name"]}</div>
            <div class="cPrice">$${subtotal.toFixed(2)}</div>
          </div>
          <input class="cDel button" type="button" value="X" onclick="cart.remove(${id})">`;
          cart.hCart.appendChild(row);
        }
 
        // CART TOTALS
        row = document.createElement("div");
        row.className = "cCell";
        row.innerHTML = `<input class="cDel button" type="button" value="X" onclick="cart.empty()">
        <div class="cInfo">
          <div class="cName">Total</div>
          <div class="cPrice">$${total.toFixed(2)}</div>
        </div>
        <input class="cDel button" type="button" value="&gt;" onclick="cart.toggle(cart.hCO, true)">`;
        cart.hCart.appendChild(row);
      }
    });
  },
 
  // (F) ADD ITEM TO CART
  add : id => cart.ajax(
    { req : "set", id : id },
    res => cart.count(res.count)
  ),
 
  // (G) CHANGE ITEM QTY
  change : (id, qty) => cart.ajax(
    { req : "set", id : id, qty : qty },
    res => cart.count(res.count)
  ),
 
  // (H) REMOVE ITEM
  remove : id => cart.ajax(
    { req : "set", id : id, qty : 0 },
    res => cart.count(res.count)
  ),
 
  // (I) NUKE
  empty : () => { if (confirm("Reset cart?")) {
    cart.ajax(
      { req : "nuke" },
      res => cart.count(res.count)
    ); 
  }},
 
  // (J) CHECKOUT
  checkout : () => {
    cart.ajax({
      req : "checkout",
      name : document.getElementById("coName").value,
      email : document.getElementById("coEmail").value
    }, res => location.href = "4c-thank-you.html"); 
    return false;
  }
};
window.onload = cart.init;

Once again, not going to explain line by line. Here’s a quick summary.

  • (A) ajax() A helper function to do a fetch call to 3-ajax-cart.php.
  • (B) toggle() Toggle show/hide for the HTML cart and checkout form.
  • (C) init() Runs on window load. Get the HTML elements and load the cart count.
  • (D To J) The shopping cart actions.
    • count() Update the total cart quantity.
    • show() Show the cart items.
    • add() Add an item to the shopping cart.
    • change() Change item quantity.
    • remove() Remove an item from the cart.
    • empty() Empty the entire cart.
    • checkout() Checkout.

 

 

4C) THANK YOU PAGE

4c-thank-you.html
<h1>Thank You</h1>
<p>Order received.</p>

Lastly, just a dummy thank you page after successful checkout.

 

EXTRAS

That’s all for this project, and here is a small section on some extras that may be useful to you.

 

NOTES & IMPROVEMENT IDEAS

My developer senses are tingling, I can feel the trolls banging on their keyboards. Of course, this is only a tutorial and a barebones shopping cart. A lot needs to be done before it is a “complete shopping cart system”:

  • If you don’t already have an existing user system – I will leave some links below.
  • Complete your own checkout sequence – Collect more customer information, send an email, etc…
  • Integrate your own payment gateway – Paypal, Stripe, Venmo, Payoneer Apple Pay, Google Pay, etc…
  • Build your own admin panel to better manage things.
  • Reports.
  • For you guys who are thinking of “customize product” and “product options” – See this ERD diagram, and you will know how complex it is. I cannot answer that in a few lines, get my eBook instead. 😛

Yep, you are building your own system here – It is up to you to complete the rest. Good luck.

 

A WAY BETTER SHOPPING CART

A shopping cart with product options, categories, discount coupons, user system, admin panel, API, and an installable web app - Click here to check out my eBook!

 

LINKS & REFERENCES

 

THE END

Thank you for reading, and we have come to the end. I hope that it has helped you to better understand, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!

61 thoughts on “Simple Shopping Cart In PHP MySQL (FREE Download)”

  1. how can i store data login session into TABLE ORDER :
    1. let say i login from system , and use this : $_SESSION[‘username’] = $_SESSION[‘username’];
    2. use the shopping cart
    3. when checkout, how can i save the username data into TABLE ORDER instead of input data manual at form

    1. Just add a new username field to the orders table, update the PHP and JS accordingly… As written in the “Adding more fields” section.

    1. Sorry, don’t quite catch your question. But I am going to assume that you want to add user ID into the database table, then get all items ordered by a certain user.

      1) Add a user_id field to the orders table.
      2) “SELECT oi.* FROM `orders_items` oi LEFT JOIN `orders` o USING (`order_id`) WHERE o.`user_id`=?” will do the trick… Doing this from memory, hope it is correct.

  2. After adding one more field for phone number, I’ve adjusted the Ajax script and Java and the library script, the check out button no longer respond. Please how do I get this done?

    1. I was finally able to fix it. Thanks man for the great job and finding time to reply to people

  3. Thanks for your great work
    am stuck on add some fields to the orders
    for example
    i need to add size of the item S/M/L
    1. i add the database field to capture the insertion
    2. AJAX handler am stuck with it where and how i will add this field
    3 and 4 also stuck 🙁

    1. Hi Ahmed. Just take it easy, deal with each file step-by-step.
      1) Database: Already done.
      2) System files: No changes.
      3A) PHP Products Library: Update all the SQL to include the size.
      3B&C) Products page: Some HTML & CSS structure changes to include the choice of size.
      4a) Javascript: add() now needs to fetch the selected size as well. change() and remove() to be updated as well.
      4b) AJAX: You need to change the way an item is being added to the cart session. Maybe instead of $_SESSION[‘cart’][‘ID’], change it to $_SESSION[‘cart’][‘ID-SIZE’] or $_SESSION[‘cart’][‘ID’][‘SIZE’]… But that will also mean changing all the other requests – count, show, change, checkout.
      4c) PHP Cart Library: Yep… All functions need to be update to reflect the size as well.

      Lazy way) If there are not too many different sizes and products, just create multiple products. E.g. Small T-Shirt, Medium T-Shirt, Large T-Shirt.

      Good luck with your project.

  4. Timothy Githinji

    Hi i want to select details of the table products based on submitted input from another page. How do i do this. Thank you

    1. 1) Simply add a new search function to the products library.
      return $this->fetch("SELECT * FROM `products` WHERE `product_name` LIKE ?", ["%$search%"], "product_id");

      2) Use that to fetch and draw the search results in your HTML page.
      $results = $productLib->search($_POST['search']);

    2. Mr. W.S. TOH
      please help me in this script…search product not work properly according to above script that you mentioned.

Leave a Comment

Your email address will not be published. Required fields are marked *