What You'll Learn
By the end of this tutorial, you'll have built a complete real-time chat application with the following features:
- Real-time bidirectional communication using WebSockets
- User authentication and session management
- Message storage and retrieval from MySQL database
- Modern, responsive chat interface
- Online user presence indicators
- Typing indicators and read receipts
Prerequisites
Before we begin, make sure you have the following installed on your development environment:
- PHP 7.4 or higher with CLI support
- Composer (PHP dependency manager)
- MySQL 5.7 or higher
- A local web server (Apache/Nginx) or PHP built-in server
- Basic knowledge of PHP, JavaScript, and MySQL
Understanding WebSockets
WebSockets provide a full-duplex communication channel over a single TCP connection, enabling real-time data transfer between client and server. Unlike traditional HTTP requests, WebSockets maintain a persistent connection, allowing instant message delivery without polling or long-polling techniques. This makes them perfect for chat applications where low latency is crucial.
Project Structure
We'll organize our chat application with a clean directory structure. Create the following folder hierarchy:
chat-application/
├── config/
│ └── database.php
├── server/
│ └── websocket-server.php
├── public/
│ ├── index.php
│ ├── login.php
│ ├── register.php
│ ├── chat.php
│ ├── css/
│ │ └── style.css
│ └── js/
│ └── chat.js
├── includes/
│ ├── auth.php
│ └── functions.php
└── composer.json
Step 1: Install Required Dependencies
We'll use the Ratchet library for WebSocket functionality. Create a composer.json file in your project root directory:
{
"require": {
"cboden/ratchet": "^0.4",
"phpmailer/phpmailer": "^6.5"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
Install the dependencies by running the following command in your terminal:
composer install
Step 2: Database Setup
Create a new MySQL database for our chat application. Execute the following SQL commands to create the necessary tables. Save this as database.sql in your project root:
CREATE DATABASE IF NOT EXISTS chat_app;
USE chat_app;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
avatar VARCHAR(255) DEFAULT 'default-avatar.png',
status ENUM('online', 'offline', 'away') DEFAULT 'offline',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_seen TIMESTAMP NULL DEFAULT NULL
);
CREATE TABLE messages (
id INT AUTO_INCREMENT PRIMARY KEY,
sender_id INT NOT NULL,
receiver_id INT NULL,
message TEXT NOT NULL,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (receiver_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE chat_rooms (
id INT AUTO_INCREMENT PRIMARY KEY,
room_name VARCHAR(100) NOT NULL,
created_by INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE room_participants (
id INT AUTO_INCREMENT PRIMARY KEY,
room_id INT NOT NULL,
user_id INT NOT NULL,
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (room_id) REFERENCES chat_rooms(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_messages_sender ON messages(sender_id);
CREATE INDEX idx_messages_receiver ON messages(receiver_id);
CREATE INDEX idx_messages_created ON messages(created_at);
Step 3: Database Configuration
Create a database configuration file at config/database.php:
<?php
class Database {
private $host = 'localhost';
private $db_name = 'chat_app';
private $username = 'root';
private $password = '';
private $conn;
public function getConnection() {
$this->conn = null;
try {
$this->conn = new PDO(
"mysql:host=" . $this->host . ";dbname=" . $this->db_name,
$this->username,
$this->password
);
$this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->conn->exec("set names utf8mb4");
} catch(PDOException $e) {
echo "Connection Error: " . $e->getMessage();
}
return $this->conn;
}
}
?>
Step 4: Authentication System
Create the authentication helper file at includes/auth.php:
<?php
session_start();
require_once __DIR__ . '/../config/database.php';
class Auth {
private $conn;
public function __construct() {
$database = new Database();
$this->conn = $database->getConnection();
}
public function register($username, $email, $password) {
try {
$hashed_password = password_hash($password, PASSWORD_BCRYPT);
$query = "INSERT INTO users (username, email, password) VALUES (:username, :email, :password)";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':username', $username);
$stmt->bindParam(':email', $email);
$stmt->bindParam(':password', $hashed_password);
if($stmt->execute()) {
return ['success' => true, 'message' => 'Registration successful'];
}
} catch(PDOException $e) {
return ['success' => false, 'message' => 'Username or email already exists'];
}
return ['success' => false, 'message' => 'Registration failed'];
}
public function login($username, $password) {
$query = "SELECT * FROM users WHERE username = :username OR email = :username LIMIT 1";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':username', $username);
$stmt->execute();
if($stmt->rowCount() > 0) {
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if(password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$update = "UPDATE users SET status = 'online', last_seen = NOW() WHERE id = :id";
$update_stmt = $this->conn->prepare($update);
$update_stmt->bindParam(':id', $user['id']);
$update_stmt->execute();
return ['success' => true, 'message' => 'Login successful'];
}
}
return ['success' => false, 'message' => 'Invalid credentials'];
}
public function logout() {
if(isset($_SESSION['user_id'])) {
$query = "UPDATE users SET status = 'offline', last_seen = NOW() WHERE id = :id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':id', $_SESSION['user_id']);
$stmt->execute();
}
session_destroy();
return true;
}
public function isLoggedIn() {
return isset($_SESSION['user_id']);
}
}
?>
Step 5: WebSocket Server
Now, let's create the WebSocket server. Create server/websocket-server.php:
<?php
require dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/config/database.php';
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
class ChatServer implements MessageComponentInterface {
protected $clients;
protected $users;
private $conn;
public function __construct() {
$this->clients = new \SplObjectStorage;
$this->users = [];
$database = new Database();
$this->conn = $database->getConnection();
echo "WebSocket Server Started on port 8080\n";
}
public function onOpen(ConnectionInterface $conn) {
$this->clients->attach($conn);
echo "New connection! ({$conn->resourceId})\n";
}
public function onMessage(ConnectionInterface $from, $msg) {
$data = json_decode($msg, true);
if (!$data) {
return;
}
switch($data['type']) {
case 'auth':
$this->handleAuth($from, $data);
break;
case 'message':
$this->handleMessage($from, $data);
break;
case 'typing':
$this->handleTyping($from, $data);
break;
case 'read':
$this->handleRead($from, $data);
break;
}
}
private function handleAuth($conn, $data) {
$userId = $data['userId'];
$username = $data['username'];
$this->users[$userId] = [
'conn' => $conn,
'username' => $username
];
$query = "UPDATE users SET status = 'online' WHERE id = :id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':id', $userId);
$stmt->execute();
$this->broadcast([
'type' => 'user_status',
'userId' => $userId,
'username' => $username,
'status' => 'online'
]);
echo "User authenticated: $username (ID: $userId)\n";
}
private function handleMessage($from, $data) {
$senderId = $data['senderId'];
$receiverId = $data['receiverId'] ?? null;
$message = htmlspecialchars($data['message']);
$query = "INSERT INTO messages (sender_id, receiver_id, message) VALUES (:sender_id, :receiver_id, :message)";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':sender_id', $senderId);
$stmt->bindParam(':receiver_id', $receiverId);
$stmt->bindParam(':message', $message);
$stmt->execute();
$messageId = $this->conn->lastInsertId();
$response = [
'type' => 'message',
'id' => $messageId,
'senderId' => $senderId,
'senderName' => $this->users[$senderId]['username'],
'receiverId' => $receiverId,
'message' => $message,
'timestamp' => date('Y-m-d H:i:s')
];
if ($receiverId && isset($this->users[$receiverId])) {
$this->users[$receiverId]['conn']->send(json_encode($response));
} else {
$this->broadcast($response);
}
$from->send(json_encode($response));
}
private function handleTyping($from, $data) {
$userId = $data['userId'];
$isTyping = $data['isTyping'];
$response = [
'type' => 'typing',
'userId' => $userId,
'username' => $this->users[$userId]['username'],
'isTyping' => $isTyping
];
$this->broadcast($response, $from);
}
private function handleRead($from, $data) {
$messageId = $data['messageId'];
$query = "UPDATE messages SET is_read = TRUE WHERE id = :id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':id', $messageId);
$stmt->execute();
$response = [
'type' => 'read',
'messageId' => $messageId
];
$this->broadcast($response);
}
private function broadcast($data, $exclude = null) {
$message = json_encode($data);
foreach($this->clients as $client) {
if ($client !== $exclude) {
$client->send($message);
}
}
}
public function onClose(ConnectionInterface $conn) {
$userId = null;
foreach($this->users as $id => $user) {
if ($user['conn'] === $conn) {
$userId = $id;
break;
}
}
if ($userId) {
$query = "UPDATE users SET status = 'offline', last_seen = NOW() WHERE id = :id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':id', $userId);
$stmt->execute();
$this->broadcast([
'type' => 'user_status',
'userId' => $userId,
'username' => $this->users[$userId]['username'],
'status' => 'offline'
]);
unset($this->users[$userId]);
}
$this->clients->detach($conn);
echo "Connection {$conn->resourceId} has disconnected\n";
}
public function onError(ConnectionInterface $conn, \Exception $e) {
echo "An error has occurred: {$e->getMessage()}\n";
$conn->close();
}
}
$server = IoServer::factory(
new HttpServer(
new WsServer(
new ChatServer()
)
),
8080
);
$server->run();
?>
Step 6: Registration Page
Create the user registration page at public/register.php:
<?php
require_once '../includes/auth.php';
$auth = new Auth();
if ($auth->isLoggedIn()) {
header('Location: chat.php');
exit;
}
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username']);
$email = trim($_POST['email']);
$password = $_POST['password'];
$confirm_password = $_POST['confirm_password'];
if (empty($username) || empty($email) || empty($password)) {
$error = 'All fields are required';
} elseif ($password !== $confirm_password) {
$error = 'Passwords do not match';
} elseif (strlen($password) < 6) {
$error = 'Password must be at least 6 characters';
} else {
$result = $auth->register($username, $email, $password);
if ($result['success']) {
$success = $result['message'];
header('refresh:2;url=login.php');
} else {
$error = $result['message'];
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - Chat App</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="auth-container">
<div class="auth-box">
<h2>Create Account</h2>
<?php if ($error): ?>
<div class="alert alert-error"><?php echo $error; ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success"><?php echo $success; ?></div>
<?php endif; ?>
<form method="POST" action="">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<label for="confirm_password">Confirm Password</label>
<input type="password" id="confirm_password" name="confirm_password" required>
</div>
<button type="submit" class="btn btn-primary">Register</button>
</form>
<p class="auth-link">Already have an account? <a href="login.php">Login here</a></p>
</div>
</div>
</body>
</html>
Step 7: Login Page
Create the login page at public/login.php:
<?php
require_once '../includes/auth.php';
$auth = new Auth();
if ($auth->isLoggedIn()) {
header('Location: chat.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username']);
$password = $_POST['password'];
if (empty($username) || empty($password)) {
$error = 'All fields are required';
} else {
$result = $auth->login($username, $password);
if ($result['success']) {
header('Location: chat.php');
exit;
} else {
$error = $result['message'];
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Chat App</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="auth-container">
<div class="auth-box">
<h2>Login to Chat</h2>
<?php if ($error): ?>
<div class="alert alert-error"><?php echo $error; ?></div>
<?php endif; ?>
<form method="POST" action="">
<div class="form-group">
<label for="username">Username or Email</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
<p class="auth-link">Don't have an account? <a href="register.php">Register here</a></p>
</div>
</div>
</body>
</html>
Step 8: Chat Interface
Create the main chat interface at public/chat.php:
<?php
require_once '../includes/auth.php';
$auth = new Auth();
if (!$auth->isLoggedIn()) {
header('Location: login.php');
exit;
}
$database = new Database();
$conn = $database->getConnection();
$query = "SELECT id, username, status, avatar FROM users WHERE id != :current_user ORDER BY status DESC, username ASC";
$stmt = $conn->prepare($query);
$stmt->bindParam(':current_user', $_SESSION['user_id']);
$stmt->execute();
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat Application</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="chat-container">
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-header">
<h3>Chat App</h3>
<div class="user-info">
<span><?php echo htmlspecialchars($_SESSION['username']); ?></span>
<a href="logout.php" class="btn-logout">Logout</a>
</div>
</div>
<div class="users-list">
<h4>Active Users</h4>
<div id="usersList">
<?php foreach($users as $user): ?>
<div class="user-item" data-user-id="<?php echo $user['id']; ?>">
<div class="user-avatar">
<img src="avatars/<?php echo htmlspecialchars($user['avatar']); ?>" alt="Avatar">
<span class="status-indicator status-<?php echo $user['status']; ?>"></span>
</div>
<div class="user-details">
<span class="username"><?php echo htmlspecialchars($user['username']); ?></span>
<span class="user-status"><?php echo $user['status']; ?></span>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- Chat Area -->
<div class="chat-area">
<div class="chat-header">
<h3 id="chatHeader">Select a user to start chatting</h3>
<div id="typingIndicator" class="typing-indicator" style="display: none;">
<span></span> is typing...
</div>
</div>
<div class="messages-container" id="messagesContainer">
<div class="no-chat-selected">
<p>? Welcome to Chat App!</p>
<p>Select a user from the left sidebar to start messaging</p>
</div>
</div>
<div class="message-input-container">
<form id="messageForm">
<input type="text" id="messageInput" placeholder="Type your message..." autocomplete="off" disabled>
<button type="submit" id="sendButton" disabled>Send</button>
</form>
</div>
</div>
</div>
<script>
const currentUserId = <?php echo $_SESSION['user_id']; ?>;
const currentUsername = '<?php echo $_SESSION['username']; ?>';
</script>
<script src="js/chat.js"></script>
</body>
</html>
Step 9: Logout Handler
Create public/logout.php:
<?php
require_once '../includes/auth.php';
$auth = new Auth();
$auth->logout();
header('Location: login.php');
exit;
?>
Step 10: CSS Styling
Create the stylesheet at public/css/style.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
/* Authentication Pages */
.auth-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.auth-box {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
width: 100%;
max-width: 400px;
}
.auth-box h2 {
color: #333;
margin-bottom: 30px;
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.alert {
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
}
.alert-error {
background-color: #fee;
color: #c33;
border: 1px solid #fcc;
}
.alert-success {
background-color: #efe;
color: #3c3;
border: 1px solid #cfc;
}
.auth-link {
text-align: center;
margin-top: 20px;
color: #666;
}
.auth-link a {
color: #667eea;
text-decoration: none;
font-weight: 600;
}
/* Chat Interface */
.chat-container {
display: flex;
height: 100vh;
background: white;
}
.sidebar {
width: 320px;
background: #f8f9fa;
border-right: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.sidebar-header h3 {
margin-bottom: 10px;
}
.user-info {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
}
.btn-logout {
background: rgba(255, 255, 255, 0.2);
color: white;
padding: 6px 12px;
border-radius: 4px;
text-decoration: none;
font-size: 12px;
transition: background 0.3s;
}
.btn-logout:hover {
background: rgba(255, 255, 255, 0.3);
}
.users-list {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.users-list h4 {
color: #666;
margin-bottom: 15px;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
}
.user-item {
display: flex;
align-items: center;
padding: 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.3s;
margin-bottom: 8px;
}
.user-item:hover {
background: #e8eaf6;
}
.user-item.active {
background: #667eea;
}
.user-item.active .username,
.user-item.active .user-status {
color: white;
}
.user-avatar {
position: relative;
margin-right: 12px;
}
.user-avatar img {
width: 45px;
height: 45px;
border-radius: 50%;
object-fit: cover;
}
.status-indicator {
position: absolute;
bottom: 2px;
right: 2px;
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid white;
}
.status-online {
background-color: #4caf50;
}
.status-offline {
background-color: #9e9e9e;
}
.status-away {
background-color: #ff9800;
}
.user-details {
flex: 1;
display: flex;
flex-direction: column;
}
.username {
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.user-status {
font-size: 12px;
color: #999;
text-transform: capitalize;
}
/* Chat Area */
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
background: white;
}
.chat-header {
padding: 20px;
background: white;
border-bottom: 1px solid #e0e0e0;
}
.chat-header h3 {
color: #333;
margin-bottom: 5px;
}
.typing-indicator {
color: #667eea;
font-size: 14px;
font-style: italic;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #fafafa;
}
.no-chat-selected {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
color: #999;
}
.no-chat-selected p {
margin: 10px 0;
}
.message {
display: flex;
margin-bottom: 20px;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.sent {
justify-content: flex-end;
}
.message-content {
max-width: 60%;
padding: 12px 16px;
border-radius: 12px;
position: relative;
}
.message.received .message-content {
background: white;
border-bottom-left-radius: 4px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.message.sent .message-content {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-bottom-right-radius: 4px;
}
.message-sender {
font-weight: 600;
font-size: 12px;
margin-bottom: 4px;
color: #667eea;
}
.message.sent .message-sender {
color: rgba(255, 255, 255, 0.9);
}
.message-text {
word-wrap: break-word;
line-height: 1.5;
}
.message-time {
font-size: 11px;
margin-top: 6px;
opacity: 0.7;
}
.message-input-container {
padding: 20px;
background: white;
border-top: 1px solid #e0e0e0;
}
#messageForm {
display: flex;
gap: 10px;
}
#messageInput {
flex: 1;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 24px;
font-size: 14px;
transition: border-color 0.3s;
}
#messageInput:focus {
outline: none;
border-color: #667eea;
}
#sendButton {
padding: 12px 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 24px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s;
}
#sendButton:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
#sendButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Responsive Design */
@media (max-width: 768px) {
.sidebar {
width: 100%;
position: absolute;
z-index: 100;
height: 100vh;
transform: translateX(-100%);
transition: transform 0.3s;
}
.sidebar.active {
transform: translateX(0);
}
.message-content {
max-width: 80%;
}
}
Step 11: JavaScript WebSocket Client
Create the client-side WebSocket handler at public/js/chat.js:
let ws;
let selectedUserId = null;
let selectedUsername = '';
let typingTimeout;
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
// Initialize WebSocket connection
function connectWebSocket() {
ws = new WebSocket('ws://localhost:8080');
ws.onopen = function() {
console.log('Connected to WebSocket server');
reconnectAttempts = 0;
// Authenticate user
ws.send(JSON.stringify({
type: 'auth',
userId: currentUserId,
username: currentUsername
}));
};
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
handleMessage(data);
};
ws.onclose = function() {
console.log('Disconnected from WebSocket server');
// Attempt to reconnect
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
console.log(`Reconnecting... Attempt ${reconnectAttempts}`);
setTimeout(connectWebSocket, 3000);
}
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
};
}
// Handle incoming messages
function handleMessage(data) {
switch(data.type) {
case 'message':
displayMessage(data);
break;
case 'user_status':
updateUserStatus(data.userId, data.status);
break;
case 'typing':
if (data.userId === selectedUserId) {
showTypingIndicator(data.username, data.isTyping);
}
break;
case 'read':
markMessageAsRead(data.messageId);
break;
}
}
// Display message in chat
function displayMessage(data) {
// Only display if it's a message for current conversation
if (data.senderId !== selectedUserId && data.senderId !== currentUserId) {
return;
}
if (data.receiverId && data.receiverId !== selectedUserId && data.receiverId !== currentUserId) {
return;
}
const messagesContainer = document.getElementById('messagesContainer');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${data.senderId === currentUserId ? 'sent' : 'received'}`;
messageDiv.dataset.messageId = data.id;
const messageContent = document.createElement('div');
messageContent.className = 'message-content';
if (data.senderId !== currentUserId) {
const senderSpan = document.createElement('div');
senderSpan.className = 'message-sender';
senderSpan.textContent = data.senderName;
messageContent.appendChild(senderSpan);
}
const messageText = document.createElement('div');
messageText.className = 'message-text';
messageText.textContent = data.message;
messageContent.appendChild(messageText);
const messageTime = document.createElement('div');
messageTime.className = 'message-time';
messageTime.textContent = formatTime(data.timestamp);
messageContent.appendChild(messageTime);
messageDiv.appendChild(messageContent);
messagesContainer.appendChild(messageDiv);
// Scroll to bottom
messagesContainer.scrollTop = messagesContainer.scrollHeight;
// Mark as read if received
if (data.senderId === selectedUserId) {
markAsRead(data.id);
}
}
// Send message
document.getElementById('messageForm').addEventListener('submit', function(e) {
e.preventDefault();
const messageInput = document.getElementById('messageInput');
const message = messageInput.value.trim();
if (message && selectedUserId && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'message',
senderId: currentUserId,
receiverId: selectedUserId,
message: message
}));
messageInput.value = '';
stopTyping();
}
});
// Handle typing indicator
document.getElementById('messageInput').addEventListener('input', function() {
if (selectedUserId && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'typing',
userId: currentUserId,
isTyping: true
}));
clearTimeout(typingTimeout);
typingTimeout = setTimeout(stopTyping, 2000);
}
});
function stopTyping() {
if (selectedUserId && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'typing',
userId: currentUserId,
isTyping: false
}));
}
}
function showTypingIndicator(username, isTyping) {
const indicator = document.getElementById('typingIndicator');
if (isTyping) {
indicator.querySelector('span').textContent = username;
indicator.style.display = 'block';
} else {
indicator.style.display = 'none';
}
}
// User selection
document.querySelectorAll('.user-item').forEach(item => {
item.addEventListener('click', function() {
const userId = parseInt(this.dataset.userId);
const username = this.querySelector('.username').textContent;
// Remove active class from all users
document.querySelectorAll('.user-item').forEach(u => u.classList.remove('active'));
this.classList.add('active');
selectedUserId = userId;
selectedUsername = username;
// Update chat header
document.getElementById('chatHeader').textContent = `Chat with ${username}`;
// Enable message input
document.getElementById('messageInput').disabled = false;
document.getElementById('sendButton').disabled = false;
document.getElementById('messageInput').focus();
// Load chat history
loadChatHistory(userId);
});
});
// Load chat history from server
function loadChatHistory(userId) {
fetch(`get_messages.php?user_id=${userId}`)
.then(response => response.json())
.then(messages => {
const messagesContainer = document.getElementById('messagesContainer');
messagesContainer.innerHTML = '';
messages.forEach(msg => {
displayMessage({
id: msg.id,
senderId: msg.sender_id,
senderName: msg.sender_name,
receiverId: msg.receiver_id,
message: msg.message,
timestamp: msg.created_at
});
});
})
.catch(error => console.error('Error loading messages:', error));
}
// Update user online status
function updateUserStatus(userId, status) {
const userItem = document.querySelector(`.user-item[data-user-id="${userId}"]`);
if (userItem) {
const statusIndicator = userItem.querySelector('.status-indicator');
const userStatus = userItem.querySelector('.user-status');
statusIndicator.className = `status-indicator status-${status}`;
userStatus.textContent = status;
}
}
// Mark message as read
function markAsRead(messageId) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'read',
messageId: messageId
}));
}
}
function markMessageAsRead(messageId) {
const message = document.querySelector(`.message[data-message-id="${messageId}"]`);
if (message) {
message.classList.add('read');
}
}
// Format timestamp
function formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const messageDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
if (messageDate.getTime() === today.getTime()) {
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
} else {
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ' ' +
date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
}
}
// Initialize connection when page loads
connectWebSocket();
Step 12: Message History API
Create public/get_messages.php to retrieve chat history:
<?php
require_once '../includes/auth.php';
$auth = new Auth();
if (!$auth->isLoggedIn()) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
$database = new Database();
$conn = $database->getConnection();
$userId = isset($_GET['user_id']) ? intval($_GET['user_id']) : 0;
$currentUserId = $_SESSION['user_id'];
if ($userId <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Invalid user ID']);
exit;
}
$query = "SELECT m.*, u.username as sender_name
FROM messages m
JOIN users u ON m.sender_id = u.id
WHERE (m.sender_id = :current_user AND m.receiver_id = :other_user)
OR (m.sender_id = :other_user AND m.receiver_id = :current_user)
ORDER BY m.created_at ASC
LIMIT 100";
$stmt = $conn->prepare($query);
$stmt->bindParam(':current_user', $currentUserId);
$stmt->bindParam(':other_user', $userId);
$stmt->execute();
$messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
header('Content-Type: application/json');
echo json_encode($messages);
?>
Step 13: Running the Application
Start the WebSocket Server
Open a terminal in your project root directory and run the following command to start the WebSocket server:
php server/websocket-server.php
You should see the message: "WebSocket Server Started on port 8080"
Start the Web Server
In a new terminal window, navigate to the public directory and start PHP's built-in web server:
cd public
php -S localhost:8000
Access the Application
Open your web browser and navigate to:
- Registration:
http://localhost:8000/register.php
- Login:
http://localhost:8000/login.php
- Chat:
http://localhost:8000/chat.php
Testing the Chat Application
To test the real-time functionality:
- Register at least two different user accounts
- Open the chat application in two different browser windows (or use incognito mode)
- Log in with different users in each window
- Select a user to chat with and send messages
- You should see messages appearing instantly in both windows
- Test the typing indicator by typing in one window
- Observe the online/offline status changes when users connect/disconnect
Advanced Features to Add
Once you have the basic chat working, consider adding these enhancements:
File Sharing
Implement file upload functionality to allow users to share images, documents, and other files within the chat.
Group Chat
Create chat rooms where multiple users can participate in a single conversation using the chat_rooms and room_participants tables.
Message Encryption
Add end-to-end encryption to secure messages using libraries like OpenSSL or Sodium.
Push Notifications
Implement browser push notifications to alert users of new messages even when they're not actively viewing the chat application.
Emoji Support
Add an emoji picker to allow users to express themselves with emojis in their messages.
Message Search
Implement a search feature to allow users to find specific messages in their chat history.
Voice and Video Calls
Integrate WebRTC for voice and video calling capabilities between users.
Security Best Practices
When deploying your chat application to production, ensure you implement these security measures:
1. Use HTTPS and WSS
Always use HTTPS for your web application and WSS (WebSocket Secure) for WebSocket connections. This encrypts all data transmitted between the client and server.
// Update WebSocket connection to use WSS in production
const ws = new WebSocket('wss://yourdomain.com:8080');
2. Implement Rate Limiting
Protect against spam and abuse by implementing rate limiting on message sending and API endpoints.
3. Input Sanitization
Always sanitize user input to prevent XSS attacks. The code already uses htmlspecialchars()
for output escaping.
4. SQL Injection Prevention
Use prepared statements (as shown in the code) to prevent SQL injection attacks.
5. Session Security
Configure secure session settings in your PHP configuration:
<?php
// Add to your auth.php or config file
ini_set('session.cookie_httponly', 1);
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_secure', 1); // Only if using HTTPS
ini_set('session.cookie_samesite', 'Strict');
?>
6. CSRF Protection
Implement CSRF tokens for all forms to prevent cross-site request forgery attacks.
Performance Optimization
Database Indexing
The SQL schema already includes indexes on frequently queried columns. Monitor your database performance and add additional indexes as needed.
Message Pagination
For chats with extensive history, implement pagination to load messages in chunks rather than all at once:
<?php
// Update get_messages.php to support pagination
$limit = isset($_GET['limit']) ? intval($_GET['limit']) : 50;
$offset = isset($_GET['offset']) ? intval($_GET['offset']) : 0;
$query = "SELECT m.*, u.username as sender_name
FROM messages m
JOIN users u ON m.sender_id = u.id
WHERE (m.sender_id = :current_user AND m.receiver_id = :other_user)
OR (m.sender_id = :other_user AND m.receiver_id = :current_user)
ORDER BY m.created_at DESC
LIMIT :limit OFFSET :offset";
$stmt = $conn->prepare($query);
$stmt->bindParam(':current_user', $currentUserId, PDO::PARAM_INT);
$stmt->bindParam(':other_user', $userId, PDO::PARAM_INT);
$stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
$stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
?>
Caching
Implement caching for frequently accessed data like user lists and recent messages using Redis or Memcached.
WebSocket Connection Pooling
For high-traffic applications, consider implementing connection pooling to manage WebSocket connections more efficiently.
Troubleshooting Common Issues
WebSocket Connection Failed
If you encounter WebSocket connection errors:
- Ensure the WebSocket server is running (
php server/websocket-server.php
) - Check if port 8080 is available and not blocked by firewall
- Verify the WebSocket URL in
chat.js
matches your server configuration - Check browser console for detailed error messages
Messages Not Appearing
If messages aren't displaying:
- Check the browser console for JavaScript errors
- Verify the database tables were created correctly
- Ensure both users are authenticated with the WebSocket server
- Check the WebSocket server logs for incoming/outgoing messages
Database Connection Errors
If you see database errors:
- Verify MySQL is running
- Check database credentials in
config/database.php
- Ensure the database and tables were created successfully
- Check MySQL error logs for detailed information
Deployment Considerations
Process Management
For production deployment, use a process manager like Supervisor to keep the WebSocket server running:
[program:websocket-server]
command=php /path/to/chat-application/server/websocket-server.php
autostart=true
autorestart=true
stderr_logfile=/var/log/websocket-server.err.log
stdout_logfile=/var/log/websocket-server.out.log
user=www-data
Nginx Configuration
Configure Nginx as a reverse proxy for your WebSocket server:
upstream websocket {
server localhost:8080;
}
server {
listen 443 ssl;
server_name yourdomain.com;
# SSL certificates
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location /ws {
proxy_pass http://websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
root /path/to/chat-application/public;
index index.php;
try_files $uri $uri/ /index.php?$query_string;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
}
Database Backups
Set up automated database backups to prevent data loss:
#!/bin/bash
# backup-chat-db.sh
mysqldump -u root -p chat_app > /backups/chat_app_$(date +%Y%m%d_%H%M%S).sql
# Keep only last 30 days of backups
find /backups -name "chat_app_*.sql" -mtime +30 -delete
Monitoring and Logging
Implement comprehensive logging to track application performance and debug issues:
<?php
// Add to websocket-server.php
class Logger {
private $logFile;
public function __construct($logFile = 'websocket.log') {
$this->logFile = __DIR__ . '/../logs/' . $logFile;
}
public function log($message, $level = 'INFO') {
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[$timestamp] [$level] $message" . PHP_EOL;
file_put_contents($this->logFile, $logMessage, FILE_APPEND);
}
public function error($message) {
$this->log($message, 'ERROR');
}
public function info($message) {
$this->log($message, 'INFO');
}
public function debug($message) {
$this->log($message, 'DEBUG');
}
}
?>
Scaling Your Chat Application
Horizontal Scaling
As your user base grows, you may need to scale horizontally by running multiple WebSocket servers. Implement a message broker like Redis Pub/Sub or RabbitMQ to synchronize messages across servers.
Load Balancing
Use a load balancer to distribute connections across multiple WebSocket server instances. Ensure session stickiness so users maintain their connection to the same server.
Database Replication
Set up MySQL master-slave replication to handle increased read operations and provide redundancy.
Conclusion
You've successfully built a fully functional real-time chat application using PHP and WebSockets! This application demonstrates fundamental concepts of real-time communication, including bidirectional data flow, user presence tracking, and message persistence.
The architecture we've implemented provides a solid foundation that you can extend with additional features like group chats, file sharing, voice calls, and more. Remember to always prioritize security and performance as you add new functionality.
Key takeaways from this tutorial:
- WebSockets enable true real-time communication with persistent connections
- Proper separation of concerns makes your application maintainable and scalable
- Security should be a primary consideration in every aspect of development
- Database design and indexing are crucial for performance
- Monitoring and logging help identify and resolve issues quickly
Next Steps
To further enhance your chat application skills, consider exploring:
- WebRTC for peer-to-peer audio and video communication
- Progressive Web App (PWA) features for offline functionality
- Integration with third-party services (Twilio, SendGrid)
- Mobile app development using React Native or Flutter
- Advanced features like message threading and reactions
- AI-powered chatbots and automated responses