Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Wsclient Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. Install, pinning the patch you have actually tested:
    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.
  2. If you're in a Symfony app, register 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.
  3. In Laravel, bind it manually (no bundle registration exists, and the bundle's 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());
        });
    }
    
  4. First use case: an artisan command that connects to an echo/staging WebSocket, sends a JSON payload, prints inbound frames until a timer or close event, then exits. That's the "does the whole stack work" smoke test before wiring it into a job or listener.

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.

Implementation Patterns

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.

Gotchas and Tips

  • 5.4.0 broke 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.
  • Interface churn is a signal, not a one-off. A patch-level fix for a removed-method BC break means the adapter layer isn't covered by a strict semver guarantee in practice. Keep your own ResponseAdapterInterface implementations thin (delegate rather than reimplement) so a signature change is a one-file edit.
  • It is not a Laravel package. There is no provider, no facade, no config publish. Everything in config/ is your own convention, and the bundle's YAML config is irrelevant unless you're actually on Symfony. Budget for the bridge code.
  • The loop is blocking. Anything that touches the DB, HTTP, or sleep() inside a message handler stalls the socket and can drop your heartbeat/close frames. Offload slow work to a queue.
  • Event names live in the underlying library, not the bundle. Don't guess m6web_ws_client.message — grep vendor/m6web/wsclient for the constants or string event names; they shifted between minor versions.
  • Headers/auth are limited by the underlying client version. If it doesn't forward custom headers on the upgrade request, you're left with a query-string token or a first-frame auth message. Document whichever you pick; it's an audit finding either way.
  • TLS (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.
  • Long-running daemon hygiene. In a persistent Laravel worker, disconnect DB connections, clear scoped bindings, and gc_collect_cycles() periodically. Framework state leaking across messages is a classic source of "works in tinker, breaks at 3am."
  • Exceptions inside callbacks kill the loop silently. Wrap every handler in try/catch and log with a dedicated channel including the frame payload and close code.
  • Debugging tips: add $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.
  • Extension points are narrow but present: decorate the factory service, subclass 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.
  • License is MIT, so forking/patching is legally fine. With a maintained upstream, prefer a thin local adapter over a fork — but if a future release repeats the 5.4.0 pattern, vendoring just the client and adapter classes into your app remains a defensible escape hatch.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core