Simple Step-By-Step Shopping Cart With PHP MySQL

Welcome to a step-by-step 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 our own basic shopping cart, without using any third-party frameworks – Read on!

ⓘ I have included a zip file with all the source code at the start of the tutorial, so you don’t have to copy-paste everything… Or if you just want to dive straight in.

 

TABLE OF CONTENTS

Download & Notes Shopping Cart Useful Bits & Links
The End

 

 

DOWNLOAD & NOTES

First, here is the download link to the example code as promised.

 

QUICK NOTES

If you prefer to “quickstart” and trace the code by yourself:

  • Create a database, import 2-products.sql and 6-orders.sql.
  • Change the database settings in 1-database.php to your own.
  • Launch 3-products.php in the browser.
If you spot a bug, feel free to comment below. I try to answer short questions too, but it is one person versus the entire world… If you need answers urgently, please check out my list of websites to get help with programming.

 

 

TUTORIAL CODE DOWNLOAD

Click here to download all the source code in a zip file – I have released it under the MIT License, so feel free to build on top of it if you want to.

 

 

PHP MYSQL SHOPPING CART

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

 

STEP 1) DATABASE LIBRARY

1-database.php
// (A) DATABASE CLASS
class DB {
  // (A1) CONSTRUCTOR - CONNECT TO DATABASE
  private $pdo = null;
  private $stmt = null;
  public $lastID = null;
  public $error = "";
  function __construct() {
    try {
      $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
      ]);
    } catch (Exception $ex) { exit($ex->getMessage()); }
  }
 
  // (A2) DESTRUCTOR - CLOSE DATABASE CONNECTION
  function __destruct() {
    if ($this->stmt !== null) { $this->stmt = null; }
    if ($this->pdo !== null) { $this->pdo = null; }
  }
 
  // (A3) EXECUTE SQL QUERY
  function exec ($sql, $data=null) {
    try {
      $this->stmt = $this->pdo->prepare($sql);
      $this->stmt->execute($data);
      $this->lastID = $this->pdo->lastInsertId();
      return true;
    } catch (Exception $ex) {
      $this->error = $ex->getMessage();
      return false;
    }
  }
 
  // (A4) FETCH ALL
  function fetchAll ($sql, $data=null, $key=null) {
    if ($this->exec($sql, $data)===false) { return false; }
    if ($key===null) { return $this->stmt->fetchAll(); }
    else {
      $res = [];
      while ($r = $this->stmt->fetch()) { $res[$r[$key]] = $r; }
      return $res;
    }
  }
 
  // (A5) AUTOCOMMIT OFF
  function start () {
    $this->pdo->beginTransaction();
  }
  function end ($pass=true) {
    if ($pass) { $this->pdo->commit(); }
    else { $this->pdo->rollBack(); }
  }
}
 
// (B) DATABASE SETTINGS - CHANGE TO YOUR OWN!
define("DB_HOST", "localhost");
define("DB_NAME", "test");
define("DB_CHARSET", "utf8");
define("DB_USER", "root");
define("DB_PASSWORD", "");
 
// (C) NEW CART OBJECT
$DB = new DB();

First, we start by creating a library to deal with all the SQL stuff. Some may find this intimidating, but keep calm and look carefully:

  • (A1 & A2) When $DB = new DB() is created, the constructor connects to the database. The destructor closes the connection.
  • (A3 To A5) The useful functions.
    • exec() Runs an SQL query.
    • fetchAll() Runs a SELECT SQL query, fetch results from the database.
    • start() and end() Auto-commit off. For dealing with multiple queries that we want to verify step-by-step. Rollback when something goes wrong.
  • (B & C) Self-explanatory. Remember to change the settings to your own.

P.S. Make sure that the PDO extension is enabled in php.ini.

 

 

STEP 2) PRODUCTS TABLE

2-products.sql
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=utf8;

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

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

This table should be self-explanatory.

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

Feel free to add more fields on your own as required.

 

STEP 3) PRODUCTS PAGE

3-products.php
<!-- (A) PRODUCTS -->
<div id="products"><?php
  // (A1) GET ALL PRODUCTS
  require "1-database.php";
  $products = $DB->fetchAll("SELECT * FROM `products`");
 
  // (A2) PRODUCTS LIST
  foreach ($products as $p) { ?>
  <div class="pCell">
    <div class="pTxt">
      <div class="pName"><?=$p["name"]?></div>
      <div class="pPrice">$<?=$p["price"]?></div>
    </div>
    <img class="pImg" src="images/<?=$p["image"]?>"/>
    <button class="pAdd" onclick="cart.add(<?=$p["pid"]?>)">
      Add To Cart
    </button>
  </div>
  <?php } ?>
</div>
 
<!-- (B) CURRENT CART -->
<div id="cart"></div>

With the database library and products table in place, this should be very obvious – We are just using the library to fetch all the products, then display them on a “nice HTML page”. Take note of the onclick="cart.add()" though, we will take care of the cart actions in the next steps.

 

 

STEP 4) CART AJAX HANDLER

4-ajax-cart.php
// (A) INIT SHOPPING CART SESSION
session_start();
if (!isset($_SESSION["cart"])) { $_SESSION["cart"] = []; }
 
// (B) STANDARD RESPONSE
function respond ($status=1, $msg="") {
  exit(json_encode(["status"=>$status, "msg"=>$msg]));
}
 
if (isset($_POST["req"])) { switch ($_POST["req"]) {
  // (C) INVALID
  default: respond(0, "Invalid Request");
 
  // (D) ADD TO CART
  case "add":
    $qty = &$_SESSION["cart"][$_POST["pid"]];
    if (isset($qty)) { $qty++; } else { $qty = 1; }
    if ($qty > 99) { $qty = 99; }
    respond();
 
  // (E) CHANGE QUANTITY
  case "set":
    $qty = &$_SESSION["cart"][$_POST["pid"]];
    $qty = $_POST["qty"];
    if ($qty > 99) { $qty = 99; }
    if ($qty <= 0) { unset($_SESSION["cart"][$_POST["pid"]]); }
    respond();
 
  // (F) REMOVE ITEM
  case "del":
    unset($_SESSION["cart"][$_POST["pid"]]);
    respond();
 
  // (G) NUKE
  case "nuke":
    $_SESSION["cart"] = [];
    respond();
 
  // (H) GET ALL ITEMS IN CART
  case "get":
    // (H1) EMPTY CART
    if (count($_SESSION["cart"])==0) { respond(1, null); }
 
    // (H2) GET ITEMS IN CART
    require "1-database.php";
    $sql = "SELECT * FROM `products` WHERE `pid` IN (";
    $sql .= str_repeat("?,", count($_SESSION["cart"]) - 1) . "?";
    $sql .= ")";
    $items = $DB->fetchAll($sql, array_keys($_SESSION["cart"]), "pid");
 
    // (H3) FILTER ILLEGAL PRODUCTS
    if (count($items)==0) {
      $_SESSION["cart"] = [];
      respond(1, null);
    }
    foreach ($_SESSION["cart"] as $pid=>$qty) {
      if (isset($items[$pid])) { $items[$pid]["qty"] = $qty; }
      else { unset($_SESSION["cart"][$pid]); }
    }
    if (count($_SESSION["cart"])==0) { respond(1, null); }
 
    // (H4) RESPOND
    respond(1, $items);
}}

This script handles all the cart actions. It looks complicated again, but how it works is dreadfully simple:

  • We send $_POST["req"] and the required parameters to perform an action. For example, $_POST = ["req"=>"add", "pid"=>123] will add product ID 123 into the cart.
  • We are storing the shopping cart in $_SESSION["cart"], in the straightforward format of [ PRODUCT ID => QUANTITY, PRODUCT ID => QUANTITY, ... ].

 

Request Description & Parameters
add Add product to cart.

  • pid Product ID
set Change item quantity in cart.

  • pid Product ID
  • qty Quantity, removes item if 0.
del Removes an item from the cart.

  • pid Product ID
nuke Empty the entire cart.
get This does a SELECT * FROM `products` WHERE `pid` IN (SESSION CART) SQL query. Also, removes all illegal items from $_SESSION["cart"] – If a product ID cannot be matched in the database, it will be removed.

 

 

STEP 5) JAVASCRIPT CART

5-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("4-ajax-cart.php", { method:"POST", body:form })
    .then((res) => res.json()).then((res) => {
      if (res.status!=1) { alert(res.msg); }
      else if (after) { after(res.msg); }
    }).catch((err) => {
      alert("Error");
      console.error(err);
    });
  },

  // (B) SHOW ITEMS IN CART
  show : () => {
    cart.ajax({ req : "get" }, (items) => {
      // (B1) GET HTML CART
      let hcart = document.getElementById("cart");
      hcart.innerHTML = "";

      // (B2) EMPTY CART
      if (items===null) { hcart.innerHTML = "Cart is empty."; }

      // (B3) DRAW CART ITEMS
      else {
        let total = 0;
        for (let [id, pdt] of Object.entries(items)) {
          // ITEM ROW HTML ELEMENTS
          let row = document.createElement("div"),
              rowA = document.createElement("button"),
              rowB = document.createElement("div"),
              rowC = document.createElement("input");

          // DELETE BUTTON
          rowA.innerHTML = "X";
          rowA.onclick = () => { cart.del(id); };
          rowA.className = "cDel";
          row.appendChild(rowA);

          // NAME
          rowB.innerHTML = pdt.name;
          rowB.className = "cName";
          row.appendChild(rowB);

          // QUANTITY
          rowC.type = "number";
          rowC.value = pdt.qty;
          rowC.min = 0; rowC.max = 99;
          rowC.onchange = function () { cart.set(id, this.value); };
          rowC.className = "cQty";
          row.appendChild(rowC);

          // ADD TO GRAND TOTAL
          total += pdt.qty * pdt.price;

          // ENTIRE ROW
          row.className = "cRow";
          hcart.appendChild(row);
        }

        // GRAND TOTAL
        let row = document.createElement("div");
        row.innerHTML = "TOTAL $" + total.toFixed(2);
        row.className = "cTotal";
        hcart.appendChild(row);

        // CHECKOUT
        row = document.createElement("button");
        row.innerHTML = "Checkout";
        row.className = "cOut";
        row.onclick = () => { location.href = "7-checkout.php"; };
        hcart.appendChild(row);

        // EMPTY CART
        row = document.createElement("button");
        row.innerHTML = "Empty";
        row.className = "cNuke";
        row.onclick = cart.nuke;
        hcart.appendChild(row);
      }
    });
  },

  // (C) ADD ITEM TO CART
  add : (pid) => {
    cart.ajax({ req : "add", pid : pid }, cart.show);
  },

  // (D) CHANGE QUANTITY
  set : (pid, qty) => {
    cart.ajax({ req : "set", pid : pid, qty : qty }, cart.show);
  },

  // (E) REMOVE ITEM
  del : (pid) => {
    cart.ajax({ req : "del", pid : pid }, cart.show);
  },

  // (F) NUKE
  nuke : () => { if (confirm("Empty cart?")) {
    cart.ajax({ req : "nuke" }, cart.show);
  }}
};
window.addEventListener("DOMContentLoaded", cart.show);

Right, this is a whole load of “scary Javascript”, but I think you catch the drift. All it does is literally:

  • (A) ajax() Helper function to call fetch() to 4-ajax-cart.php.
  • (B) show() Get the current cart items from the server, update the HTML.
  • (C To F) Yep. All AJAX calls to drive the cart actions.
    • add() Add item to the cart, then update the current cart items.
    • set() Set item quantity, then update the current cart items.
    • del() Remove an item, update HTML.
    • nuke() Clear the entire cart, update HTML.

That’s all to the cart mechanics. Easy? Let us deal with the checkout and order taking next.

 

 

STEP 6) ORDERS DATABASE

ORDERS TABLE

6-orders.sql
-- (A) 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=utf8;
 
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;

This is the “main orders table”, where we hold 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.

Go ahead – Modify this table to your own needs.

 

ORDER ITEMS TABLE

6-orders.sql
-- (B) ORDERS ITEMS TABLE
CREATE TABLE `orders_items` (
  `oid` bigint(20) NOT NULL,
  `pid` bigint(20) NOT NULL,
  `qty` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
ALTER TABLE `orders_items`
  ADD PRIMARY KEY (`oid`,`pid`);

Next, we have another table to hold all the order items.

  • oid Order ID, primary and foreign key.
  • pid Product ID, primary and foreign key.
  • qty Quantity.

P.S. You may want to modify this to save the full products details. Some people may think it is redundant, but consider the possibility of products getting deleted or updated in the future.

 

STEP 7) CHECKOUT

7-checkout.php
// (A) CHECK CART
session_start();
  if (!isset($_SESSION["cart"]) || count($_SESSION["cart"])==0) {
  exit("Cart is empty");
}
 
// (B) DATABASE - AUTOCOMMIT OFF
require "1-database.php";
$DB->start();
 
// (C) MAIN ORDER - USING A DUMMY USER FOR THIS EXAMPLE
$name = "Jon Doe";
$email = "jon@doe.com";
$pass = $DB->exec(
  "INSERT INTO `orders` (`name`, `email`) VALUES (?,?)",
  [$name, $email]
);
 
// (D) ORDER ITEMS
if ($pass) {
  // (D1) INSERT SQL
  $sql = "INSERT INTO `orders_items` (`oid`, `pid`, `qty`) VALUES ";
  $sql .= str_repeat("(?,?,?),", count($_SESSION["cart"]));
  $sql = substr($sql, 0, -1) . ";";
 
  // (D2) INSERT DATA
  $data = [];
  foreach ($_SESSION["cart"] as $pid=>$qty) {
    $data[] = $DB->lastID;
    $data[] = $pid;
    $data[] = $qty;
  }
 
  // (D3) GO!
  $pass = $DB->exec($sql, $data);
}
 
// (E) COMMIT OR DISCARD?
$DB->end($pass);
if ($pass) { $_SESSION["cart"] = []; }
echo $pass ? "DONE!" : $DB->error;

Final step – Record $_SESSION["cart"] into the orders table.

 

COMPLETE YOUR OWN CHECKOUT PROCESS

Now, everyone has a different starting point and requirement. Completing the checkout process is your homework.

  • If you are thinking – Where do we collect the customer’s name and email?
    • Create your own HTML to collect whatever is required.
    • Clicking on checkout should redirect to your HTML form.
    • Submit the form to 7-checkout.php. Modify it to collect your data.
  • If you already have an existing users system.
    • Just adapt from the session and pop it into 7-checkout.php.
    • If not signed in, ask the user to register or log in first.

Lastly, send a confirmation email to the customer. Maybe also to the admin. Do whatever is required – You make your own system.

 

USEFUL BITS & LINKS

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

 

FUTURE EXPANSIONS & MORE TUTORIALS

This tutorial has barely covered a small part of a possibly massive system. It is still missing a user system, an administrator’s panel, and so much more. Here are a few links to my other guides that may be useful:

Or consider getting my eBook below.

 

A BETTER SHOPPING CART

Need a shopping cart with product options? Product categories? Discount coupons? User system and admin panel? Click here to check out my eBook!

 

FULL-FLEDGED SHOPPING CARTS

Changed your mind? Don’t want to do so much coding? Try:

 

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 Step-By-Step Shopping Cart With PHP MySQL”

  1. Is this comment section still active ?
    Posted a comment/question a few days ago and seems to be gone …

    Thanks
    Bart

    1. Sorry, I just can’t reply to everyone. The spam filter has discarded over thousands of messages too… If you need help up urgently, please check the above “places to get help with programming”, or hire a freelancer instead.

  2. make this shopping cart available. I have a question, when the order is inserted in the bank, it goes with the ID, product ID and a quantity. I would also like to add the name of the product, try all the most unsuccessful ways. Could you help me, please?

    1. As in the “adding more fields” section –
      1) DATABASE – Add product name field in order items table.
      2A) SERVER SCRIPT – Update 4c-lib-cart.php, function checkout. Before inserting the items, use $this->details() to get all the cart items > Then just add the product name INSERT INTO order_items (order_id, product_id, product_name)...
      2B) Also change function get, remove LEFT JOIN products.
      3) CLIENT-SCRIPT – Shouldn’t need any updates.

  3. Hello,
    the supplemental email order section does not work correctly, it does not retrieve any values from the database.

    1. Fixed, missed out a couple of small stuff – Download and try again.
      1) 4c-lib-cart.php, “checkout()” – Stored the order ID into $this->orderID instead.
      2) 4b-ajax-cart.php, “checkout-email” – Did not create $cartLib = new Cart() object.
      3) 4b-ajax-cart.php, “checkout-email” – Used $cartLib->orderID to get the order instead.

  4. 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. 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.

  5. 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?

  6. 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.

  7. 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']);

      1. 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 *