Customer Queue Number System With PHP (Free Download)

Welcome to a tutorial on how to create a customer queue number system with PHP. Need to run an online queue number system? Or maybe in an intranet setting? Well, here’s a simple example built with WebSockets and PHP – Read on!

ⓘ I have included a zip file with all the source code at the start of this tutorial, so you don’t have to copy-paste everything… Or if you just want to dive straight in.

 

 

TABLE OF CONTENTS

Download & Notes PHP Queue Useful Bits & Links
The End

 

DOWNLOAD & NOTES

Firstly, here is the download link to the example code as promised.

 

QUICK NOTES

  • Download and unzip into your HTTP folder.
  • Open the command line and navigate to your HTTP folder.
  • Install Composer if you have not already done so. Run composer require cboden/ratchet to install Ratchet – It will be downloaded into vendor/ in your HTTP folder.
  • Run php 1-server.php.
  • That’s all. Access 3a-admin.html in the browser for the “admin page”, and 4a-board.html for the “queue board”.
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.

 

EXAMPLE CODE DOWNLOAD

Click here to download all the example source code, I have released it under the MIT license, so feel free to build on top of it or use it in your own project.

 

 

PHP QUEUE NUMBER SYSTEM

All right, let us now get into more details on how to create a queue number system in PHP.

 

PRELUDE) SETUP OVERVIEW

Before we start, here’s a quick overview of the system structure. There are 3 components:

  • 1-server.php Queue management WebSocket.
  • 3a-admin.html The “admin page” to issue queue numbers, advance to the next customer.
  • 4a-board.html The “current queue number” display board.

Yes, this example can run on a single device or as many as you want.

  • Single device, but requires 2 screens. One for the admin, another for the display board.
  • Multiple devices. One to act as the server, as many admin and display boards as you like.

 

1) WEBSOCKET SERVER

1A) COMMAND LINE CHECK

1-server.php
<?php
// (A) COMMAND LINE (CLI) CHECK
// CREDITS : https://www.binarytides.com/php-check-running-cli/
 
// (A1) ONLY CLI HAS SCRIPT ARGUMENTS
// WILL BE EMPTY ARRAY EVEN IF NONE PASSED IN
// IS RELIABLE ENOUGH, BUT MORE CHECKS BELOW FOR "SAFETY"
$iscli = isset($_SERVER["argv"]);
 
// (A2) STANDARD INPUT
// STDIN IS PREDEFINED IN CLI, BUT MAY NOT BE SO WHEN RUN IN CRON
if (!$iscli) { $iscli = defined("STDIN"); }
 
// (A3) PHP SAPI NAME
// IS "CLI" WHEN RUN FROM TERMINAL, BUT THIS MAY ALSO BE "CGI-FCGI"
if (!$iscli) { $iscli = php_sapi_name()==="cli"; }
 
// (A4) CLI - ABSENCE OF REMOTE SERVER VARS
// MAY NOT ALWAYS BE SO, SOME FREAK MAY "LOCALHOST" THIS DESPITE BEING RUN FROM CLI
if (!$iscli) { $iscli = !isset($_SERVER["REMOTE_ADDR"]) && !isset($_SERVER["HTTP_USER_AGENT"]); }
 
// (A5) NOPE
if (!$iscli) { exit("Please run this script in the command line."); }

First, this script drives the mechanics behind the queue number system. Take note that it should be run in the command line… The first section makes sure it is so, with some “rather extensive checks”.

 

 

1B) LOAD RATCHET

1-server.php
// (B) LOAD RATCHET
require "vendor/autoload.php";
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;

For you guys who are new:

  • Ratchet is a PHP library for WebSockets.
  • WebSockets are basically persistent connections.

Long story short, it is very difficult to drive a live queue system using the “connect download disconnect” HTTP. Thus the use of WebSockets.

 

1C) QUEUE CLASS

1-server.php
// (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) {
    // (C6-0) JSON DECODE + DEBUG
    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 () {
    // (C7-1) QUEUE MESSAGE
    $msg = json_encode([
      "r" => "putQ",
      "qt" => $this->qt,
      "qn" => $this->qn
    ]);
 
    // (C7-2) SEND TO ALL
    foreach ($this->clients as $client) { $client->send($msg); }
  }
}

Oh no. Big scary library.

  • (C1 to C5) When a client connects, we store the connection in $this->clients; Remove it when the client disconnects.
  • (C6 & C7) This is the “core” of the queue system.
    • (C1) $this->qt Total queue number. $this->qn Current queue number.
    • (C6-3) When the admin issues a queue number, $this->qt++. Push updated queue numbers to all clients.
    • (C6-4) When the admin hits “next customer”, $this->qn++. Push updated queue numbers to all clients.

That’s all.

 

1D) START WEBSOCKET SERVER

1-server.php
// (D) WEBSOCKET SERVER START!
$server = IoServer::factory(new HttpServer(new WsServer(new Queue())), 8080); // @CHANGE if not port 8080
$server->run();

Don’t think this needs explanation.

 

2) WEBSOCKET JAVASCRIPT “LIBRARY”

2-ws.js
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
    }));
  }
};

Next, we have a “Javascript wrapper” to work with WebSockets. This is more for convenience, so that we won’t have duplicated code later on.

 

3) ADMIN PAGE

3A) ADMIN HTML

3a-admin.html
<!-- (A) SERVER STATUS -->
<div id="server" class="red">
  <div id="scolor"></div>
  <div id="stxt">Disconnected</div>
</div>
 
<!-- (B) CURRENT & TOTAL QUEUE NUMBER -->
<div id="queue">
  <div id="qtotal">
    <div class="qtxt">Total</div>
    <div class="qnum"></div>
  </div>
  <div id="qnow">
    <div class="qtxt">Now</div>
    <div class="qnum"></div>
  </div>
</div>
 
<!-- (C) INSTRUCTIONS -->
<div id="inst">
  Tap on "total" to issue a queue number.<br>
  Tap on "now" to move to the next queue number.
</div>

This should be pretty straightforward, there are only 3 sections on this page.

  • <div id="server> The current server status, is connected or disconnected.
  • <div id="qtotal"> <div id="qnow"> Total queue number, and the current queue number.
  • <div id="inst"> Short instructions.

 

3B) ADMIN JAVASCRIPT

3b-admin.js
var adm = {
  // (A) PROPERTIES
  // (A1) SERVER STATUS
  connected : false,
  ready : false,

  // (A2) HTML ELEMENTS
  sc : null, st : null, // server status
  qt : null, qtn : null, // queue total
  qn : null, qnn : null, // queue now

  // (B) ADMIN INIT
  init : () => {
    // (B1) GET HTML ELEMENTS
    adm.sc = document.getElementById("server");
    adm.st = document.getElementById("stxt");
    adm.qt = document.getElementById("qtotal");
    adm.qtn = document.querySelector("#qtotal .qnum");
    adm.qn = document.getElementById("qnow");
    adm.qnn = document.querySelector("#qnow .qnum");

    // (B2) ATTACH CONTROLS
    adm.qt.onclick = adm.issQ;
    adm.qn.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); },
      msg : (e) => {
        // (B3-2) JSON DECODE SERVER MESSAGE
        let res = JSON.parse(e.data);
        switch (res.r) {
          // (B3-3) INVALID
          default: break;

          // (B3-3) UPDATE HTML QUEUE NUMBERS
          case "putQ":
            adm.putQ(res.qt, res.qn);
            break;
        }
      }
    });
  },

  // (C) TOGGLE CONNECTION STATUS
  toggle : (good) => {
    if (good) {
      adm.connected = true;
      adm.sc.className = "green";
      adm.st.innerHTML = "Connected";
      adm.getQ();
    } else {
      adm.connected = false;
      adm.ready = false;
      adm.sc.className = "red";
      adm.st.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.qtn.innerHTML = qt;
    adm.qnn.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;

Yikes. Big scary script once again.

  • (B) adm.init() runs on page load. Gets all the HTML elements, connect to the WebSocket server, and updates the current queue count.
  • (F) When the admin clicks on “issue queue number”, this simply sends issQ to the server to update the total count.
  • (B3-3) The server updates the total queue count and mass sends the updated count.
  • (G & B3-3) The same pretty much happens when the admin clicks on “next customer”.

 

4) DISPLAY BOARD

4A) DISPLAY BOARD HTML

4a-board.html
<!-- (A) SERVER STATUS -->
<div id="server" class="red">
  <div id="scolor"></div>
  <div id="stxt">Disconnected</div>
</div>
 
<!-- (B) CURRENT & TOTAL QUEUE NUMBER -->
<div id="qnow">
  <div class="qtxt">Serving Now</div>
  <div class="qnum"></div>
</div>

A simplified version of the admin panel, only showing the connection status and current queue number.

 

 

4B) DISPLAY BOARD JAVASCRIPT

4b-board.js
var board = {
  // (A) PROPERTIES
  // (A1) SERVER STATUS
  connected : false,
  ready : false,

  // (A2) HTML ELEMENTS
  dd : null, // ding dong audio
  sc : null, st : null, // server status
  qnn : null, // queue now

  // (B) ADMIN INIT
  init : () => {
    // (B1) PRELOAD AUDIO
    board.dd = new Audio("ding-dong.mp3");

    // (B2) GET HTML ELEMENTS
    board.sc = document.getElementById("server");
    board.st = document.getElementById("stxt");
    board.qnn = document.querySelector("#qnow .qnum");

    // (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); },
      msg : (e) => {
        // (B3-2) JSON DECODE SERVER MESSAGE
        let res = JSON.parse(e.data);
        switch (res.r) {
          // (B3-3) INVALID
          default: break;

          // (B3-3) UPDATE HTML QUEUE NUMBERS
          case "putQ":
            board.putQ(res.qn);
            break;
        }
      }
    });
  },

  // (C) TOGGLE CONNECTION STATUS
  toggle : (good) => {
    if (good) {
      board.connected = true;
      board.sc.className = "green";
      board.st.innerHTML = "Connected";
      board.getQ();
    } else {
      board.connected = false;
      board.sc.className = "red";
      board.st.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) {
    if (board.qnn.innerHTML != qn) {
      board.qnn.innerHTML = qn;

      // ANNOYING - WILL NOT PLAY UNTIL YOU CLICK ON THE PAGE ONCE
      board.dd.currentTime = 0;
      board.dd.play();
    }
  }}
};

window.onload = board.init;

Look no further, this is actually a simplified version of the admin.

 

USEFUL BITS & LINKS

That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.

 

A COUPLE OF IMPROVEMENTS

Yes, this is only a working example and foundation. There are many things you can add and customize to make it better.

  • Define your own “reset queue number” algorithm… If not, it will run to very large numbers in due time.
  • Probably makes sense to save the current queue number into a database or file. The queue number will not be lost when the server crashes.
  • Also possible to tie in an existing user database – Send notifications when the customer’s turn nears.
  • Not much of an issue if this example is within an intranet. But various security checks need to be in place if you want to put this online.

 

COMPATIBILITY CHECKS

A modern “Grade A” browser is required for this example to work correctly.

 

LINKS & REFERENCES

 

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!

Leave a Comment

Your email address will not be published.