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´m Marcos, from Brazil. Very nice project. Works perfectly!! My only challenge now is to make a fill adjustments for my small project :-), but no pain no gain :-).

  2. Hi
    Many thanks for the script. We have too many products to be listed or shown. Can you please tell me what modifications will be needed for buyer to to start the process by typing the part number, rather than clicking on a displayed item.

    Thanks

    1. Hi Ashot,
      1) Database – Modify the product and order_item tables, add a new part number or SKU column.
      2) Server-side scripts – Modify the library and AJAX files to include the new SKU column. Add a new “search product” function to the cart library.
      3) Client-side scripts – Add a search form.

      The rest of the mechanics are pretty much the same. Good luck!

    1. There are 3 parts to the system and you need to update them accordingly –
      – Add your extra fields into the database
      – Update the orders library and AJAX handler
      – Update the HTML form and the Javascript

      1. Not sure the code still works.
        Just get a black website with the following:
        Array ( [items] => Array ( ) )

      2. Gotcha. Fixed, and tested working with PHP 7.2.5, MySQL 5, and Apache 2.4.33 – Should be friendly on most modern browsers.

    1. Yes. There are 3 tables in this project – Products, orders, and order items. I have included the SQL in the zip download, and you can easily import it using phpMyAdmin (or use the MySQL command line if you are feeling adventurous).

  3. hey … really nice code 😉 works perfect 😉 bu i have one question …
    I want modify the code…. so that…. the cart will be visible all the time and the products added to the cart were automatically displayed on it ?
    big thanks

    1. Sure thing, you can do that with some changes:
      1) Modify the layout of the page, so that the cart is always visible.
      2) Change the toggleCart function to a showCart function instead – Remove the hide portion, so that it will only update the cart contents.
      3) Then on every successful AJAX cart action (add, change, remove), simply call showCart to update the contents.
      But I will personally discourage this design, it is not friendly on mobile devices – Reloading the cart on every add/update/remove will consume more data, is slower, and there is not much screen space to show the entire cart anyway.

  4. i’ve error in ajax-cart

    Parse error: syntax error, unexpected ‘[‘

    // CONNECT TO DB
    $pdo = new PDO(
    “mysql:host=$host;dbname=$dbname;charset=$charset”,
    $user, $password, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
    ]
    );

  5. please this line
    switch ($_POST[‘request’]) {
    in ajax.cart.php is bringing undefined jndex error, pleas how do i rectify this?

    1. Hi! Double check your code – That is because you have not posted the request variable to the script, maybe a spelling mistake somewhere?

Leave a Comment

Your email address will not be published.