Skip to content

Instantly share code, notes, and snippets.

@code-boxx
Last active May 26, 2023 05:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save code-boxx/e5594de754467b9ab0ec42b534ae43a1 to your computer and use it in GitHub Desktop.
Save code-boxx/e5594de754467b9ab0ec42b534ae43a1 to your computer and use it in GitHub Desktop.
PHP Queue Number System

PHP QUEUE NUMBER SYSTEM

https://code-boxx.com/customer-queue-number-system-php/

NOTES

  1. Install Composer if you have not already done so, run composer require cboden/ratchet to install Ratchet.
  2. Change the port/URL in 1-server.php and 2-queue.js if not deploying at ws://localhost:8080.
  3. Run php 1-server.php in the command line.
  4. Access 3a-admin.html in the browser for the "admin page", and 4a-board.html for the "queue board".
  5. Get your own alarm sound, do a search for "free sound effects" on the Internet - Rename it to ding-dong.mp3 in your project folder.

LICENSE

Copyright by Code Boxx

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

<?php
// (A) COMMAND LINE CHECK
// CREDITS : https://stackoverflow.com/questions/933367/php-how-to-best-determine-if-the-current-invocation-is-from-cli-or-web-server
function is_cli () {
if (php_sapi_name()==="cli") { return true; }
if (defined("STDIN")) { return true; }
if (array_key_exists("SHELL", $_ENV)) { return true; }
if (!array_key_exists("REQUEST_METHOD", $_SERVER)) { return true; }
if (empty($_SERVER["REMOTE_ADDR"]) && !isset($_SERVER["HTTP_USER_AGENT"]) && count($_SERVER["argv"])>0) { return true; }
return false;
}
if (!is_cli()) { exit("Please run this in the command line."); }
// (!) PHP 8.2+ HAS DEPRECATED DYNAMIC PROPERTIES
// (!) RATCHET IS STILL USING DYNAMIC PROPERTIES AT TIME OF UPDATE
// (!) WILL THROW DEPRECATED WARNINGS, THIS IS TO "SILENCE" THE ERROR
error_reporting(E_ALL & ~E_DEPRECATED);
// (B) LOAD RATCHET
// composer require cboden/ratchet
require "vendor/autoload.php";
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
// (C) QUEUE CLASS
class Queue implements MessageComponentInterface {
// (C1) PROPERTIES
private $debug = true; // debug mode
protected $clients; // connected clients
private $qt = 0; // total queue number
private $qn = 0; // current queue number
// (C2) CONSTRUCTOR - INIT LIST OF CLIENTS
public function __construct () {
$this->clients = new \SplObjectStorage;
if ($this->debug) { echo "Queue server started.\r\n"; }
}
// (C3) ON CLIENT CONNECT - STORE INTO $THIS->CLIENTS
public function onOpen (ConnectionInterface $conn) {
$this->clients->attach($conn);
if ($this->debug) { echo "Client connected: {$conn->resourceId}\r\n"; }
}
// (C4) ON CLIENT DISCONNECT - REMOVE FROM $THIS->CLIENTS
public function onClose (ConnectionInterface $conn) {
$this->clients->detach($conn);
if ($this->debug) { echo "Client disconnected: {$conn->resourceId}\r\n"; }
}
// (C5) ON ERROR
public function onError (ConnectionInterface $conn, \Exception $e) {
$conn->close();
if ($this->debug) { echo "Client error: {$conn->resourceId} | {$e->getMessage()}\r\n"; }
}
// (C6) ON RECEIVING MESSAGE FROM CLIENT
public function onMessage (ConnectionInterface $from, $msg) {
if ($this->debug) { echo "Received message from {$from->resourceId}: {$msg}\r\n"; }
$msg = json_decode($msg, true);
switch ($msg["r"]) {
// (C6-1) INVALID REQUEST
default: break;
// (C6-2) ADMIN - UPDATE QUEUE NUMBER REQUEST
case "getQ":
$from->send(json_encode([
"r" => "putQ",
"qt" => $this->qt,
"qn" => $this->qn
]));
break;
// (C6-3) ISSUE A QUEUE NUMBER
case "issQ":
$this->qt++;
$this->putAllQ();
break;
// (C6-4) NEXT CUSTOMER
case "nextQ":
if ($this->qn < $this->qt) {
$this->qn++;
$this->putAllQ();
}
break;
}
}
// (C7) PUSH QUEUE UPDATE TO ALL CLIENTS
public function putAllQ () {
$msg = json_encode([
"r" => "putQ",
"qt" => $this->qt,
"qn" => $this->qn
]);
foreach ($this->clients as $client) { $client->send($msg); }
}
}
// (D) WEBSOCKET QUEUE SERVER START!
$server = IoServer::factory(new HttpServer(new WsServer(new Queue())), 8080); // @CHANGE if not port 8080
$server->run();
var queue = {
// (A) PROPERTIES
host : "ws://localhost:8080", // @CHANGE to your own!
socket : null, // websocket object
// (B) CONNECT TO WS SERVER
connect : opt => {
// (B1) CREATE WEB SOCKET
queue.socket = new WebSocket(queue.host);
// (B2) READY - CONNECTED TO SERVER
if (opt.open) { queue.socket.onopen = opt.open; }
// (B3) ON CONNECTION CLOSE
if (opt.close) { queue.socket.onclose = opt.close; }
// (B4) ON RECEIVING DATA FROM SEREVER
if (opt.msg) { queue.socket.onmessage = opt.msg; }
// (B5) ON ERROR
if (opt.error) { queue.socket.onerror = opt.error; }
},
// (C) SEND MESSAGE
send : (req, data) => {
if (data === undefined) { data = null; }
queue.socket.send(JSON.stringify({ r: req, d: data }));
}
};
<!DOCTYPE html>
<html>
<head>
<title>Queue Admin</title>
<script src="2-queue.js"></script>
<script src="3b-admin.js"></script>
<link rel="stylesheet" href="3c-admin.css">
</head>
<body>
<!-- (A) SERVER STATUS -->
<div id="server" class="red">Disconnected</div>
<!-- (B) CURRENT & TOTAL QUEUE NUMBER -->
<div id="qTotal">
<div class="qTxt">TOTAL</div>
<div class="qNum">0</div>
</div>
<div id="qNow">
<div class="qTxt">NOW</div>
<div class="qNum">0</div>
</div>
<!-- (C) INSTRUCTIONS -->
<div id="instruct">
Tap on "total" to issue a queue number.<br>
Tap on "now" to move to the next queue number.
</div>
</body>
</html>
var adm = {
// (A) PROPERTIES
connected : false, // connected to websocket
ready : false, // app ready
hQT : null, // html queue total
hQN : null, // html queue now
hSS : null, // html server status
// (B) ADMIN INIT
init : () => {
// (B1) GET HTML ELEMENTS
adm.hQT = document.querySelector("#qTotal .qNum");
adm.hQN = document.querySelector("#qNow .qNum");
adm.hSS = document.getElementById("server");
// (B2) ATTACH CONTROLS
document.getElementById("qTotal").onclick = adm.issQ;
document.getElementById("qNow").onclick = adm.nextQ;
// (B3) CONNECT TO WEBSOCKET
queue.connect({
// (B3-1) ON OPEN/CLOSE/ERROR
open : e => adm.toggle(true),
close : e => adm.toggle(false),
error : e => adm.toggle(false),
// (B3-2) INIT QUEUE NUMBER
msg : e => {
try {
let res = JSON.parse(e.data);
if (res.r=="putQ") { adm.putQ(res.qt, res.qn); }
} catch (e) {
alert("Init error");
console.error(e);
}
}
});
},
// (C) TOGGLE CONNECTION STATUS
toggle : good => {
if (good) {
adm.connected = true;
adm.hSS.className = "green";
adm.hSS.innerHTML = "Connected";
adm.getQ();
} else {
adm.connected = false;
adm.ready = false;
adm.hSS.className = "red";
adm.hSS.innerHTML = "Disconnected";
}
},
// (D) REQUEST QUEUE NUMBERS UPDATE FROM SERVER
getQ : () => { if (adm.connected) {
queue.send("getQ");
}},
// (E) UPDATE HTML QUEUE NUMBERS
putQ : (qt, qn) => { if (adm.connected) {
adm.hQT.innerHTML = qt;
adm.hQN.innerHTML = qn;
adm.ready = true;
}},
// (F) ISSUE QUEUE NUMBER
issQ : () => { if (adm.ready) {
queue.send("issQ");
}},
// (G) NEXT!
nextQ : () => { if (adm.ready) {
queue.send("nextQ");
}}
};
window.onload = adm.init;
/* (A) WHOLE PAGE */
* {
font-family: Arial, Helvetica, sans-serif;
box-sizing: border-box;
}
body {
padding: 20px; margin: 0;
width: 100vw; height: 100vh;
color: #fff;
background: #000;
max-width: 640px;
}
/* (B) SERVER STATUS */
#server {
padding: 0 0 0 10px;
margin-bottom: 10px;
}
#server.red {
border-left: 5px solid #f00;
color: #ed0000;
}
#server.green {
border-left: 5px solid #198512;
color: #198512;
}
/* (C) QUEUE STATUS */
#qTotal, #qNow {
display: flex;
padding: 40px 20px;
font-size: 40px;
cursor: pointer;
}
#qTotal {
color: #d1e1ff;
background: #101381;
}
#qNow {
color: #ffded1;
background: #4e0909;
}
.qTxt, .qNum { font-family: Impact, 'Arial Narrow Bold', sans-serif !important; }
.qTxt { width: 120px; }
/* (D) INSTRUCTION */
#instruct {
margin-top: 10px;
font-size: 0.9em;
}
<!DOCTYPE html>
<html>
<head>
<title>Queue Display Board</title>
<script src="2-queue.js"></script>
<script src="4b-board.js"></script>
<link rel="stylesheet" href="4c-board.css">
</head>
<body>
<!-- (A) CLICK TO START -->
<div id="start" onclick="board.start()">START</div>
<!-- (B) DISPLAY BOARD -->
<div id="board">
<!-- (B1) SERVER STATUS -->
<div id="server" class="red">Disconnected</div>
<!-- (B2) CURRENT QUEUE NUMBER -->
<div id="qTxt">NOW SERVING</div>
<div id="qNum">0</div>
</div>
</body>
</html>
var board = {
// (A) PROPERTIES
connected : false, // connected to websocket
aDD : null, // ding dong audio
hQN : null, // html queue now
hSS : null, // html server status
// (B) INIT QUEUE BOARD
start : () => {
// (B1) PRELOAD AUDIO
board.aDD = new Audio("ding-dong.mp3");
// (B2) GET + TOGGLE HTML ELEMENTS
board.hSS = document.getElementById("server");
board.hQN = document.getElementById("qNum");
document.getElementById("start").style.display = "none";
document.getElementById("board").style.display = "block";
// (B3) CONNECT TO WEBSOCKET
queue.connect({
// (B3-1) ON OPEN/CLOSE/ERROR
open : e => board.toggle(true),
close : e => board.toggle(false),
error : e => board.toggle(false),
// (B3-2) INIT QUEUE NUMBER
msg : e => {
try {
let res = JSON.parse(e.data);
if (res.r=="putQ") { board.putQ(res.qn); }
} catch (e) {
alert("Init error");
console.error(e);
}
}
});
},
// (C) TOGGLE CONNECTION STATUS
toggle : good => {
if (good) {
board.connected = true;
board.hSS.className = "green";
board.hSS.innerHTML = "Connected";
board.getQ();
} else {
board.connected = false;
board.hSS.className = "red";
board.hSS.innerHTML = "Disconnected";
}
},
// (D) REQUEST QUEUE NUMBERS UPDATE FROM SERVER
getQ : () => { if (board.connected) {
queue.send("getQ");
}},
// (E) UPDATE HTML QUEUE NUMBERS
putQ : qn => { if (board.connected && board.hQN.innerHTML!=qn) {
board.hQN.innerHTML = qn;
board.aDD.currentTime = 0;
board.aDD.play();
}}
};
/* (A) WHOLE PAGE */
* {
font-family: Arial, Helvetica, sans-serif;
box-sizing: border-box;
}
body {
display: flex;
align-items: center;
justify-content: center;
padding: 0; margin: 0;
width: 100vw; height: 100vh;
background: #000;
}
/* (B) START */
#start {
font-size: 32px;
color: #fff;
padding: 20px;
background: #a92222;
cursor: pointer;
}
/* (C) DISPLAY BOARD */
#board { display: none; }
/* (D) SERVER STATUS */
#server {
padding: 0 0 0 10px;
margin-bottom: 5px;
}
#server.red {
border-left: 5px solid #f00;
color: #ed0000;
}
#server.green {
border-left: 5px solid #198512;
color: #198512;
}
/* (E) QUEUE STATUS */
#queue {
text-align: center;
text-transform: uppercase;
}
#qTxt, #qNum {
font-family: Impact, 'Arial Narrow Bold', sans-serif !important;
}
#qTxt {
color: #fff;
font-size: 32px;
}
#qNum {
color: #ff0909;
font-size: 100px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment