Real-Time Chat Using PHP WebSockets

Understanding the Basics of WebSockets and PHP

Hello Developers, Based on the response to the previous article ”Realtime private chat using PHP Websockets”, I am going to publish an updated version of the real-time private chat using PHP WebSockets with UI enhancement and bug fixes.

php-chat-app_TOSw10EB_11988778417173004676.png

Introduction to WebSockets

WebSockets is a bi-directional, full-duplex, persistent connection from a web browser to a server. Once a WebSocket connection is established the connection stays open until the client or server decides to close this connection. With this open connection, the client or server can send a message at any given time to the other. This makes web programming entirely event-driven, not (just) user-initiated. It is stateful. As well, at this time, a single running server application is aware of all connections, allowing you to communicate with any number of open connections at any given time.

The WebSocket protocol enables interaction between a web browser (or other client application) and a web server with lower overhead than half-duplex alternatives such as HTTP polling, facilitating real-time data transfer from and to the server.

To learn more about WebSockets click here

Introduction to Angular JS

AngularJS is a JavaScript-based open-source front-end web framework mainly maintained by Google and by a community of individuals and corporations to address many of the challenges encountered in developing single-page applications.

Prerequisites

Before you start proceeding with this tutorial, we are assuming that you are already aware of the basics of JavaScript and understand the HTTP protocol. If you are not well aware of these concepts, then we will suggest you go through our short tutorials on JavaScript and HTTP.

Installation

  1. Clone the repository from Github using — “git clone https://github.com/harendra21/Realtime-One-To-One-Chat.git
  2. Place the cloned folder on your local server.
  3. Now open cmd in this in the cloned directory and run — “composer install”
  4. Change your directory to the public and run “npm install” to download dependencies of angular js and bootstrap.
  5. Then change the directory to the bin folder by — “cd bin”
  6. Stat-server by — “php chat-server.php”
  7. Now hit the public folder of the project by your browser — “localhost/path_to_your_folder/public”
  8. Connect your application with the database by simply replacing the details of the database in the chat.php file at line no 23.
  9. Create the database table by following the query —
CREATE TABLE socket_id ( id int(11) NOT NULL AUTO_INCREMENT, user varchar(255) DEFAULT NULL, socket_id int(11) DEFAULT NULL, PRIMARY KEY (id) )
  1. Enjoy!

Code

app.php

<?php
namespace App;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
require './../vendor/autoload.php';
use Medoo\Medoo;
class Chat implements MessageComponentInterface
{
    protected $clients;
    private $activeUsers;
    private $activeConnections;
    private $database;
    public function __construct()
    {
        $this->database = new Medoo([
            'database_type' => 'mysql',
            'database_name' => 'chat_db',
            'server' => 'localhost',
            'username' => 'root',
            'password' => 'root'
        ]);
        $this->clients = new \SplObjectStorage;
        $this->activeUsers = [];
        $this->activeConnections = [];
        session_start();
    }
    public function onOpen(ConnectionInterface $conn){
        // Store the new connection to send messages to later
        $this->clients->attach($conn);
        //echo "New connection! ({$conn->resourceId})\n";
    }
    public function onMessage(ConnectionInterface $conn, $msg)
    {
        $jsonMsg = json_decode($msg);
        if ($jsonMsg->type == "login") {
            $onlineUsers = [];
            $onlineUsers['type'] = "onlineUsers";
            $this->activeUsers[$conn->resourceId] = $jsonMsg->name;

            $this->updateSocketId($jsonMsg->name,$conn->resourceId);

            $onlineUsers['onlineUsers'] = $this->activeUsers;
            $this->sendMessageToAll(json_encode($onlineUsers));
        } elseif ($jsonMsg->type == "message") {
            $this->sendMessageToUser($conn, $jsonMsg);
        }
    }

    public function sendMessageToUser($conn, $msg){
        $to = $msg->data->to;
        $data = $this->database->select('socket_id', [
            'socket_id'
        ], [
            'user' => $to
        ]);

        $toSocketId = $data[0]['socket_id'];

        foreach ($this->clients as $client) {
            if ($client->resourceId == $toSocketId) {
                $client->send(json_encode(['type' => 'message','data' => $msg->data]));
            }
        }
    }

    public function sendMessageToOthers($conn, $msg){
        foreach ($this->clients as $client) {
            if ($conn !== $client) {
                $client->send($msg);
            }
        }
    }

    public function sendMessageToAll($msg){
        foreach ($this->clients as $client) {
            
            $client->send($msg);
        }
    }

    public function onClose(ConnectionInterface $conn)
    {
        // The connection is closed, remove it, as we can no longer send it messages
        $this->clients->detach($conn);
        unset($this->activeUsers[$conn->resourceId]);
        $onlineUsers = [];
        $onlineUsers['type'] = "onlineUsers";
        $onlineUsers['onlineUsers'] = $this->activeUsers;
        $this->sendMessageToOthers($conn, json_encode($onlineUsers));
        //echo "Connection {$conn->resourceId} has disconnected\n";
    }

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

        $conn->close();
    }

    public function updateSocketId($user,$id){
        
        $data = $this->database->select('socket_id', [
            'user'
        ], [
            'user' => $user
        ]);
        if(empty($data)){
            // insert
            //echo 'Insert';
            $this->database->insert('socket_id', [
                'user' => $user,
                'socket_id' => $id
            ]);
        }else{
            // update
            //echo 'Update';
            $data = $this->database->update('socket_id', [
                'socket_id' => $id
            ], [
                'user' => $user
            ]);
        }
    }
}

This file is used to implement the server-side logic of the ratchet library.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chat App</title>
    <link rel="stylesheet" href="./node_modules/bootstrap/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="./css/style.css">
</head>
<body ng-app="myApp">
    <div ng-controller="appCtrl" ng-init="init()" class="container" ng-cloak>
        <div ng-if="!isUserLoggedIn" class="row">
            <div class="col-md-8">
                <input type="text" ng-model="userName" class="form-control" placeholder="Your name">
            </div>
            <div class="col-md-4 text-right">
                <button ng-click="login(userName)" class="btn btn-primary btn-block">Login</button>
            </div>
        </div>

        <div ng-if="isUserLoggedIn" class="row">
            
            <div class="col-md-4 offset-md-8">
                <button ng-click="logout()" class="btn btn-primary btn-block">Logout</button>
            </div>
            <h2>I'm {{ loggedInUser }}</h2>
        </div>
        <div class="container-fluid h-100" ng-if="onlineUsers">
            <div class="row justify-content-center h-100">
                <div class="col-md-4 col-xl-3 chat"><div class="card mb-sm-3 mb-md-0 contacts_card">
                    
                    <div class="card-body contacts_body">
                        
                        <small ng-if="newMessage != null && newMessage != toUser">New message {{ newMessage }}</small>
                        <ui class="contacts">
                            
                            <li class="active" ng-repeat="(key, value) in onlineUsers" ng-if="loggedInUser != value" ng-click="selectUser(value)">
                                <div class="d-flex bd-highlight">
                                    <div class="user_info">
                                        <span>{{ value }}</span>
                                        <p>{{ value }} is online</p>
                                    </div>
                                </div>
                            </li>
                            
                        </ui>
                    </div>
                    <div class="card-footer"></div>
                </div></div>
                <div class="col-md-8 col-xl-6 chat">


                    
                    
                    <div class="card" ng-repeat="(key, value) in onlineUsers" ng-if="toUser == value">
                        <div class="card-header msg_head">
                            <div class="d-flex bd-highlight">
                                
                                <div class="user_info">
                                    <span>Chat with {{ toUser }}</span>
                                    
                                </div>
                                
                            </div>
                            
                        </div>
                        <div class="card-body msg_card_body">


                            <div ng-repeat="msg in messages" >
                                <div ng-if="msg.to == toUser" class="d-flex justify-content-end mb-4">
                                    <div class="msg_cotainer_send">
                                        {{ msg.message }}
                                    </div>
                                </div>
                                <div ng-if="msg.from == toUser" class="d-flex justify-content-start mb-4">
                                    <div class="msg_cotainer">{{ msg.message }}</div> 
                                </div>
                            </div>
                        </div>
                        <div class="card-footer">
                            <div class="input-group">
                                <div class="input-group-append">
                                    <span class="input-group-text attach_btn"><i class="fas fa-paperclip"></i></span>
                                </div>
                                <textarea class="form-control type_msg" placeholder="Type your message..." ng-model="messageModel"></textarea>
                                <div class="input-group-append" ng-click="sendMsg(messageModel)">
                                    <span class="input-group-text send_btn"><i class="fas fa-location-arrow"></i>Send</span>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>

    </div>
    <script src="./node_modules/angular/angular.min.js"></script>
    <script src="./script/app.js"></script>
</body>
</html>

index.html is used for the main view of the application.

app.js

var app = angular.module("myApp", []);
var conn = new WebSocket('ws://localhost:8081');


app.controller('appCtrl', function($scope) {
    $scope.onlineUsers = [];
    $scope.loggedInUser = null;
    $scope.isUserLoggedIn = false;
    $scope.userName = null;
    $scope.toUser = null;
    $scope.messageModel = null;
    $scope.messages = [];
    $scope.newMessage = null;
    $scope.init = function(){
        var userLogedIn = localStorage.getItem('loggedInUser');
        if(userLogedIn != null || userLogedIn != undefined){
            $scope.loggedInUser = userLogedIn;
            $scope.isUserLoggedIn = true;
            setTimeout(() => {
                var data = {'type' : 'login', 'name': userLogedIn};
                conn.send(JSON.stringify(data));
            },100)
        }else{
            $scope.isUserLoggedIn = false;
            $scope.loggedInUser = null;
        }
    }
    $scope.logout = function(){
        localStorage.removeItem('loggedInUser')
        $scope.init();
        conn.close()
    }
    $scope.login = function(userName){
        var data = {'type' : 'login', 'name': userName};
        localStorage.setItem('loggedInUser',userName)
        $scope.init();
        conn.send(JSON.stringify(data));
    }
    conn.onmessage = function(e) {
        var data = JSON.parse(e.data);
        //console.log(data);
        if(data.type == "onlineUsers"){
            $scope.onlineUsers = data.onlineUsers;
            $scope.$apply();
        }else if (data.type == "message"){
            $scope.messages.push(data.data);
            $scope.newMessage = data.data.from;
            $scope.$apply();
            //$scope.playAudio();
            setTimeout(() => {
                $scope.newMessage = null;
                $scope.$apply();
            },2000)
        }
    };
    $scope.sendMsg = function(message){
        if ( message != null ){
            var data = {'type' : 'message', data : {
                from : $scope.loggedInUser,
                to : $scope.toUser,
                message : message
            }};
            $scope.messages.push(data.data)
            conn.send(JSON.stringify(data));
            setTimeout(() => {
                $scope.messageModel = null;
                $scope.$apply();
                console.log('Here');
            },100)
        }
    }
    $scope.selectUser = function(toUser){
        $scope.toUser = toUser;
    }
    $scope.playAudio = function() {
        var audio = new Audio('audio/beep.mp3');
        audio.play();
    };
});

app.js contains angular js app code for the application and all logic from the client side.

style.css

body,
html {
  height: 100%;
  margin: 0;
  background: #7f7fd5;
  background: -webkit-linear-gradient(to right, #91eae4, #86a8e7, #7f7fd5);
  background: linear-gradient(to right, #91eae4, #86a8e7, #7f7fd5);
}

.chat {
  margin-top: auto;
  margin-bottom: auto;
}

.card {
  height: 500px;
  border-radius: 15px !important;
  background-color: rgba(0, 0, 0, 0.4) !important;
}

.contacts_body {
  padding: 0.75rem 0 !important;
  overflow-y: auto;
  white-space: nowrap;
}

.msg_card_body {
  overflow-y: auto;
}

.card-header {
  border-radius: 15px 15px 0 0 !important;
  border-bottom: 0 !important;
}

.card-footer {
  border-radius: 0 0 15px 15px !important;
  border-top: 0 !important;
}

.container {
  align-content: center;
}

.search {
  border-radius: 15px 0 0 15px !important;
  background-color: rgba(0, 0, 0, 0.3) !important;
  border: 0 !important;
  color: white !important;
}

.search:focus {
  box-shadow: none !important;
  outline: 0px !important;
}

.type_msg {
  background-color: rgba(0, 0, 0, 0.3) !important;
  border: 0 !important;
  color: white !important;
  height: 60px !important;
  overflow-y: auto;
}

.type_msg:focus {
  box-shadow: none !important;
  outline: 0px !important;
}

.attach_btn {
  border-radius: 15px 0 0 15px !important;
  background-color: rgba(0, 0, 0, 0.3) !important;
  border: 0 !important;
  color: white !important;
  cursor: pointer;
}

.send_btn {
  border-radius: 0 15px 15px 0 !important;
  background-color: rgba(0, 0, 0, 0.3) !important;
  border: 0 !important;
  color: white !important;
  cursor: pointer;
}

.search_btn {
  border-radius: 0 15px 15px 0 !important;
  background-color: rgba(0, 0, 0, 0.3) !important;
  border: 0 !important;
  color: white !important;
  cursor: pointer;
}

.contacts {
  list-style: none;
  padding: 0;
}

.contacts li {
  width: 100% !important;
  padding: 5px 10px;
  margin-bottom: 15px !important;
}

.active {
  background-color: rgba(0, 0, 0, 0.3);
}

.user_img {
  height: 70px;
  width: 70px;
  border: 1.5px solid #f5f6fa;
}

.user_img_msg {
  height: 40px;
  width: 40px;
  border: 1.5px solid #f5f6fa;
}

.img_cont {
  position: relative;
  height: 70px;
  width: 70px;
}

.img_cont_msg {
  height: 40px;
  width: 40px;
}

.online_icon {
  position: absolute;
  height: 15px;
  width: 15px;
  background-color: #4cd137;
  border-radius: 50%;
  bottom: 0.2em;
  right: 0.4em;
  border: 1.5px solid white;
}

.offline {
  background-color: #c23616 !important;
}

.user_info {
  margin-top: auto;
  margin-bottom: auto;
  margin-left: 15px;
}

.user_info span {
  font-size: 20px;
  color: white;
}

.user_info p {
  font-size: 10px;
  color: rgba(255, 255, 255, 0.6);
}

.video_cam {
  margin-left: 50px;
  margin-top: 5px;
}

.video_cam span {
  color: white;
  font-size: 20px;
  cursor: pointer;
  margin-right: 20px;
}

.msg_cotainer {
  margin-top: auto;
  margin-bottom: auto;
  margin-left: 10px;
  border-radius: 25px;
  background-color: #82ccdd;
  padding: 10px;
  position: relative;
}

.msg_cotainer_send {
  margin-top: auto;
  margin-bottom: auto;
  margin-right: 10px;
  border-radius: 25px;
  background-color: #78e08f;
  padding: 10px;
  position: relative;
}

.msg_time {
  position: absolute;
  left: 0;
  bottom: -15px;
  color: rgba(255, 255, 255, 0.5);
  font-size: 10px;
}

.msg_time_send {
  position: absolute;
  right: 0;
  bottom: -15px;
  color: rgba(255, 255, 255, 0.5);
  font-size: 10px;
}

.msg_head {
  position: relative;
}

#action_menu_btn {
  position: absolute;
  right: 10px;
  top: 10px;
  color: white;
  cursor: pointer;
  font-size: 20px;
}

.action_menu {
  z-index: 1;
  position: absolute;
  padding: 15px 0;
  background-color: rgba(0, 0, 0, 0.5);
  color: white;
  border-radius: 15px;
  top: 30px;
  right: 15px;
  display: none;
}

.action_menu ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

.action_menu ul li {
  width: 100%;
  padding: 10px 15px;
  margin-bottom: 5px;
}

.action_menu ul li i {
  padding-right: 10px;
}

.action_menu ul li:hover {
  cursor: pointer;
  background-color: rgba(0, 0, 0, 0.2);
}

@media (max-width: 576px) {
  .contacts_card {
    margin-bottom: 15px !important;
  }
}

style.css contains all CSS required for the application.

Thank you for reading