WebSocket
App::ws($path, $onMessage, $onOpen, $onClose) — register a WebSocket endpoint. ZealPHP uses OpenSwoole\WebSocket\Server which is backward-compatible with HTTP routes on the same port.
WebSocket registration
$app->ws(
'/ws/chat',
onMessage: function($server, $frame, $g) {
// $frame->data — message text
// $frame->fd — connection id
// $frame->opcode — 1=TEXT, 2=BINARY (PING/PONG filtered automatically)
$server->push($frame->fd, 'echo: ' . $frame->data);
},
onOpen: function($server, $request, $g) {
// $request->fd — connection id
// $request->cookie — cookies from upgrade request
// $request->get — query params from ws://host/path?key=val
$server->push($request->fd, json_encode(['event' => 'connected']));
},
onClose: function($server, $fd, $g) {
// clean up per-connection state
}
);
Live demo — 6 endpoints
Echo
Broadcast
Ticker
Rooms
Auth
Binary
WS /ws/echo — mirrors every message back verbatim.
$app->ws('/ws/echo', onMessage: fn($server,$frame) => $server->push($frame->fd, 'echo: '.$frame->data));
WS /ws/broadcast — every message goes to ALL connected clients.
$broadcastClients = [];
$app->ws('/ws/broadcast',
onMessage: function($server, $frame, $g) use (&$broadcastClients) {
foreach (array_keys($broadcastClients) as $fd) {
if ($server->isEstablished($fd))
$server->push($fd, json_encode(['from'=>$frame->fd,'msg'=>$frame->data]));
}
},
onOpen: fn($s,$req) => $broadcastClients[$req->fd] = true,
onClose: fn($s,$fd) => unset($broadcastClients[$fd])
);
WS /ws/ticker — server pushes every 1s using a spawned coroutine.
$app->ws('/ws/ticker',
onMessage: fn($s,$f) => trim($f->data)==='stop' ? $s->close($f->fd) : null,
onOpen: function($server, $request, $g) {
$fd = $request->fd;
go(function() use ($server, $fd) {
$i = 0;
while ($server->isEstablished($fd)) {
co::sleep(1);
$server->push($fd, json_encode(['tick' => ++$i, 'time' => date('H:i:s')]));
}
});
}
);
WS /ws/rooms?room=general — cross-worker rooms via Store (OpenSwoole\Table). Every worker shares the same client registry.
// Shared across all workers — created before run()
Store::make('ws_rooms', 4096, [
'room' => [\OpenSwoole\Table::TYPE_STRING, 64],
'uid' => [\OpenSwoole\Table::TYPE_STRING, 128],
]);
$app->ws('/ws/rooms',
onOpen: fn($server, $request, $g) => Store::set('ws_rooms', (string)$request->fd, [
'room' => $request->get['room'] ?? 'general',
'uid' => $request->get['uid'] ?? 'guest_'.$request->fd,
]),
onMessage: function($server, $frame, $g) {
$me = Store::get('ws_rooms', (string)$frame->fd);
foreach (Store::table('ws_rooms') as $fd => $info)
if ($info['room'] === $me['room'] && $server->isEstablished((int)$fd))
$server->push((int)$fd, json_encode(['from'=>$me['uid'],'msg'=>$frame->data]));
},
onClose: fn($server, $fd, $g) => Store::del('ws_rooms', (string)$fd)
);
WS /ws/auth?token=secret — validates token in onOpen, disconnects with code 4001 if invalid.
$app->ws('/ws/auth',
onOpen: function($server, $request, $g) {
$token = $request->get['token'] ?? null;
if ($token !== 'secret') {
$server->push($request->fd, json_encode(['error' => 'Unauthorized']));
$server->disconnect($request->fd, 4001, 'Unauthorized');
return;
}
$server->push($request->fd, json_encode(['event' => 'authenticated']));
},
onMessage: fn($server, $frame) => $server->push($frame->fd, 'secure: '.$frame->data)
);
WS /ws/binary — checks $frame->opcode, echoes binary as binary. PING/PONG filtered automatically by ZealPHP.
$app->ws('/ws/binary',
onMessage: function($server, $frame, $g) {
if ($frame->opcode === \OpenSwoole\WebSocket\Server::WEBSOCKET_OPCODE_BINARY) {
// Echo raw bytes back as a binary frame
$server->push($frame->fd, $frame->data, \OpenSwoole\WebSocket\Server::WEBSOCKET_OPCODE_BINARY);
} else {
$server->push($frame->fd, json_encode(['bytes' => strlen($frame->data)]));
}
}
);
Browser JavaScript
HTTPS-aware browser client
const endpoint = '/ws/echo';
const scheme = location.protocol === 'https:' ? 'wss://' : 'ws://';
const socket = new WebSocket(scheme + location.host + endpoint);
socket.addEventListener('open', () => {
socket.send('Hello from the browser');
});
socket.addEventListener('message', event => {
console.log('received:', event.data);
});
function sendWhenReady(message) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(message);
return;
}
socket.addEventListener('open', () => socket.send(message), { once: true });
}
Live browser client
Choose an endpoint and connect. Send will auto-connect if needed.