WebSocket
Introduction
WebSockets provide full-duplex, real-time communication between the server and connected clients. Unlike HTTP requests, which are stateless and short-lived, a WebSocket connection stays open indefinitely — the server can push data to clients at any time without waiting for a request.
Slenix includes a pure-PHP WebSocket server built on PHP's native socket extension, with zero external dependencies. It is ideal for chat applications, live notifications, dashboards, collaborative tools, and any feature that requires instant updates.
Starting the WebSocket Server
Together with the HTTP server
The simplest way to run both servers during development is to pass the --ws flag to serve:
php celestial serve --wsThis starts the HTTP server on port 8080 and the WebSocket server on port 8081 simultaneously.
You can customise both ports:
php celestial serve --ws --ws-port=9000As a standalone server
For production or when you need more control, run the WebSocket server independently:
php celestial ws:serve
php celestial ws:serve --port=9000
php celestial ws:serve --host=0.0.0.0 --port=9000Running both in parallel (production pattern)
# Terminal 1 — HTTP server (or your web server / reverse proxy)
php celestial serve
# Terminal 2 — WebSocket server
php celestial ws:serve --port=8081Registering WebSocket Routes
WebSocket routes are registered in routes/web.php alongside your HTTP routes, using Router::websocket():
use Slenix\Http\Routing\Router;
use App\Controllers\ChatController;
use App\Controllers\NotificationController;
// HTTP routes
Router::get('/', [HomeController::class, 'index']);
// WebSocket routes
Router::websocket('/ws/chat', ChatController::class);
Router::websocket('/ws/notifications', NotificationController::class);Each path maps to a handler class that will be instantiated when the WebSocket server starts.
Creating a Handler
WebSocket handlers extend Slenix\Core\WebSocket\WebSocketHandler and override one or more lifecycle methods:
php celestial make:controller ChatThen extend WebSocketHandler instead of a plain controller:
<?php
declare(strict_types=1);
namespace App\Controllers;
use Slenix\Core\WebSocket\WebSocketHandler;
use Slenix\Core\WebSocket\Connection;
class ChatController extends WebSocketHandler
{
/**
* Called when a new client connects.
*/
public function onOpen(Connection $conn): void
{
echo "New connection: {$conn->getId()}" . PHP_EOL;
$conn->send([
'type' => 'connected',
'message' => 'Welcome to the chat!',
]);
$this->broadcast([
'type' => 'user_joined',
'message' => 'A new user joined.',
], except: $conn);
}
/**
* Called when a message is received from a client.
* $data is automatically decoded from JSON if the payload is valid JSON.
*/
public function onMessage(Connection $conn, mixed $data): void
{
$type = $data['type'] ?? 'message';
if ($type === 'message') {
// Broadcast to everyone including the sender
$this->broadcast([
'type' => 'message',
'sender_id' => $conn->getId(),
'text' => $data['text'] ?? '',
'time' => date('H:i'),
]);
}
}
/**
* Called when a client disconnects.
*/
public function onClose(Connection $conn): void
{
$this->broadcast([
'type' => 'user_left',
'message' => 'A user disconnected.',
]);
}
/**
* Called when an error occurs on a connection.
*/
public function onError(Connection $conn, \Throwable $e): void
{
echo "Error on {$conn->getId()}: {$e->getMessage()}" . PHP_EOL;
}
}Sending Messages
Sending to a specific connection
$conn->send('Hello!');
// Arrays are automatically JSON-encoded
$conn->send([
'type' => 'message',
'text' => 'Hello!',
]);Broadcasting to all connections
// Send to everyone on this handler
$this->broadcast(['type' => 'alert', 'text' => 'Server restarting in 60s.']);
// Send to everyone except the sender
$this->broadcast(['type' => 'message', 'text' => $data['text']], except: $conn);Sending to a specific connection by ID
$this->sendTo($connectionId, [
'type' => 'private',
'message' => 'Only for you.',
]);Connection Attributes
You can store arbitrary data on a connection — useful for tracking the authenticated user, room membership, or any per-client state:
public function onOpen(Connection $conn): void
{
// Store data on the connection
$conn->setAttribute('user_id', 42);
$conn->setAttribute('room', 'general');
}
public function onMessage(Connection $conn, mixed $data): void
{
// Read it back later
$userId = $conn->getAttribute('user_id');
$room = $conn->getAttribute('room', 'lobby');
}Real-World Example — Chat with Rooms
<?php
declare(strict_types=1);
namespace App\Controllers;
use Slenix\Core\WebSocket\WebSocketHandler;
use Slenix\Core\WebSocket\Connection;
class ChatController extends WebSocketHandler
{
public function onOpen(Connection $conn): void
{
$conn->setAttribute('room', 'general');
$conn->setAttribute('username', 'Guest#' . rand(1000, 9999));
$conn->send([
'type' => 'connected',
'username' => $conn->getAttribute('username'),
'room' => $conn->getAttribute('room'),
'online' => $this->connectionCount(),
]);
}
public function onMessage(Connection $conn, mixed $data): void
{
$type = $data['type'] ?? '';
// Change room
if ($type === 'join_room') {
$conn->setAttribute('room', $data['room']);
$conn->send(['type' => 'room_joined', 'room' => $data['room']]);
return;
}
// Set username
if ($type === 'set_username') {
$conn->setAttribute('username', htmlspecialchars($data['username']));
return;
}
// Broadcast message to users in the same room
if ($type === 'message') {
$room = $conn->getAttribute('room');
foreach ($this->getConnections() as $c) {
if ($c->getAttribute('room') === $room) {
$c->send([
'type' => 'message',
'username' => $conn->getAttribute('username'),
'text' => htmlspecialchars($data['text'] ?? ''),
'room' => $room,
'time' => date('H:i'),
]);
}
}
}
}
public function onClose(Connection $conn): void
{
$this->broadcast([
'type' => 'user_left',
'username' => $conn->getAttribute('username'),
'online' => $this->connectionCount() - 1,
]);
}
}JavaScript Client
Connect to your WebSocket server from the browser:
const ws = new WebSocket('ws://127.0.0.1:8081/ws/chat');
ws.onopen = () => {
console.log('Connected!');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'message') {
console.log(`[${data.time}] ${data.username}: ${data.text}`);
}
if (data.type === 'user_joined') {
console.log('Someone joined the chat.');
}
};
ws.onclose = () => {
console.log('Disconnected.');
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
// Send a message
function sendMessage(text) {
ws.send(JSON.stringify({ type: 'message', text }));
}
// Change room
function joinRoom(room) {
ws.send(JSON.stringify({ type: 'join_room', room }));
}Using Jobs and WebSocket Together
A common pattern is combining Jobs and WebSocket for real-time notifications after a background task completes. For example, after a user places an order, an email is sent in the background and the seller is notified in real time:
public function checkout(Request $request, Response $response): void
{
$order = Order::create([...]);
// Background jobs — do not block the response
dispatch(new SendOrderConfirmationEmailJob($order));
dispatch(new GenerateInvoicePdfJob($order));
dispatch(new UpdateStockJob($order));
$response->status(201)->json([
'success' => true,
'message' => 'Order placed successfully.',
'data' => $order,
]);
}Inside GenerateInvoicePdfJob::handle(), after the PDF is ready, notify the client via WebSocket:
public function handle(): void
{
$pdf = $this->generatePdf($this->order);
// Notify the user's browser in real time
WebSocketClient::send("/ws/notifications", $this->order->user_id, [
'type' => 'invoice_ready',
'order' => $this->order->id,
'pdf_url' => $pdf->url(),
]);
}Handler Method Reference
| Method | When it is called |
|---|---|
onOpen(Connection $conn) | A new client connects and the handshake is complete |
onMessage(Connection $conn, mixed $data) | A message frame is received (JSON auto-decoded) |
onClose(Connection $conn) | A client disconnects |
onError(Connection $conn, \Throwable $e) | An exception is thrown inside a lifecycle method |
Broadcasting Method Reference
| Method | Description |
|---|---|
$this->broadcast($data) | Send to all connected clients |
$this->broadcast($data, except: $conn) | Send to all except one connection |
$this->sendTo($id, $data) | Send to a specific connection by ID |
$this->getConnections() | Returns all active Connection objects |
$this->connectionCount() | Returns the number of active connections |
Connection Method Reference
| Method | Description |
|---|---|
$conn->getId() | Returns the unique connection ID |
$conn->send($data) | Sends a message (string or array) to this client |
$conn->close() | Closes the connection |
$conn->setAttribute($key, $value) | Stores a value on the connection |
$conn->getAttribute($key, $default) | Retrieves a stored value |
$conn->hasAttribute($key) | Checks whether an attribute exists |