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. I bought the ebook from the store, but I lacked the integration with paypal, would you be able to help describe where to change ajaxcart to close the sale after confirming paypal?

    1. Thanks for the purchase! But sorry, I cannot offer much help. As explained in the book itself, there are endless ways to do payment processing and everyone is different – Did you collect the payment details on your website? Redirect to Paypal? Buy now, subscription, or installments? As such, I can only give general advice.

      1) On checkout, set the order status to “pending payment”.
      2) Do your own payment processing.
      3) Write your own API and library function to verify the payment, update the order status accordingly (payment OK or canceled or error).
      4) Decide for yourself if you want to follow up with failed payments for sales recovery. Allow customer to switch payment modes.

      Good luck.
      https://developer.paypal.com/docs/business/checkout/
      https://code-boxx.com/faq/#help

  2. Hi, i’m trying to add some other fields before going checkout, such as surname(just for adding another text-field). I added the input with associated id (#surname) in “ajax_cart.php” (and modified the case with $_POST[‘name’], $_POST[‘surname’], $_POST[’email’], added in cart.js the following surname:document.getElementById(“surname”).value, updated the database table
    and updated core.php and i get this SQLSTATE[23000]: Integrity constraint violation: 1048 Column ‘order_surname’ cannot be null. What am i doing wrong?

Leave a Comment

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