WebSockets

Info: Modular Architecture: Core implementation uses PHP 8.5 Fibers with zero dependencies. For high-scale production, drop in the larafony/websocket-react bridge package.

Overview

Larafony's WebSocket system provides complete real-time communication capabilities:

Quick Start

1. Basic Server

use Larafony\Framework\WebSockets\Engine\FiberEngine;
use Larafony\Framework\WebSockets\Server;

$server = new Server(new FiberEngine(), '0.0.0.0', 8080);

$server->on('open', function ($data, $connection) {
echo "Connected: {$connection->getId()}\n";
});

$server->on('message', function ($payload, $connection) {
$connection->send("Echo: {$payload}");
});

$server->on('close', function ($data, $connection) {
echo "Disconnected: {$connection->getId()}\n";
});

$server->run();

2. Using Console Command

# Start with default config
php bin/larafony websocket:start

# Custom host and port
php bin/larafony websocket:start --host=127.0.0.1 --port=9000

3. Configuration

// config/websocket.php
return [
'host' => env('WEBSOCKET_HOST', '0.0.0.0'),
'port' => (int) env('WEBSOCKET_PORT', 8080),
];

Architecture

Success: Clean Separation: Protocol logic (Frame, Encoder, Decoder) is shared between engines. Only the I/O layer differs.

┌─────────────────────────────────────────────────────┐
│ SHARED PROTOCOL LAYER │
├─────────────────────────────────────────────────────┤
│ Frame, Encoder, Decoder, Opcode, Handshake │
│ Server, EventDispatcher, Connection logic │
├─────────────────────────────────────────────────────┤
│ ENGINE ABSTRACTION (EngineContract) │
├──────────────────────┬──────────────────────────────┤
│ FiberEngine │ ReactEngine │
│ (Core - no deps) │ (Bridge - react/*) │
└──────────────────────┴──────────────────────────────┘

Custom Event Handling

The server automatically parses JSON messages with event field:

// Client sends:
ws.send(JSON.stringify({
event: 'chat_message',
data: { message: 'Hello!' }
}));
// Server handles:
$server->on('chat_message', function ($data, $connection) {
$message = $data['message'];
// Process chat message...
});

Broadcasting

// Broadcast to all connections
$server->broadcast('Hello everyone!');

// Broadcast with filter (exclude sender)
$server->broadcast(
json_encode(['type' => 'notification', 'text' => 'New message']),
fn($conn) => $conn->getId() !== $currentConnection->getId()
);

Service Provider Pattern

Extend WebSocketServiceProvider to register your handlers:

namespace App\Providers;

use Larafony\Framework\WebSockets\Contracts\ServerContract;
use Larafony\Framework\WebSockets\ServiceProviders\WebSocketServiceProvider;

class ChatWebSocketProvider extends WebSocketServiceProvider
{
protected function registerDefaultHandlers(ServerContract $server): void
{
$server->on('chat_message', function ($data, $connection) {
// Handle chat messages
});

$server->on('user_typing', function ($data, $connection) use ($server) {
// Broadcast typing indicator
$server->broadcast(
json_encode(['event' => 'typing', 'user' => $data['userId']]),
fn($c) => $c->getId() !== $connection->getId()
);
});
}
}

Bridge Package: ReactPHP

For production environments requiring high concurrency:

composer require larafony/websocket-react
// Use ReactWebSocketServiceProvider instead of WebSocketServiceProvider
use Larafony\WebSocket\ReactWebSocketServiceProvider;

class MyReactProvider extends ReactWebSocketServiceProvider
{
protected function registerDefaultHandlers(ServerContract $server): void
{
// Same API as FiberEngine!
$server->on('message', fn($data, $conn) => ...);
}
}

Warning: Performance: FiberEngine handles ~1000 concurrent connections. ReactEngine scales to 10,000+ with libuv/libev event loops.

Protocol Components

Frame

use Larafony\Framework\WebSockets\Protocol\Frame;

// Create frames with factory methods
$textFrame = Frame::text('Hello');
$binaryFrame = Frame::binary($data);
$pingFrame = Frame::ping();
$closeFrame = Frame::close(1000, 'Normal closure');

// Send directly
$connection->send($textFrame);
$connection->send('Or just a string');

Opcode Enum

use Larafony\Framework\WebSockets\Protocol\Opcode;

Opcode::TEXT; // 1
Opcode::BINARY; // 2
Opcode::CLOSE; // 8
Opcode::PING; // 9
Opcode::PONG; // 10

$opcode->isControl(); // true for CLOSE, PING, PONG

Client-Side (Vue.js Example)

const ws = new WebSocket('ws://localhost:8080');

ws.onopen = () => {
console.log('Connected');
};

ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.event === 'ai_response') {
displayMessage(data.data);
}
};

const sendMessage = (message) => {
ws.send(JSON.stringify({
event: 'chat_message',
data: { message }
}));
};

Practical Example: AI Chat in demo-app 🤖

Complete demonstration of WebSocket integration with AI - from Vue frontend through WebSocket to backend calling OpenAI API.

Controller (Inertia)

demo-app/src/Controllers/ChatAIController.php

class ChatAIController extends Controller
{
public function index(ConfigContract $config): \Inertia\Response
{
return inertia('Chat/Index', [
'wsHost' => $config->get('websocket.host', 'localhost'),
'wsPort' => $config->get('websocket.port', 8080),
]);
}
}

Message Listener with OpenAI

demo-app/src/Listeners/ChatMessageListener.php

class ChatMessageListener
{
public function __construct(
private readonly ConfigContract $config,
) {}

public function __invoke(array $data, ConnectionContract $connection): void
{
$message = $data['message'] ?? '';
if (empty($message)) return;

$response = $this->callOpenAI($message);

$connection->send(Frame::text(json_encode([
'event' => 'ai_response',
'data' => ['message' => $response, 'timestamp' => time()],
])));
}

private function callOpenAI(string $message): string
{
$client = new CurlHttpClient();
$apiKey = $this->config->get('openai.api_key');

$request = new Request(
'POST',
new Uri('https://api.openai.com/v1/chat/completions'),
['Content-Type' => 'application/json', 'Authorization' => "Bearer {$apiKey}"],
json_encode([
'model' => $this->config->get('openai.model', 'gpt-4'),
'messages' => [['role' => 'user', 'content' => $message]],
])
);

$response = $client->sendRequest($request);
$body = json_decode((string) $response->getBody(), true);

return $body['choices'][0]['message']['content'] ?? 'Error';
}
}

ServiceProvider

demo-app/src/Providers/ChatWebSocketProvider.php

class ChatWebSocketProvider extends WebSocketServiceProvider
{
protected function registerDefaultHandlers(ServerContract $server): void
{
$listener = $this->container->get(ChatMessageListener::class);
$server->on('chat_message', $listener);

$server->on('open', fn($data, $conn) =>
$conn->send(json_encode(['event' => 'welcome', 'data' => 'Connected']))
);
}
}

Vue Component

demo-app/resources/js/Pages/Chat/Index.vue

const ws = new WebSocket(`ws://${props.wsHost}:${props.wsPort}`);

ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.event === 'ai_response') {
messages.value.push({
type: 'ai',
text: data.data.message,
timestamp: data.data.timestamp,
});
}
};

function sendMessage() {
ws.send(JSON.stringify({
event: 'chat_message',
data: { message: newMessage.value },
}));
}

Data Flow

The entire flow works without page reload, with instant response thanks to persistent WebSocket connection.

Framework Comparison 🔥

How does Larafony's WebSocket implementation compare to other PHP frameworks?

vs Laravel Reverb

Laravel introduced Reverb in 2024 as its first-party WebSocket solution:

Aspect Laravel Reverb Larafony
Dependencies Ratchet, Redis (for scaling) **Zero** (core), ReactPHP (optional)
Protocol Pusher protocol **Native RFC 6455**
Architecture Separate Reverb server process **Integrated into framework**
Scaling Requires Redis pub/sub **Built-in broadcast**
Learning Curve Pusher concepts, channels, events **Simple `on('event', callback)`**
External Services Often paired with Pusher/Soketi **Fully self-contained**

Success: Larafony advantage: No external services, no Pusher protocol abstraction, no Redis requirement. Just pure WebSockets with a clean, minimal API.

vs Symfony

Symfony does not include a built-in WebSocket solution:

Aspect Symfony Larafony
Native Support ❌ None **✅ Full RFC 6455**
Recommended Solution Mercure (SSE) or third-party Ratchet **Native FiberEngine or ReactEngine**
Protocol Mercure uses Server-Sent Events **True bidirectional WebSockets**
Integration Manual setup required **ServiceProvider + console command**
Real-time One-way (SSE) or external package **True bidirectional**

Success: Larafony advantage: First-class WebSocket support built from scratch, not delegated to external projects or limited to Server-Sent Events.

Why Larafony WebSockets Stand Out

Zero Dependencies

Core uses only PHP 8.5 Fibers and ext-sockets. No composer packages for basic functionality.

RFC 6455 From Scratch

Complete protocol implementation you can learn from and extend.

Swappable Engines

FiberEngine for dev, ReactEngine for production - same API, same handlers.

Simple Mental Model

No channels, no presence, no Pusher protocol. Just connections, events, broadcasts.

// That's it. No Redis, no Pusher, no external services.
$server = new Server(new FiberEngine(), '0.0.0.0', 8080);
$server->on('message', fn($data, $conn) => $conn->send("Echo: $data"));
$server->run();

Summary

Feature FiberEngine (Core) ReactEngine (Bridge)
Dependencies None (ext-sockets) react/event-loop, react/socket
Concurrency Model PHP 8.5 Fibers Event Loop + Callbacks
Scale ~1,000 connections ~10,000+ connections
Best For Development, learning, simple apps Production, high-traffic