How To Build a Real-time Chat Application With PHP

PHP Realtime Chat Application

Because of its lack of native support, PHP is rarely mentioned when discussing WebSockets. Furthermore, Apache, the HTTP server on which PHP is typically run, was not designed with persistent connections in mind, putting the burden of implementation on third-party libraries.

In this tutorial, we will be using a PHP library for WebSocket communications called Ratchet.

How do Websockets work?

During the initial setup, WebSockets use HTTP to establish TCP-style connections in a browser-compatible manner. Websocket messages can be sent in any protocol, freeing the application from the sometimes unnecessary overhead of HTTP requests and responses (including headers, cookies, and other artifacts). However, the ability to deliver downstream (server-to-client) messages to coworkers are critical.

What are you going to need in this tutorial?

  • PHP Installed locally
  • XAMPP
  • COMPOSER

Getting started

First, you need to download XAMPP here https://www.apachefriends.org

then, download Composer from this link https://getcomposer.org

Go to your XAMPP folder after installation, inside the htdocs folder, create a new folder with the name “chat” or whatever you want.

Inside your project folder, open the terminal and type the following command to install ratchet.

php ~/composer.phar require cboden/ratchet

That will install Ratchet, and your project directory should look something like this.

htdocs
├── chat
│       ├── vendor
│       ├── composer.json
│       ├── composer.lock

Open the composer.json file and paste this code into it.

{
    "autoload": {
        "psr-4": {
            "MyApp\\": "src"
        }
    },
    "require": {
        "cboden/ratchet": "^0.4"
    }
}

We use the PSR-4 protocol for the autoloader, and MyApp is mapped to the app folder that we created in our project setup. In the following steps, we will use this namespace to include our project classes. Our composer.json uses the required key to specify the Ratchet package in our project, as is required by default.

After we’ve created our composer.json file, we need to install it;

run this command to install it.

$ composer install

The Websocket server file.

Next, we need two important files to run our WebSocket server and start implementing the app logic.

In your project’s root directory, create a new folder called “bin” and create a new file “server.php” Inside it.

Add this code to the server.php file :

<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use MyApp\Chat;

    require dirname(__DIR__) . '/vendor/autoload.php';

    $server = IoServer::factory(
        new HttpServer(
            new WsServer(
                new Chat()
            )
        ),
        8080
    );

    $server->run();
?>

This code by Ratchet creates a WebSocket server that listens for websocket connections on port 8080.

In the root directory of the project, use this command to run the server

php bin/server.php

The Chat Class

This will be the core logic of our application, and it will handle all of the WebSocket communication between the browser and our server.

Create a new folder and call it “src” and create a new file inside it and call it “Chat.php”

Notice: the folder name must be “src” because ratchet uses this to access it. but the class name can be whatever you want!

It contains four functions :

  • onOpen: Runs when a new connection is connected to the server.
  • onMessage: Runs when a new message is sent by a client.
  • onClose: Runs when a connection is lost.
  • onError: Runs whenever any error occurs.
<?php
namespace MyApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class Chat implements MessageComponentInterface {
    protected $clients;
    public function __construct() {
        $this->clients = new \SplObjectStorage;
        echo "server started";
    }

    public function onOpen(ConnectionInterface $conn) {

          //New connection
    }

    public function onMessage(ConnectionInterface $from, $msg) {
         //New message
    }

    public function onClose(ConnectionInterface $conn) {
         //Connection closed
    }


    public function onError(ConnectionInterface $conn, \Exception $e) {
         //Error 
    }

?>

Creating the database

I’m going to use MySQL as a database for this project. Go to localhost/PHPMyAdmin after starting your XAMPP apache and mysql servers and create a new database. I’ll call it “chat”.

We only need two tables, one for the users and the other for the messages.

the users’ table structure is as follows:

  • id, INT(11), Primary key, Auto Increment
  • name, VARCHAR(60)
  • email, VARCHAR(250)
  • password, VARCHAR(250)
  • chat_token, VARCHAR(500)
  • conn_id, VARCHAR(60)
  • is_online, BOOLEAN

The messages table structure is as follows:

  • from_id, INT(11)
  • to_id, INT(11)
  • body, VARCHAR(500)

Connecting to the database

Create a new file in the root directory of the project and call it “config.php”.

use Php PDO class to connect to MySQL database, for more information about PDO https://www.php.net/manual/en/book.pdo.php

<?php
ob_start();
session_start();
date_default_timezone_set("Africa/Cairo");

class DB {
    public static $con;

    public static function connect() {
        self::$con = new PDO("mysql:dbname=chat;host=localhost", "root", "");
        self::$con->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
        return self::$con;
    } 
}

try {
    DB::connect();
}
catch (PDOException $e) {
   exit('Something went wrong, Please try again later');
}

?>

Register Users

Create a new file “register.php” and create HTML form with 5 inputs for name, email, email verification, password, and password verification.

Output the errors in the errors array (We will create that when we start parsing the form data) right above the form element.

<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="style.css">
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap" rel="stylesheet">
</head>

<body>
    <div class="form-body">
        <div class="form-header">
            <h1>Create A New Account</h1>
        </div>
        <?php 
            if(isset($_POST["register"])) {
                if(!empty($errors)) {
                    echo "<div class='errors'>";
                    foreach($errors as $error) {
                        echo $error;
                    }
                    echo "</div>";
                }
            } 
        ?>
        <form action="register.php" method="POST">
            <input type="text" id="name" class="input" name="name" value="<?php lastValue('name') ?>" placeholder="First Name" autocomplete="off" required>
            <input type="email" id="email" class="input" name="email" value="<?php lastValue('email') ?>" placeholder="Email" autocomplete="off" required>
            <input type="email" id="email2" class="input" name="email2" value="<?php lastValue('email') ?>" placeholder="Confirm Email" autocomplete="off" required>
            <input type="password" id="password" class="input" name="password" placeholder="Password" autocomplete="off" required>
            <input type="password" id="password2" class="input" name="password2" placeholder="Confirm Password" autocomplete="off" required>
            <input type="submit" id="register" class="btn" name="register" value="Register">
        </form>
        <a href="login.php" class="form-text">Already Have an Account? Login here!</a>
    </div>
</body>
</html>

Parsing register form data

First, we need to create some validation functions to validate user input data for errors.

Create a new file “functions.php”

<?php

function lastValue($input) {
    if(isset($_POST[$input])) {
        echo $_POST[$input];
    }
}
function clear($text) {
    $text = strip_tags($text);
    $text = trim($text);
    $text = htmlspecialchars($text);
    return $text;
}
function clearText($text) {
    $text = clear($text);
    $text = str_replace(" ", "", $text);
    $text = strtolower($text);
    $text = ucfirst($text);
    return $text;
}
function clearEmail($email) {
    $email = str_replace(" ", "", $email);
    $email = clear($email);
    return $email;
}
function validateName($name) {
    $errors = array();
    if(strlen($name) > 25 || strlen($name) < 2) {
        array_push($errors, "<span class='error' data-errorType='name'>Your first name must be between 2 and 25 character</span>");
    }
    return $errors;
}
function validateLastName($lastName) {
    $errors = array();
    if(strlen($lastName) > 25 || strlen($lastName) < 2) {
        array_push($errors, "<span class='error' data-errorType='lastName'>Your last name must be between 2 and 25 character</span>");
    }
    return $errors;
}
function validateEmail($con, $email, $confirmation) {
    $errors = array();
    if($email != $confirmation) {
        array_push($errors, "<span class='error' data-errorType='email'>Your emails do not match</span>");
        return $errors;
    }
    if(!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        array_push($errors, "<span class='error' data-errorType='email'>Please enter a valid email adress</span>");
        return $errors;
    }

    $query = $con->prepare("SELECT email FROM users WHERE email=:em");
    $query->bindParam(":em", $email);
    $query->execute();

    if($query->rowCount() != 0) {
        array_push($errors, "<span class='error' data-errorType='email'>This Email is already exists</span>");
        return $errors;
    } 
    return $errors;
}
function validatePasswords($password, $confirmation) {
    $errors = array();
    if($password != $confirmation) {
        array_push($errors, "<span class='error'>Your Passwords do not match</span>");
        return $errors;
    }
    if(preg_match("/[^A-za-z0-9]/", $password)) {
        array_push($errors, "<span class='error'>Your password can only contain numbers and letters</span>");
        return $errors;
    }
    if(strlen($password) > 30 || strlen($password) < 5) {
        array_push($errors, "<span class='error'>Your password must be between 5 and 30 characters</span>");
        return $errors;
    }
    return $errors;
}

?>

The lastValue function is used to print the old data the user submitted in case of an error so the user won’t have to write it all again, but not the password.

The rest of the functions use PHP built-in validation functions to clear and filter the input data, you should push any errors to the errors array and return that array at the end of each function.

At the top of “register.php” open PHP tags and let’s start to parse the form data.

<?php
require_once("config.php");
require_once("functions.php");

if(isset($_POST["register"])) {
    $errors = array();

    $name = clearText($_POST["name"]);
    $email = clearEmail($_POST["email"]);
    $confirmEmail = clearEmail($_POST["email2"]);
    $password = clear($_POST["password"]);
    $confirmPassword = clear($_POST["password2"]);

    $con = DB::connect();

    if(!empty(validateName($name))) {
        foreach(validateName($name) as $err) {
            $errors[] = $err;
        }    
    }

    if(!empty(validateEmail($con, $email, $confirmEmail))) {
        foreach(validateEmail($con, $email, $confirmEmail) as $err) {
            $errors[] = $err;
        }    
    }

    if(!empty(validatePasswords($password, $confirmPassword))) {
        foreach(validatePasswords($password, $confirmPassword) as $err) {
            $errors[] = $err;
        }    
    }

    if(empty($errors)) {
        $hashedPassword = password_hash($password, PASSWORD_DEFAULT);
        $query = $con->prepare("INSERT INTO users (`name`, `email`, `password`)
        VALUES (:name, :email, :pw)");

        if($query->execute([
            'name' => $name,
            'email' => $email,
            'pw' => $hashedPassword,
        ])) {
            $_SESSION["userLoggedIn"] = $email;
            $_SESSION["chatToken"] = md5(uniqid());
            header('Location: /chat/home.php');
        }
    }
}

?>

Use the validation functions on its related inputs and push the errors that come from each function to the errors array we created on this page.

Finally, check if the errors array is empty, which means the user entered all the data correctly, and we should register that user, insert the data into the database and when it is done save the user email in a session variable called “userLoggedIn” and generate a new chat token with Php built-in functions “md5” and “uniqid”. and redirect the user to the home.php page, which we will create a bit later.

Login

Login is the same functionality as the register, except we are going to check for the email and verify the password then compare them to the user data and on success, we want to do the same three things we did when we registered users successfully.

  • Save the email in a session variable $_SESSION[“userLoggedIn”]
  • generate a new chat token with md5 and uniqid and save it in a session variable $_SESSION[“chatToken”]
  • redirect the user to the home page
<?php
require_once("config.php");
require_once("functions.php");


if(isset($_POST["login"])) {
    $email = clearEmail($_POST["email"]);
    $password = clear($_POST["password"]);
    $errors = array();


    $query = DB::connect()->prepare("SELECT * FROM users WHERE email=:em");
    $query->bindParam("em", $email);

    $query->execute();
    $sqlData = $query->fetch(PDO::FETCH_ASSOC);

    if($query->rowCount() === 1) {
        if(password_verify($password, $sqlData["password"])) {
           $_SESSION["userLoggedIn"] = $email;
           $_SESSION["chatToken"] = md5(uniqid());
           header('Location: /chat/home.php');
        }
        else {
            $errors[] = "<span class='error'>Your email or password was incorrect</span>";
        }

    }
    else {
        $errors[] = "<span class='error'>Your email or password was incorrect</span>";
    }
}

?>
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="style.css">
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap" rel="stylesheet">
</head>

<body>
    <div class="form-body">
        <div class="form-header">
            <h1>Login</h1>
        </div>
        <?php 
            if(isset($_POST["login"])) {
                if(!empty($errors)) {
                    echo "<div class='errors'>";
                    foreach($errors as $error) {
                        echo $error;
                    }
                    echo "</div>";
                }
            } 
        ?>
        <form action="login.php" method="POST">
            <input type="email" id="email" class="input" name="email" value="<?php lastValue('email') ?>" placeholder="Email" autocomplete="off" required>
            <input type="password" id="password" class="input" name="password" placeholder="Password" autocomplete="off" required>
            <input type="submit" id="login" class="btn" name="login" value="Login">
        </form>
        <a href="register.php" class="form-text">Don't have an account? Create a new one here!</a>
    </div>
</body>
</html>

The Home Page

This is the main page which will contain the users and chat. We want two sections on the home page, one for the chat and a side section for online users.

<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="style.css">
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap" rel="stylesheet">
</head>

<body>
    <div class="main-chat-container">
        <div class="chat-container">
            <div class="chat-header">
                <span id="otherUserName">Welcome To Chat</span>
            </div>
            <div id="chatArea" class="chat">
                Please Select A User To Chat With
            </div>
            <div class="input-container">
                <textarea id="chatMessage" name="chatMessage" value="" rows="3" cols="50" placeholder="Type your message here"></textarea>
                <button class="chat-submit" onclick="sendMessage()">Send</button>
            </div>
        </div>
        <div id="users" class="users-container">
            <span class="users-header">Online Users</span>
        </div>
    </div>
</body>
</html>

add the styling in a style.css file in the root directory.

html, body, * {
    margin: 0;
    padding: 0;
    font-family: 'Roboto', sans-serif;
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
}
body {
    width: 100%;
    min-height: 100vh;
    background: #f1f1f1;
    display: flex;
    align-items: center;
    justify-content: center;
}
form {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
}
input, button, input:focus, button:focus, input:active, button:active, textarea, textarea:focus, textarea:active {
    border: none;
    outline: none;
}
.form-body {
    width: 500px;
    height: auto;
    background: #fff;
    border: 1px solid #dcdcdc;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 15px;
    border-radius: 8px;
}
.form-header {
    width: 100%;
    height: auto;
    display: flex;
    justify-content: flex-start;
}
.form-header h1 {
    font-size: 22px;
    font-weight: 500;
    color: #666666;
    margin-bottom: 15px;
}
.errors {
    width: 100%;
    height: auto;
    display: flex;
    flex-direction: column;
    padding: 5px;
    margin-bottom: 5px;
}
.input {
    width: 100%;
    height: 35px;
    background: transparent;
    margin: 5px 0px;
    padding: 0px 10px;
    font-size: 16px;
    color: #333;
    border-bottom: 1px solid #dcdcdc;
}
.input:focus {
    border-bottom: 1px solid #dcdcdc;
}
.btn {
    font-size: 18px;
    color: white;
    background-color: #4343ff;
    padding: 10px 30px;
    border: none;
    outline: none;
    border-radius: 3px;
    margin: 10px 0;
    cursor: pointer;
}
.btn:hover {
    background-color: #5f5ff9;
}
.form-text {
    font-size: 13px;
    font-weight: 400px;
    color: #404040;
    text-decoration: none;
}
.form-text:hover {
    text-decoration: underline;
}
.errors {
    width: 100%;
    height: auto;
    background: rgb(170, 13, 13);
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: flex-start;
    padding: 5px;
}
.error {
    font-size: 15px;
    color: #f1f1f1;
}
.main-chat-container {
    width: 1000px;
    height: 600px;
    padding: 20px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.chat-container {
    width: 60%;
    height: 100%;
    background-color: #fff;
    border: 1px solid #dcdcdc;
    border-radius: 5px;
    margin-right: 10px;
    display: flex;
    flex-direction: column;
}
.chat-header {
    width: 100%;
    height: 50px;
    background-color: #3e3ef7;
    color: #fff;
    font-size: 20px;
    padding: 5px 20px;
    display: flex;
    justify-content: flex-start;
    align-items: center;
}
.chat {
    width: 100%;
    flex: 1;
    padding: 5px;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
}
.msg-container {
    width: 100%;
    height: auto;
    padding: 5px;
    display: flex;
}
.blue-container {
    justify-content: flex-start;
}
.dark-container {
    justify-content: flex-end;
}
.msg {
    width: auto;
    height: auto;
    max-width: 70%;
    border-radius: 20px;
    padding: 10px;
    text-align: left;
    font-size: 16px;
    overflow: hidden;
    word-wrap: break-word;
}
.blue-container .msg {
    background-color: #3e3ef7;
    color: #fff;
}
.dark-container .msg {
    background-color: #ccc;
    color: #333;
}
.users-container {
    width: 33%;
    height: 100%;
    background-color: #fff;
    color: #333;
    border: 1px solid #dcdcdc;
    border-radius: 5px;
    padding: 15px;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: flex-start;
}
.users-header {
    font-size: 22px;
    font-weight: 700;
    color: #333;
    margin-bottom: 10px;
}
.user {
    font-size: 18px;
    color: #333;
    margin-bottom: 10px;
    cursor: pointer;
    height: auto;
    display: flex;
    justify-content: center;
    align-items: center;
}
.user:hover {
    text-decoration: underline;
}
.chat-container .input-container {
    width: 100%;
    padding: 5px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.chat-container .input-container textarea {
    border: 1px solid #dcdcdc;
    width: 70%;
    padding: 5px;
}
.chat-submit {
    width: 150px;
    height: auto;
    max-height: 90%;
    padding: 10px 20px;
    font-size: 20px;
    background-color: #3e3ef7;
    color: #fff;
    border-radius: 5px;
    cursor: pointer;
}
.msg-count {
    margin: 0px 5px;
    background: #3e3ef7;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    color: #fff;
    font-size: 12px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-weight: 700;
}
.hide {
    display: none;
}

Now the home page should look like this:

Protect the home page

Now we have to protect the home page so only logged-in users can visit it. At the top of home.php write this code and use the session variable we set before to determine if the user logged in or not.

<?php
require_once("config.php");

if(!isset($_SESSION["userLoggedIn"])) {
    header("Location: /chat/login.php");
    die();
}

?>

Connect to the websocket server from the client

<script>
const conn = new WebSocket('ws://localhost:8080?token=<?php echo $_SESSION["chatToken"].'&email='.$_SESSION["userLoggedIn"]?>');
conn.onopen = function(e) {
   console.log("Connection established!");
};
</script>

open a script tag at the bottom of the home.php page and use javascript native WebSocket library to connect to the WebSocket server. and use PHP to echo the email saved in $_SESSION[“userLoggedIn”] and also the chat token saved in $_SESSION[“chatToken”] as URL Params.

the connection variable “conn” now has the WebSocket object methods, use onopen method to console.log some text to confirm that the connection established.

Working on the WebSocket backend

We will use the Chat class and its four functions (onOpen, onMessage, onClose, onError) to handle communications between the browser and the server.

Handle new connections

Open the Chat class in the src folder and let’s start implementing the communication logic on the backend.

<?php
namespace MyApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class Chat implements MessageComponentInterface {
    protected $clients;
    private $con;

    public function __construct() {
        $this->clients = new \SplObjectStorage;
        echo "server started";
        $this->connect();
    }

    private function connect() {
        $this->con = new \PDO("mysql:dbname=chat;host=localhost", "root", "");
        $this->con->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_WARNING);
    }

First, we need to have a way to connect to the database from this class. Create a private function and establish a new connection using PDO and save it on a private variable of this class.

Next, when a new connection hits the server we need to collect the data about this user from the database using the email we passed to the URL of the WebSocket connection.

public function onOpen(ConnectionInterface $conn) {
    $connectionId = $conn->resourceId;
    $queryString = $conn->httpRequest->getUri()->getQuery();
    parse_str($queryString, $paramsArray);

    $userQuery = $this->con->prepare("SELECT * FROM users WHERE email = :em");
    $userQuery->bindParam(":em", $paramsArray["email"]);
    $userQuery->execute();

    $userData = $userQuery->fetch(\PDO::FETCH_ASSOC);

use onOpen function and parse the HTTP request Uri and get the URL params. then get the data from the users’ table using the user email.

Now we need to send a message to this user to inform him about his id on the database to use it for further communication with the server.

$conn->send(json_encode(array(
    "type" => "CONNECTION_ESTABLISHED",
    "connId" => $connectionId,
    "userId" => $userData["id"]
)));

Send another message to all the users connected to the server to inform them about the new user connection.

        foreach ($this->clients as $client) {
            $client->send(json_encode(array(
                "type" => "NEW_USER_CONNECTED",
                "name" => $userData["name"],
                "connId" => $connectionId,
                "userId" => $userData["id"]
            )));
        }

        $conn->userId = $userData["id"];

        $this->clients->attach($conn);

We send the message as a JSON string to be able to have a specific structure to handle on the front end by parsing it.

The type property will help us to identify what this message is about on the front end.

then add the database id as a property to the $conn which is the coming connection.

and add the coming connection to the whole clients’ array of this class.

Finally, We need to update the database and set this user to be online and also set the chat token and the WebSocket Connection id.

$isOnline = true;
$updateQuery = $this->con->prepare("UPDATE users SET chat_token = :token, conn_id = :con, is_online = :isOnline  WHERE email = :em");
$updateQuery->bindParam(":token", $paramsArray["token"]);
$updateQuery->bindParam(":con", $connectionId);
$updateQuery->bindParam(":isOnline", $isOnline, \PDO::PARAM_INT);
$updateQuery->bindParam(":em", $paramsArray["email"]);
$updateQuery->execute();

This is the whole onOpen function now :

public function onOpen(ConnectionInterface $conn) {
   $connectionId = $conn->resourceId;
   $queryString = $conn->httpRequest->getUri()->getQuery();
   parse_str($queryString, $paramsArray);

   $userQuery = $this->con->prepare("SELECT * FROM users WHERE email = :em");
   $userQuery->bindParam(":em", $paramsArray["email"]);
   $userQuery->execute();

   $userData = $userQuery->fetch(\PDO::FETCH_ASSOC);

   $conn->send(json_encode(array(
       "type" => "CONNECTION_ESTABLISHED",
       "connId" => $connectionId,
       "userId" => $userData["id"]
   )));

   foreach ($this->clients as $client) {
        $client->send(json_encode(array(
             "type" => "NEW_USER_CONNECTED",
             "name" => $userData["name"],
             "connId" => $connectionId,
             "userId" => $userData["id"]
        )));
   }

   $conn->userId = $userData["id"];

   $this->clients->attach($conn);

   $isOnline = true;
   $updateQuery = $this->con->prepare("UPDATE users SET chat_token = :token, conn_id = :con, is_online = :isOnline  WHERE email = :em");
   $updateQuery->bindParam(":token", $paramsArray["token"]);
   $updateQuery->bindParam(":con", $connectionId);
   $updateQuery->bindParam(":isOnline", $isOnline, \PDO::PARAM_INT);
   $updateQuery->bindParam(":em", $paramsArray["email"]);
   $updateQuery->execute();
}

Handling Upcoming Messages

Also, Upcoming messages sent from clients will be in JSON format and will be in the same structure.

public function onMessage(ConnectionInterface $from, $msg) {
    $msgData = json_decode($msg);

    foreach ($this->clients as $client) {
         if($client->userId == $msgData->toUserId || $client->userId == $msgData->fromUserId) {
              echo $client->userId;
              $client->send(json_encode(array(
                    "type" => "NEW_MESSAGE",
                    "fromConnectionId" => $from->resourceId,
                    "fromUserId" => $msgData->fromUserId,
                    "toConnectionId" => $msgData->toConnectionId,
                    "toUserId" => $msgData->toUserId,
                    "body" => $msgData->body
               )));
          }
    }

When a new message is sent, parse it using json_decode to transform the JSON string into a PHP object.

The new message will be sent from the front end holding these data toConnectionId, toUserId, fromUserId, body:

Then we need to send a message to the sender and the recipient to inform them about a message that was sent from one of them by looping through the clients’ array and check if the current client is the sender or the recipient by using their ids sent with the message.

finally, we need to save the new message in the database

$insertQuery = $this->con->prepare("INSERT INTO messages (`from_id`, `to_id`, `body`) VALUES (:from_id, :to_id, :body)");
$insertQuery->bindParam(":from_id", $msgData->fromUserId);
$insertQuery->bindParam(":to_id", $msgData->toUserId);
$insertQuery->bindParam(":body", $msgData->body);
$insertQuery->execute();

Now, this is the whole onMessage function:

public function onMessage(ConnectionInterface $from, $msg) {
     $msgData = json_decode($msg);

     foreach ($this->clients as $client) {
          if($client->userId == $msgData->toUserId || $client->userId == $msgData->fromUserId) {
               echo $client->userId;
               $client->send(json_encode(array(
                   "type" => "NEW_MESSAGE",
                   "fromConnectionId" => $from->resourceId,
                   "fromUserId" => $msgData->fromUserId,
                   "toConnectionId" => $msgData->toConnectionId,
                   "toUserId" => $msgData->toUserId,
                   "body" => $msgData->body
               )));
          }
     }

     $insertQuery = $this->con->prepare("INSERT INTO messages (`from_id`, `to_id`, `body`) VALUES (:from_id, :to_id, :body)");
     $insertQuery->bindParam(":from_id", $msgData->fromUserId);
     $insertQuery->bindParam(":to_id", $msgData->toUserId);
     $insertQuery->bindParam(":body", $msgData->body);

     $insertQuery->execute();
}

Handling Closed Connections and errors

    public function onClose(ConnectionInterface $conn) {
        $this->clients->detach($conn);

        $queryString = $conn->httpRequest->getUri()->getQuery();
        parse_str($queryString, $paramsArray);

        $closeQuery = $this->con->prepare("UPDATE users SET chat_token = :token, conn_id = :con, is_online = :isOnline  WHERE email = :em");
        $closeQuery->bindValue(':token', null, \PDO::PARAM_NULL);
        $closeQuery->bindValue(':con', null, \PDO::PARAM_NULL);
        $closeQuery->bindValue(':isOnline', false, \PDO::PARAM_INT);
        $closeQuery->bindValue(':token', null, \PDO::PARAM_NULL);
        $closeQuery->bindParam(":em", $paramsArray["email"]);
        $closeQuery->execute();

        foreach ($this->clients as $client) {
            $client->send(json_encode(array(
                "type" => "USER_DISCONNECTED",
                "connId" => $conn->resourceId,
                "userId" => $conn->userId
            )));
        }
    }

    public function onError(ConnectionInterface $conn, \Exception $e) {
        echo "An error has occurred: {$e->getMessage()}\n";
        $conn->close();
    }

onClose connection, we just need to get the user’s email from the URL the same way we did it on the onOpen function, and then update the database by setting this user to is_online to false and chat token and connection id to null.

then send a new message to all clients by using foreach loop to notify them about a user disconnected and attach with it, his connection id and his database user id to update the front end and remove him from the online user’s section.

onError, echo the error message and call the close method on the connection object which will call onClose eventually.

Working On The Front End

We need to declare some variables that will hold: the logged-in user connection id, the logged-in user database id, the other user connection id, the other user database id. and whether the user selected another user to chat with or not.

The WebSocket connection we saved in the javascript variable conn. Now this variable has some methods like onopen and onmessage we can use these functions to parse data coming from the server.

let currentConnectionId, currentUserId, toConnectionId, toUserId, userSelected;
const conn = new WebSocket('ws://localhost:8080?token=<?php echo $_SESSION["chatToken"].'&email='.$_SESSION["userLoggedIn"]?>');
conn.onopen = function(e) {
   console.log("Connection established!");
};

Handle new messages coming from the server

Remember the type that we sent it with every message to know what this message is about.

let’s use a switch statement against the type to know which case we handling.

conn.onmessage = function(e) {
    const data = JSON.parse(e.data);
    switch(data.type) {
        case "CONNECTION_ESTABLISHED":

        break;
        case "NEW_USER_CONNECTED": 
                
        break;
        case "USER_DISCONNECTED": 

        break;
        case "NEW_MESSAGE": 

        break;
    }
};

Case1: Connection Established

When the client connection with the server is established, the server responds with the client’s id from the database and other things.

So we need first to set the variables we declared to the user id and connection id.

Then we need to get all online users and add their names to the users div in the HTML template, for that we are going to use ajax to fetch a PHP page which I will create next that return all online users.

currentConnectionId = data.connId;
currentUserId = data.userId;
const usersContainer = document.getElementById("users");
fetch(`/chat/ajax/getOnlineUsers.php`).then(response => response.json()).then(res => {
    if(res.length === 0) return;
    res.forEach(user => {
         if(user.id === currentUserId) return;
         users.insertAdjacentHTML("beforeend", `
         <span id="${user.id}" class="user" onclick="initChat(${user.id}, ${user.conn_id},'${user.name}')">${user.name}<span id="msgCount" class="msg-count hide"></span></span>
`);
    });
}).catch(err => console.log(err));

Create a new folder “ajax” in the root directory and create a new file “getOnlineUsers.php”.

<?php
require_once("../config.php");

$results = array();
$usersQuery = DB::connect()->prepare("SELECT * FROM users WHERE is_online = :isOn");
$usersQuery->bindValue(':isOn', true, PDO::PARAM_INT);
$usersQuery->execute();

while($row = $usersQuery->fetch(PDO::FETCH_ASSOC)) {
    $results[] = $row;
}

echo json_encode($results);
?>

Case2: New User Connected

Add it to the user div HTML element as a span element and add onclick attribute that executes a function we will create later that initiates a chat between the logged-in user and the one clicked on, this function takes 3 parameters, and they will be found in the message properties.

Also, set the id of this span as the user id, we will use that to remove the span once the user is disconnected.

case "NEW_USER_CONNECTED": 
      document.getElementById("users").insertAdjacentHTML("beforeend", `<span id=${data.userId} class="user" onclick="initChat(${data.userId}, ${data.connId}, '${data.name}')">${data.name}<span id="msgCount" class="msg-count hide"></span></span>`);
      break;

Case3: User disconnected

When a user is disconnected from the server we want to remove the span that holds his name from the users section.

case "USER_DISCONNECTED": 
     const spanToRemove = document.getElementById(data.userId);
     spanToRemove.parentNode.removeChild(spanToRemove);
     break;

The initChat function

This function fires when a user clicks on a span element from the online users, then it will ad his name to the main chat area header and use ajax to fetch the old messages between the two users.

But before we implement this function, let’s create a file in the ajax folder that retrieves the old messages between two users and call it “getOlderMessages.php”.

<?php
require_once("../config.php");

if(isset($_GET["to"]) && isset($_GET["from"])) {
    $results = array();

    $msgQuery = DB::connect()->prepare("SELECT * FROM messages WHERE from_id = :user1 AND to_id = :user2 OR from_id = :user2 AND to_id = :user1");
    $msgQuery->bindParam(":user1", $_GET["to"]);
    $msgQuery->bindParam(":user2", $_GET["from"]);
    $msgQuery->execute();

    while($row = $msgQuery->fetch(PDO::FETCH_ASSOC)) {
        $results[] = $row;
    }

    echo json_encode($results);
}
else {
    http_response_code(400);
    die();
}
?>

and this is the whole initChat function:

const initChat = (userId, connId, name) => {
    userSelected = true;
    toConnectionId = connId;
    toUserId = userId;
    const otherUserName = document.getElementById("otherUserName");
    const chatArea = document.getElementById("chatArea");
    chatArea.innerHTML = "";
    otherUserName.innerHTML = name;

    fetch(`/chat/ajax/getOlderMessages.php?to=${userId}&from=${currentUserId}`).then(response => response.json()).then(res => {
        if(res.length === 0) return;
        res.forEach(msg => {
            chatArea.insertAdjacentHTML("beforeend", `
               <div class="msg-container ${msg.from_id === currentUserId ? 'blue-container' : 'dark-container'}">
                    <div class="msg">${msg.body}</div>
               </div>
`);
        });
        readChat();
    }).catch(err => console.log(err));
}

The readChat function

Later on, when a user receives a new message, we will increase the count span that is inside each user span by one, so the user can notice that he got a new message, this function will be executed when the logged-in user opens the chat to remove the count and scroll to the bottom of the chat.

const readChat = () => {
    const chatArea = document.getElementById("chatArea");
    chatArea.scrollTop = chatArea.scrollHeight;

    const fromUserSpan = document.getElementById(toUserId);
    const fromUserSpanCount = fromUserSpan.querySelector(".msg-count");
    fromUserSpanCount.innerHTML = "";
    fromUserSpanCount.classList.add("hide");
}

Send a message

In order to send a message, we need to get the value of the chat’s textarea by its id chatMessage.

Check if the input is not empty and there is a user-selected to chat with, if not just return from the function.

Send the message as a JSON string with the recipient connection id, the recipient database id, the sender database id and the message body, then empty the textarea and call the readChat function.

    const sendMessage = () => {
        const msgInput = document.getElementById("chatMessage");
        const msg = msgInput.value;
        if(msg === "" || !userSelected) return; 
        conn.send(JSON.stringify({
            toConnectionId,
            toUserId, 
            fromUserId: currentUserId,
            body: msg
        }));
        msgInput.value = "";
        readChat();
    }

Case 4: New Message

The first thing to do when someone sends a message to another person, we want to update the count beside the span element that holds the sender’s name, so check if the message is not from the logged-in user or the user that is currently chatting with.

if(data.fromUserId !== toUserId && data.fromUserId !== currentUserId) {
   const fromUserSpan = document.getElementById(data.fromUserId);
   const fromUserSpanCount = fromUserSpan.querySelector(".msg-count");
   if(fromUserSpanCount.innerHTML === "" || parseInt(fromUserSpanCount.innerHTML) === 0) {
        fromUserSpanCount.innerHTML = "1";
        fromUserSpanCount.classList.remove("hide");
   }
   else {
        let count = parseInt(fromUserSpanCount.innerHTML);
        count++
        fromUserSpanCount.innerHTML = count;
   }
}

Check if the span count element is empty or has 0 value, then update it to 1 else; you want to increase whatever number it holds.

Next, check if the message is sent from the logged-in user or to the logged-in user to update the main chat area and read the chat because it is already open.

if(currentUserId == data.toUserId && toUserId == data.fromUserId || currentUserId == data.fromUserId && toUserId == data.toUserId) {
       //currently chatting with this user
       chatArea.insertAdjacentHTML("beforeend", `
          <div class="msg-container ${data.fromUserId === currentUserId ? 'blue-container' : 'dark-container'}">
             <div class="msg">${data.body}</div>
          </div>
                    `);
       readChat();
}

Now, have a look at the whole new message case.

case "NEW_MESSAGE": 
    if(data.fromUserId !== toUserId && data.fromUserId !== currentUserId) {
        const fromUserSpan = document.getElementById(data.fromUserId);
        const fromUserSpanCount = fromUserSpan.querySelector(".msg-count");
        if(fromUserSpanCount.innerHTML === "" || parseInt(fromUserSpanCount.innerHTML) === 0) {
            fromUserSpanCount.innerHTML = "1";
            fromUserSpanCount.classList.remove("hide");
        }
        else {
            let count = parseInt(fromUserSpanCount.innerHTML);
            count++
            fromUserSpanCount.innerHTML = count;
        }
    }
    if(currentUserId == data.toUserId && toUserId == data.fromUserId || currentUserId == data.fromUserId && toUserId == data.toUserId) {
        //currently chatting with this user
        chatArea.insertAdjacentHTML("beforeend", `
            <div class="msg-container ${data.fromUserId === currentUserId ? 'blue-container' : 'dark-container'}">
                <div class="msg">${data.body}</div>
            </div>
        `);
        readChat();
    }
    break;

Conclusion

Now you should have a fully functioning real-time, one-to-one chat application with PHP and with register and login functionality. Messages will also be saved in the database so users can have chat history. You can apply this logic to different kinds of applications with PHP. As of now real-time communications between the browser and PHP server are so much easier than before.

You can find this project’s source code in this GitHub repository

0 Shares:
You May Also Like