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
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
and6-orders.sql
. - Change the database settings in
1-database.php
to your own. - Launch
3-products.php
in the browser.
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
// (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 aSELECT
SQL query, fetch results from the database.start()
andend()
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
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
<!-- (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
// (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.
|
set |
Change item quantity in cart.
|
del |
Removes an item from the cart.
|
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
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 callfetch()
to4-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
-- (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
-- (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
// (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.
- Just adapt from the session and pop it into
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:
- How to import MySQL Files
- AJAX – Beginner’s Examples
- Trouble sending out Email?
- How to create a user registration form.
- Simple administration panel.
- Import/export CSV, if you need to do reports.
- Looking to add online payment methods? Paypal, Stripe.
Or consider getting my eBook below.
A BETTER SHOPPING CART

FULL-FLEDGED SHOPPING CARTS
Changed your mind? Don’t want to do so much coding? Try:
- Good old Magento
- WordPress and Woocommerce
- Shopify
- XCart
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!
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
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.
Hi
I want to buy the e-book but when i press purshase i get checkout page with this text: your cart is impty !!
Thanks for reporting. Disabled caching on the shop and checkout pages… It should be less funky now.
I followed all instruction step by step still not able to view the cart details page.
please help!
What is “not able to view the cart details”?
https://code-boxx.com/faq/#notwork
https://code-boxx.com/simple-javascript-debugging-troubleshooting/
* 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’)
A quick freebie. Add a product name and subtotal field to the order items table, and change the checkout function. Do consider getting the ebook if you are working on a commercial project – https://code-boxx.com/shop/shopping-cart-system/
Here are possible places to get free help with your project – https://code-boxx.com/websites-get-help-programming/
Otherwise… https://code-boxx.com/faq/#help
What should I do if I want to add products to the cart using their id instead of select from the list.
$_CC->Cart->add(ID, QTY);