Welcome to a tutorial on how to create a simple food ordering system in PHP and MYSQL. So you have picked up a project for a restaurant, or maybe you are stuck with this as a school assignment? Well, here is a quick sharing of mine – 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 FOOD ORDERING
Let us now get into the details of the food ordering system. But a disclaimer – This is more of an “in-store ordering kiosk” and not an online ordering platform.
PART 1) THE DATABASE
1A) MENU
-- (A) MENU
CREATE TABLE `menu` (
`item_id` bigint(20) NOT NULL,
`item_name` varchar(255) NOT NULL,
`item_desc` varchar(255) DEFAULT NULL,
`item_img` varchar(255) DEFAULT NULL,
`item_price` decimal(12,2) NOT NULL DEFAULT 0.00
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
ALTER TABLE `menu`
ADD PRIMARY KEY (`item_id`),
ADD KEY `item_name` (`item_name`);
ALTER TABLE `menu`
MODIFY `item_id` bigint(20) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5;
Let us address the elephant in the room. We need a database to store the menu items and orders. This one should be pretty self-explanatory, the available menu items.
1B) ORDERS
-- (B) ORDERS
CREATE TABLE `orders` (
`order_id` bigint(20) NOT NULL,
`order_date` datetime NOT NULL DEFAULT current_timestamp(),
`order_status` tinyint(1) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
ALTER TABLE `orders`
ADD PRIMARY KEY (`order_id`),
ADD KEY `order_date` (`order_date`),
ADD KEY `order_status` (`order_status`);
ALTER TABLE `orders`
MODIFY `order_id` bigint(20) NOT NULL AUTO_INCREMENT;
Yep, there is no customer information in the orders. This is an “ordering kiosk” and not an “online ordering platform”. If you want, you can add the customer information on your own.
1C) ORDER ITEMS
-- (C) ORDER ITEMS
CREATE TABLE `order_items` (
`order_id` bigint(20) NOT NULL,
`item_id` bigint(20) NOT NULL,
`item_name` varchar(255) NOT NULL,
`item_price` decimal(12,2) NOT NULL,
`item_qty` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
ALTER TABLE `order_items`
ADD PRIMARY KEY (`order_id`,`item_id`);
The items being ordered. The item name and price are captured at the time of checkout for “data integrity”, they are not “redundant”.
1D) ORDER TOTALS
-- (D) ORDER TOTALS
CREATE TABLE `order_totals` (
`order_id` bigint(20) NOT NULL,
`total_id` bigint(20) NOT NULL,
`total_name` varchar(255) NOT NULL,
`total_amt` decimal(12,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
ALTER TABLE `order_totals`
ADD PRIMARY KEY (`order_id`,`total_id`);
Lastly, a table for the totals – Subtotal, discount, taxes, surcharge, grand total, etc…
PART 2) PHP FOOD ORDERING LIBRARY
2A) INIT
class Order {
// (A) CONSTRUCTOR - CONNECT TO THE DATABASE
private $pdo = null;
private $stmt = 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; }
}
// ...
}
// (I) 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", "");
// (J) ORDER STATUS
define("ORDER_STAT", [
0 => "Preparing", 1 => "Ready", 2 => "Delivered"
]);
// (K) START!
$_ORD = new Order();
session_start();
if (!isset($_SESSION["cart"])) { $_SESSION["cart"] = []; }
This may be intimidating for some beginners, but keep calm and look closely.
- (A, B, K) When
$_ORD = new Order()
is created, the constructor connects to the database. The destructor closes the connection. - (I) Remember to change the database settings to your own.
- (J) The order status or process flow – Feel free to change this.
- (K) Take note that we are using
$_SESSION
to handle the user’s shopping cart. More on that later.
2B) LIBRARY FUNCTIONS
// (C) EXECUTE SQL QUERY
function query ($sql, $data=null) : void {
$this->stmt = $this->pdo->prepare($sql);
$this->stmt->execute($data);
}
// (D) GET ALL MENU ITEMS
function getMenu () {
$this->query("SELECT * FROM `menu`");
return $this->stmt->fetchAll();
}
// (E) GET ITEMS IN CART
function getCart () {
// (E1) SQL QUERY
$this->query(sprintf(
"SELECT * FROM `menu` WHERE `item_id` IN (%s)",
substr(str_repeat("?,", count($_SESSION["cart"])), 0, -1)
), array_keys($_SESSION["cart"]));
// (E2) GET DATA
$cart = ["i"=>[], "t"=>0];
while ($r = $this->stmt->fetch()) {
$cart["i"][$r["item_id"]] = [
"n" => $r["item_name"], // name
"q" => $_SESSION["cart"][$r["item_id"]], // quantity
"p" => $r["item_price"] * $_SESSION["cart"][$r["item_id"]] // price
];
$cart["t"] += $cart["i"][$r["item_id"]]["p"];
}
return $cart;
}
// (F) CHECKOUT
function checkout () : void {
// (F1) EMPTY CART
if (count($_SESSION["cart"])==0) { exit("Cart is empty."); }
// (F2) "MAIN ORDER"
$this->pdo->beginTransaction();
$this->query("INSERT INTO `orders` (`order_status`) VALUES (0)");
$oid = $this->pdo->lastInsertId();
// (F3) ORDER ITEMS
$cart = $this->getCart();
$i = 1;
foreach ($cart["i"] as $item) {
$this->query(
"INSERT INTO `order_items` (`order_id`, `item_id`, `item_name`, `item_price`, `item_qty`) VALUES (?, ?, ?, ?, ?)",
[$oid, $i, $item["n"], $item["p"], $item["q"]]
);
$i++;
}
// (F4) ORDER TOTALS
$this->query(
"INSERT INTO `order_totals` (`order_id`, `total_id`, `total_name`, `total_amt`) VALUES (?, ?, ?, ?)",
[$oid, 1, "total", $cart["t"]]
);
// (F5) DONE
$this->pdo->commit();
$_SESSION["cart"] = [];
echo "OK";
}
// (G) GET ALL INCOMPLETE ORDERS
function getOrders () {
$orders = [];
$this->query("SELECT * FROM `orders` WHERE `order_status`<2");
foreach ($this->stmt->fetchAll() as $o) {
$orders[$o["order_id"]] = $o;
$this->query("SELECT `item_name`, `item_price`, `item_qty` FROM `order_items` WHERE `order_id`=?", [$o["order_id"]]);
$orders[$o["order_id"]]["i"] = $this->stmt->fetchAll();
$this->query("SELECT `total_name`, `total_amt` FROM `order_totals` WHERE `order_id`=?", [$o["order_id"]]);
$orders[$o["order_id"]]["t"] = $this->stmt->fetchAll();
}
return $orders;
}
// (H) UPDATE ORDER
function update ($id) : void {
$this->query(
"UPDATE `orders` SET `order_status`=`order_status`+1 WHERE `order_id`=?",
[$id]
);
}
Not going to explain every function line-by-line, so a quick summary:
query()
Helper function to run an SQL query.getMenu()
Get all the menu items.getCart()
Get all the items in the shopping cart.checkout()
Checkout the current shopping cart.getOrders()
Get all the incomplete orders.update()
Update an order.
PART 3) ORDERING PAGE
3A) THE HTML
<!-- (A) MENU ITEMS -->
<div id="menu"><?php
require "2-lib.php";
foreach ($_ORD->getMenu() as $item) { ?>
<div class="item">
<img class="item-img" src="<?=$item["item_img"]?>">
<div class="item-name">
<?=$item["item_name"]?>
$<?=$item["item_price"]?>
</div>
<div class="item-desc"><?=$item["item_desc"]?></div>
<input class="item-add" type="button" value="Add To Cart" onclick="cart.add(<?=$item["item_id"]?>)">
</div>
<?php } ?>
</div>
<!-- (B) CART -->
<div id="cart"></div>
This is the screenshot right at the top.
- Use the PHP library to get all the menu items, draw the HTML.
- An empty
<div>
to show what is in the current shopping cart.
3B) THE JAVASCRIPT
var cart = {
// (A) PROPERTIES
hCart : null, // html cart div
// (B) AJAX HELPER
ajax : opt => {
// (B1) FORM DATA
let form = new FormData();
for (let [k,v] of Object.entries(opt.data)) {
form.append(k, v);
}
// (B2) AJAX FETCH
fetch("3c-cart.php", { method:"post", body:form })
.then(res => res.text())
.then(res => opt.ok(res))
.catch(err => console.error(err));
},
// (C) CART INIT
init : () => {
cart.hCart = document.getElementById("cart");
cart.list();
},
// (D) LIST CART
list : () => cart.ajax({
data : { req : "list" },
ok : res => cart.hCart.innerHTML = res
}),
// (E) ADD ITEM TO CART
add : id => cart.ajax({
data : { req : "add", id : id },
ok : res => cart.list()
}),
// (F) SET ITEM QUANTITY
set : (id, qty) => cart.ajax({
data : { req : "set", id : id, qty : qty },
ok : res => cart.list()
}),
// (G) REMOVE ITEM FROM CART
del : id => cart.ajax({
data : { req : "del", id : id },
ok : res => cart.list()
}),
// (H) CHECKOUT
checkout : () => cart.ajax({
data : { req : "checkout" },
ok : res => {
if (res=="OK") {
alert("Checkout successful!");
// @TODO - UP TO YOU
} else { alert(res); }
cart.list();
}
})
};
window.onload = cart.init;
Very “scary”. But look carefully, we are pretty much just doing AJAX calls to 3c-cart.php
to drive the cart. The end.
3C) AJAX CART HANDLER
<?php
require "2-lib.php";
if (isset($_POST["req"])) { switch($_POST["req"]) {
// (A) LIST CART ITEMS
case "list":
if (count($_SESSION["cart"]) > 0) {
// (A1) CART ITEMS
$cart = $_ORD->getCart();
foreach($cart["i"] as $id=>$item) { ?>
<div class="cart-row">
<div class="cart-del" onclick="cart.del(<?=$id?>)">X</div>
<div class="cart-item">
<?=$item["n"]?>
$<?=$item["p"]?>
</div>
<input class="cart-qty" type="number" value="<?=$item["q"]?>" min="1" max="99" onchange="cart.set(<?=$id?>, this.value)">
</div>
<?php }
// (A2) TOTAL
printf("<div class='cart-row total'>
<div class='cart-item'>Total</div>
<div>$%.2f</div>
</div>", $cart["t"]);
// (A3) CHECKOUT
echo "<input class='cart-checkout' type='button' value='Checkout' onclick='cart.checkout()'>";
} else { echo "<div class='cart-row'>Cart is empty.</div>"; }
break;
// (B) ADD ITEM TO CART
case "add":
if (isset($_SESSION["cart"][$_POST["id"]])) {
$_SESSION["cart"][$_POST["id"]]++;
} else {
$_SESSION["cart"][$_POST["id"]] = 1;
}
break;
// (C) SET QUANTITY
case "set":
$_SESSION["cart"][$_POST["id"]] = $_POST["qty"];
break;
// (D) REMOVE ITEM
case "del":
unset($_SESSION["cart"][$_POST["id"]]);
break;
// (E) CHECKOUT
case "checkout":
$_ORD->checkout();
break;
}}
How the shopping cart works – $_SESSION["cart"][ITEM-ID] = QUANTITY
.
PART 4) DUMMY ADMIN
4A) THE HTML
<!-- (A) UPDATE ORDER -->
<form id="upform" method="post">
<input type="hidden" id="upid" name="id">
</form>
<?php
require "2-lib.php";
if (isset($_POST["id"])) { $_ORD->update($_POST["id"]); }
?>
<!-- (B) ORDERS LIST -->
<div id="orders"><?php
foreach ($_ORD->getOrders() as $id=>$o) { ?>
<div class="order" onclick="up(<?=$id?>)">
<!-- (B1) ORDER DATA -->
<div class="order-id">#<?=$id?></div>
<div>
<div class="order-stat"><?=ORDER_STAT[$o["order_status"]]?></div>
</div>
<div class="order-time"><?=$o["order_date"]?></div>
<!-- (B2) ORDER ITEMS -->
<ul class="order-items"><?php
foreach ($o["i"] as $i) {
printf("<li>%s x %d</li>", $i["item_name"], $i["item_qty"]);
}
?></ul>
</div>
<?php }
?></div>
Just a dummy admin page to list all the orders, click on an order to update the order status.
4B) THE JAVASCRIPT
function up (id) {
document.getElementById("upid").value = id;
document.getElementById("upform").submit();
}
EXTRAS
That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.
JUST A STARTING POINT
Yes, we have only scratched the surface with all of the above. There are a lot of blanks that you have to fill in by yourself:
- Complete the ordering process, it’s different for everyone:
- Pending – Preparing – Ready – Collected?
- Pending – Payment – Prepare – Ready – Collected?
- Delivery? Self-collect? Send to table number? Queue number system?
- Cancellations and refunds?
- Implement a user system if you want “more than just a kiosk”.
- If there is an Internet connection, it is possible to process online payments – Paypal, Apple Pay, Google Pay, Amazon Pay, etc…
- Security and checks.
- A proper admin panel.
- More checks for “valid cart items” during ordering and checkout.
A WAY BETTER SHOPPING CART

LINKS & REFERENCES
- Chrome Kiosk Mode – Code Boxx
- PHP MYSQL User Role Management System – Code Boxx
- NodeJS Food Ordering System – Code Boxx
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!