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
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
-- (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
-- (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
-- (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
<?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
<?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 ofPRODUCT 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 |
|
nuke |
Empty the entire cart. |
get |
Get all items in the cart. |
checkout |
Checkout the current cart.
|
PART 4) SHOPPING PAGE
4A) HTML SHOPPING PAGE
<!-- (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)">⇦</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
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=">" 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 to3-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
<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
- User Registration With PHP MYSQL – Code Boxx
- Simple User Login System With PHP MySQL – Code Boxx
- User Role Management In PHP MYSQL – Code Boxx
- Simple PHP Admin Panel – Code Boxx
- If you change your mind, there are plenty of “full shopping carts”.
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!
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?
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
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?
Control - F5
https://code-boxx.com/force-browsers-reload-javascript-css/
many many thanks, you pretty saved my exam!