m6web/wsclient-bundle
A Symfony bundle that provides a WebSocket client service, letting your application connect to and send messages to WebSocket servers. Includes configurable connections, event dispatching, and logging integration for real-time features.
m6web/wsclient-bundle is a Symfony bundle wrapper around m6web/wsclient, a ReactPHP-based WebSocket client (Ratchet RFC6455 under the hood). There is no Laravel service provider, no auto-discovery, and no Laravel-facing API — so from a Laravel codebase you either (a) write a thin bridge that pulls the configured WsClient out of the bundle's container, or (b) skip the bundle and depend on m6web/wsclient directly, which is usually simpler.
The package is on an active 5.x line (current patch: 5.4.1), so treat version constraints as real rather than as an artefact of a dead package — but also verify them, because the constraints in composer.json are the only authority on which PHP/Symfony versions you can actually install alongside Laravel:
composer show m6web/wsclient-bundle # read requires/conflicts before you commit
composer why-not m6web/wsclient-bundle 5.4.1
Minimal steps:
composer require m6web/wsclient-bundle:^5.4.1
If the resolver fights with your Laravel/Symfony-bridge versions, that conflict is real — don't paper over it with --ignore-platform-reqs on a production build.M6Web\Bundle\WsClientBundle\M6WebWsClientBundle in bundles.php/AppKernel and define a client:
m6web_ws_client:
clients:
default:
url: "ws://127.0.0.1:8080"
The bundle exposes a client factory service and per-client services keyed by name.Extension/Configuration classes are not wired into Laravel's container):
// app/Providers/WsClientServiceProvider.php
public function register(): void
{
$this->app->singleton(WsClientFactory::class, function () {
// Confirm the exact class + constructor signature in
// vendor/m6web/wsclient-bundle/DependencyInjection/M6WebWsClientExtension.php
return new WsClientFactory(React\EventLoop\Factory::create());
});
}
Where to look first in vendor/:
m6web/wsclient-bundle/DependencyInjection/Configuration.php — real config keys and defaults..../DependencyInjection/M6WebWsClientExtension.php — service IDs and factory wiring.m6web/wsclient/src/ — the actual WsClient class, its on* methods and event names, plus ResponseAdapterInterface (see Gotchas). The bundle's README is thin; the underlying library is the source of truth.CLI worker with a bounded loop. The client needs a React event loop, and the loop must be driven explicitly. Keep it inside a command/job, never in a request handler:
class WsListenCommand extends Command
{
protected $signature = 'ws:listen {url=ws://127.0.0.1:8080}';
public function handle(WsClientFactory $factory): int
{
$loop = React\EventLoop\Factory::create();
$client = $factory->createClient($this->argument('url'));
$client->onMessage(function ($payload) {
Log::channel('ws')->info('frame', ['payload' => $payload]);
});
$loop->addTimer(30, fn () => $loop->stop()); // hard stop for cron/supervisor
$client->connect();
$loop->run();
return self::SUCCESS;
}
}
Event-driven decoupling. The bundle's selling point is dispatching Symfony events on open/message/close/error. The cleanest Laravel analogue is to keep the client dumb and dispatch Laravel events from the listeners, so handlers (SendHeartbeat, PersistQuote, BroadcastToPusher) don't know about WebSockets:
$client->onMessage(function ($raw) {
event(new WsFrameReceived(json_decode($raw, true)));
});
If you want to reuse the bundle's Symfony EventDispatcher listeners, wrap them once in a small adapter class; don't scatter if (app()->bound('m6web_ws_client...')) checks through your code.
Named clients for multi-endpoint apps. Configure one client per upstream (e.g. quotes, orders) and resolve by name. For per-tenant URLs, prefer the factory at runtime — but cache the loop as a singleton, since creating multiple loops in one process leads to "which loop is running?" bugs.
Reconnect with backoff. There is no robust reconnect in this library. Implement it in the command:
$attempt = 0;
$connect = function () use (&$connect, &$attempt, $client, $loop) {
$client->connect();
$attempt++;
$loop->addTimer(min(30, 2 ** $attempt), $connect); // called on close
};
Reset $attempt on a successful open, and add jitter. Exit non-zero on unrecoverable errors so Supervisor restarts the process.
Queue boundary. For outbound messages triggered by HTTP requests (e.g. "notify the socket service"), push a job onto a queue and let the long-running worker send(). Never open a socket synchronously inside Controller — the handshake blocks the FPM worker.
Pin the patch, then read the diff. The 5.4.x line has already shipped one interface regression (see Gotchas). Depend on ^5.4.1, commit the lock file, and read CHANGELOG/diff on every patch bump — the version numbers alone won't tell you whether an interface gained or lost methods.
Testing seam. Wrap the client behind your own WsClientInterface with connect/send/close. Unit-test message handlers by calling them directly with fixture frames; only one integration test should spin up a local Ratchet/websocketd echo server.
ResponseAdapterInterface retrocompatibility; 5.4.1 restores it. The previous release removed methods from the interface and this patch puts them back. Two consequences: (a) if you pinned 5.4.0 and implemented a custom response adapter, your class may now be expected to provide the restored methods — run your adapter tests after upgrading; (b) if you "fixed" your implementation by deleting methods to satisfy 5.4.0, revert that before bumping to 5.4.1. Upgrade to ^5.4.1, don't sit on 5.4.0.ResponseAdapterInterface implementations thin (delegate rather than reimplement) so a signature change is a one-file edit.config/ is your own convention, and the bundle's YAML config is irrelevant unless you're actually on Symfony. Budget for the bridge code.sleep() inside a message handler stalls the socket and can drop your heartbeat/close frames. Offload slow work to a queue.m6web_ws_client.message — grep vendor/m6web/wsclient for the constants or string event names; they shifted between minor versions.wss://) depends on the underlying React socket/SSL context. Verify peer settings and that your CA bundle is current; failures surface as opaque close codes.gc_collect_cycles() periodically. Framework state leaking across messages is a classic source of "works in tinker, breaks at 3am."$loop->addPeriodicTimer(5, ...) heartbeats, log open/close/error events with the WebSocket close code, and run against websocketd or a local Ratchet echo server before blaming the remote. When a new patch lands, re-run the smoke command first — it exercises the adapter path that broke in 5.4.0.WsClient, swap the injected event dispatcher for a Laravel Dispatcher adapter, or plug your own ResponseAdapterInterface implementation. The bundle's Configuration.php is the place to see what's actually overridable.How can I help you explore Laravel packages today?