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

  3. Hello!! finally a fully functional source code Im so grateful thank you so much ^_^
    Im new to this field and i want to explore more Im really confused can I ask how can I change the database settings in lib/core.php? Where can i change it T^T

    1. Eh… You can do that by learning basic HTML, CSS, JS, PHP, MYSQL, AMP servers, and how to use a code editor first.

      Seriously, start from the basics, go through the basic tutorials one-by-one. If you are asking this kind of question, this tutorial is way beyond your current level. Good luck.

  4. * What if I want the product name and subtotal to be inserted into the db tho?
    * Hey if I wanted to insert product name into the order item db, where do i need to alter?
    * I’ve got a uncaught error: call to a member function getAll() on null when i used $_CC->Cart->getAll() in lib-cart.php
    * Where do you have to use $_CC->Cart->getAll()? I received a fatal error that traced back to ajax-cart.php(100): Cart->checkout(‘John Doe’, ‘john@doe.com’)

Leave a Comment

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