Build Real‑Time Group & Private Chat with WebSocket and PHP Validation
This guide explains how to design group and private chat messages, define a unified JSON protocol, validate payloads with a custom validator, and implement WebSocket callbacks (onWorkerStart, onWebSocketConnect, onMessage, onClose) using PHP and JavaScript client examples for both one‑to‑one and group conversations.
Message Protocol
The client and server exchange a JSON payload with the following fields:
event : join (join connection) or speak (send message)
mode : 1 for private chat, 2 for group chat
group_id : ID of the group; use 0 for private chat
from_user_id / from_username : sender’s ID and nickname
to_user_id : receiver’s ID (ignored for group messages)
content : message text (max 128 characters)
{
"event": "join",
"mode": 1,
"group_id": 0,
"from_user_id": "10086",
"from_username": "OpenSourceTech",
"to_user_id": "10000",
"content": "Hi, OpenSourceTech"
}Payload Validation
The project reuses the Validate plugin from the Webman framework. Install it with Composer: composer require tinywan/validate A custom validator MessageFormatValidate.php defines rules and error messages for each field:
<?php
declare(strict_types=1);
namespace app\common\validate;
class MessageFormatValidate extends BaseValidate {
protected $rule = [
'mode' => 'require|in:1,2',
'group_id' => 'require|number',
'from_user_id'=> 'require',
'from_username'=> 'require',
'from_avatar' => 'require',
'content' => 'require|max:128',
];
protected $message = [
'mode.require' => 'Message mode is required',
'mode.in' => 'Message mode must be 1 or 2',
'group_id.require' => 'Group ID is required',
'from_user_id.require'=> 'User ID is required',
'from_username.require'=> 'Username is required',
'from_avatar.require' => 'User avatar is required',
'content.require' => 'Message content is required',
'content.max' => 'Message content can be at most 128 characters',
];
}Server Callback Functions
The WebSocket server defines four key callbacks that are invoked by the Webman gateway: onWorkerStart(): runs once when a business worker process starts. onWebSocketConnect(): executed after the client completes the WebSocket handshake. onMessage(): validates incoming JSON, handles join and speak events, and routes messages. onClose(): runs when a client disconnects; notifies the group (if any) and logs errors.
/** onWebSocketConnect – called after handshake */
public static function onWebSocketConnect(string $clientId, array $data): bool {
try {
$_SESSION['client_ip'] = get_client_real_ip($data['server']['HTTP_X_FORWARDED_FOR'] ?? $data['server']['HTTP_REMOTEIP'] ?? '127.0.0.1');
$_SESSION['browser'] = $data['server']['HTTP_USER_AGENT'] ?? 'unknown';
} catch (\Throwable $e) {
Log::error('[onWebSocketConnect] '.$e->getMessage());
return Gateway::sendToCurrentClient(broadcast_json(500, $e->getMessage()));
}
return true;
} /** onMessage – handles join and speak events */
public static function onMessage(string $clientId, string $message): bool {
$originMessage = json_decode($message, true);
if (json_last_error() !== JSON_ERROR_NONE) {
Gateway::closeClient($clientId, broadcast_json(400, 'Invalid JSON'));
return false;
}
$validate = new MessageFormatValidate();
if (false === $validate->check($originMessage)) {
Gateway::closeClient($clientId, broadcast_json(400, $validate->getError()));
return false;
}
// routing logic for join and speak (bindUid, joinGroup, sendToUid, sendToGroup)
return true;
} /** onClose – client disconnect handling */
public static function onClose(string $clientId): bool {
$data = [
'event' => 'leave',
'group_id' => $_SESSION['group_id'] ?? '',
'client_id' => $clientId,
'content' => 'leaving group',
'from_user_id' => $_SESSION['from_user_id'] ?? '',
'from_username'=> $_SESSION['from_username'] ?? ''
];
if (!empty($data['group_id'])) {
GateWay::sendToGroup($data['group_id'], broadcast_json(0, 'close', $data));
return true;
}
return Gateway::sendToCurrentClient(broadcast_json(500, 'error', $data));
}Private Chat (Single‑Chat) Example
JavaScript client creates a WebSocket connection, sends a join payload, then exchanges speak messages.
var ws = new WebSocket("ws://127.0.0.1:8783");
ws.onopen = function () {
let payload = {
"event": "join",
"mode": 1,
"group_id": 0,
"from_user_id": "10086",
"from_username": "UserA",
"to_user_id": "10000",
"content": "join session"
};
ws.send(JSON.stringify(payload));
};
ws.onmessage = function (evt) {
console.log("[UserA] received: " + evt.data);
};
// send a private message
let msg = {
"event": "speak",
"mode": 1,
"group_id": 0,
"from_user_id": "10086",
"from_username": "UserA",
"to_user_id": "10000",
"content": "Hi, I am UserA"
};
ws.send(JSON.stringify(msg));Group Chat Example
Set mode to 2 and use a shared group_id (e.g., 100). The same join and speak logic applies, but messages are broadcast to the whole group.
var ws = new WebSocket("ws://127.0.0.1:8783");
ws.onopen = function () {
let payload = {
"event": "join",
"mode": 2,
"group_id": 100,
"from_user_id": "10086",
"from_username": "UserA",
"to_user_id": "10000",
"content": "join group"
};
ws.send(JSON.stringify(payload));
};
ws.onmessage = function (evt) {
console.log("[UserA] received: " + evt.data);
};
// send a group message
let groupMsg = {
"event": "speak",
"mode": 2,
"group_id": 100,
"from_user_id": "10086",
"from_username": "UserA",
"to_user_id": "10000",
"content": "[Group] I am UserA"
};
ws.send(JSON.stringify(groupMsg));Source Repository
Complete source code for this tutorial is available at:
https://github.com/Tinywan/webman-admin
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Open Source Tech Hub
Sharing cutting-edge internet technologies and practical AI resources.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
