plesk/ratchetphp
RatchetPHP is a PHP WebSocket library for building real-time, bidirectional applications. This Plesk-maintained package provides server and client components for event-driven messaging, making it easier to add live updates and persistent connections to PHP projects.
plesk/ratchetphp is a maintained fork of cboden/ratchet (same Ratchet\ namespace, MIT), so upstream docs at socketo.me and the original GitHub examples apply verbatim. The fork's value is PHP 8.x compatibility patches, not new features.
composer require plesk/ratchetphp
First place to look is vendor/plesk/ratchetphp/src/Ratchet/: read MessageComponentInterface.php (four methods: onOpen, onMessage, onClose, onError), then Server/IoServer.php, Http/HttpServer.php, and WebSocket/WsServer.php. The examples/ directory has a runnable echo server and a chat server — start there.
Minimal server:
<?php
require __DIR__.'/vendor/autoload.php';
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
class EchoServer implements MessageComponentInterface {
protected SplObjectStorage $clients;
public function __construct() { $this->clients = new SplObjectStorage; }
public function onOpen(ConnectionInterface $conn) { $this->clients->attach($conn); }
public function onMessage(ConnectionInterface $conn, $msg) {
foreach ($this->clients as $c) { $c->send($msg); }
}
public function onClose(ConnectionInterface $conn) { $this->clients->detach($conn); }
public function onError(ConnectionInterface $conn, \Exception $e) { $conn->close(); }
}
IoServer::factory(
new HttpServer(new WsServer(new EchoServer)),
8080
)->run();
Run it with php server.php — not through a web server. Then connect from the browser: new WebSocket('ws://localhost:8080').
First real use case for a Laravel dev: a long-running sidecar process that pushes events (notifications, presence, live dashboard updates) to browsers, while Laravel itself stays a stateless HTTP app. Everything below assumes that shape.
Stack the decorators, don't reinvent them. The default composition is HttpServer(WsServer($component)). Insert middleware in that chain: SessionProvider (Symfony sessions), OriginCheck, or a router for multiple endpoints. You can wrap your component in additional decorators that inspect $conn->httpRequest during onOpen before delegating.
Keep components as plain services. Inject dependencies via constructor; store per-connection state on $conn (it accepts dynamic properties) or in an SplObjectStorage keyed by connection. $conn->resourceId exists but treat it as an opaque key, not something to parse.
Rooms/topics, the standard shape:
public function onOpen(ConnectionInterface $conn): void {
$this->clients->attach($conn);
// validate auth here; close immediately if invalid
}
public function onMessage(ConnectionInterface $from, $raw): void {
$msg = json_decode($raw, true);
if (!is_array($msg) || !isset($msg['room'])) { return; }
$room = $this->rooms[$msg['room']] ??= new SplObjectStorage;
$room->attach($from);
foreach ($room as $client) {
if ($client !== $from) { $client->send(json_encode($msg)); }
}
}
public function onClose(ConnectionInterface $conn): void {
$this->clients->detach($conn);
foreach ($this->rooms as $room) { $room->detach($conn); }
}
Run it as an Artisan command under Supervisor, so php artisan ws:serve shares config, DI, and logging with the rest of the app:
public function handle(): int {
$loop = React\EventLoop\Factory::create();
$app = new NotificationServer($loop, app(LoggerInterface::class));
$socket = new React\Socket\SocketServer(
sprintf('0.0.0.0:%d', $this->option('port')), [], $loop
);
$server = new IoServer(new HttpServer(new WsServer($app)), $socket, $loop);
$loop->addSignal(SIGTERM, fn() => $loop->stop()); // graceful shutdown
$loop->run();
return 0;
}
Note the direct SocketServer + IoServer construction — IoServer::factory() creates its own loop, and you need the loop reference for timers and signals.
Cross-process push is the hard part. Laravel HTTP workers cannot call $conn->send() on connections living in another process. The durable pattern: Laravel publishes to Redis (or a DB queue, or SNS); the WebSocket process bridges that into the loop. Use clue/redis-react and $loop->addReadStream() on the Redis socket, or fall back to $loop->addPeriodicTimer(0.25, fn() => drainQueue()). Never attempt to hit the WebSocket process synchronously from a controller.
Auth on connect. The browser WebSocket API cannot set custom headers. Use one of: a signed URL (URL::temporarySignedRoute), a session cookie (works same-origin; SessionProvider can hydrate it), or a first-message handshake. Validate inside onOpen and call $conn->close() immediately on failure — the HTTP upgrade has already completed by then, so rejection is a fast close, not a 401.
Keepalive via the loop:
$ws = new WsServer($app);
$ws->enableKeepAlive($loop, 30);
Test the component, not the socket. MessageComponentInterface takes an interface you can mock trivially; write PHPUnit tests that pass a fake ConnectionInterface with a captured send() buffer. Don't spin up real sockets in unit tests.
->run() from an HTTP request hangs an FPM worker forever. If you see requests time out after the deploy, someone wired a controller into the server.DB::select, file_get_contents, or sleep() inside onMessage stalls the entire event loop. Offload I/O to async clients (clue/reactphp-*) or push work to Laravel's queue and deliver the result back through Redis pub/sub.onError for internal socket/parser failures, but an exception thrown from your onMessage can tear down the connection or crash the loop. Wrap every handler body in try/catch and close the offending connection explicitly.onClose must detach from SplObjectStorage collections, room arrays, timers, and any registry you keep. Add a $loop->addPeriodicTimer(60, ...) that logs count($clients) and memory_get_usage() — unbounded growth here is the classic failure mode after a few days of uptime.$conn->send() writes straight into the React stream buffer. Broadcasting large payloads to slow clients balloons memory. Cap payload sizes yourself (strlen($msg) > N → close), drop slow consumers, and keep the underlying React connection reference if you want to honor the drain event.$conn->httpRequest->getHeaderLine('Origin') in onOpen (or use Ratchet\Http\OriginCheck) and allowlist explicitly.proxy_http_version 1.1, proxy_set_header Upgrade $http_upgrade, proxy_set_header Connection "upgrade", and a long proxy_read_timeout. Without the upgrade headers the handshake 400s. The peer address will be the proxy — read X-Forwarded-For from $conn->httpRequest, not the socket.0.0.0.0 vs 127.0.0.1 is the difference between working and "connection refused" through Docker/containers. Verify with ss -ltnp | grep 8080.cboden/ratchet — fixes are usually PHP 8.2+ deprecation patches, not upstream features. Pin the version in composer.json and check that Composer isn't also pulling cboden/ratchet transitively (the fork should replace it; if you see duplicate Ratchet\ classes, add an explicit conflict or top-level require).stderr while developing; log $conn->resourceId on every open/close/message so you can correlate client reports with server logs. Under Supervisor, stopsignal=TERM plus the SIGTERM handler above gives clean shutdown instead of Supervisor's SIGKILL after stopwaitsecs.enableKeepAlive() handles ping/pong but tune the interval — aggressive values flap mobile clients.Ratchet\Wamp\WampServer gives you topics and RPC without building rooms yourself, at the cost of requiring a WAMP client library on the frontend. For a single Laravel app with simple rooms, hand-rolled is usually less friction.How can I help you explore Laravel packages today?