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.

Open Source Tech Hub
Open Source Tech Hub
Open Source Tech Hub
Build Real‑Time Group & Private Chat with WebSocket and PHP Validation

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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendvalidationWebSocketPHPgroup chatReal-time Chatprivate chat
Open Source Tech Hub
Written by

Open Source Tech Hub

Sharing cutting-edge internet technologies and practical AI resources.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.