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

Web Socket Bundle Laravel Package

gos/web-socket-bundle

A Symfony bundle for adding real-time WebSocket features to your app. It provides a Ratchet-based server, topic-based publish/subscribe, RPC, authentication, and Symfony service integration for easy bidirectional communication.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up (no Flex recipe — do this by hand):

composer require gos/web-socket-bundle

Symfony 4/5/6: register Gos\Bundle\WebSocketBundle\GosWebSocketBundle in config/bundles.php (if not auto-registered), then create config/packages/gos_web_socket.yaml:

gos_web_socket:
    server:
        host: 127.0.0.1
        port: 8080
    # topic/rpc/periodic services tagged below are picked up automatically

Write one Topic — this is the unit of work for everything the client subscribes to:

namespace App\WebSocket;

use Gos\Bundle\WebSocketBundle\Topic\TopicInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Wamp\Topic;

class ChatTopic implements TopicInterface
{
    public function onOpen(ConnectionInterface $connection): void {}
    public function onClose(ConnectionInterface $connection): void {}

    public function onSubscribe(ConnectionInterface $connection, Topic $topic): void
    {
        $topic->broadcast(['msg' => $connection->resourceId . ' joined']);
    }

    public function onUnsubscribe(ConnectionInterface $connection, Topic $topic): void {}

    public function onPublish(ConnectionInterface $connection, Topic $topic, $event, array $exclude, array $eligible): void
    {
        $topic->broadcast($event);
    }

    public function getName(): string
    {
        return 'chat'; // client subscribes to "chat"
    }
}

Register it (autoconfigure handles the tag if you use services.yaml defaults):

App\WebSocket\ChatTopic:
    tags: ['gos_web_socket.topic']

Run the daemon:

php bin/console gos:websocket:server -vvv

Connect from the browser. The bundle ships an Autobahn.JS-based client under Resources/public/js. Run php bin/console assets:install --symlink and include @GosWebSocketBundle/client.js (or pull Autobahn from a CDN yourself), then:

const socket = gosSocket();           // or new GosSocket({...})
socket.connect('ws://127.0.0.1:8080');
socket.subscribe('chat', (data) => console.log(data.msg));

First use case to prove the wiring: a notifications channel. On onSubscribe, inject ClientManipulatorInterface, look up the user from the session ($connection->Session), and $topic->broadcast() a "hello" — you've now validated auth, topic routing and the daemon in one pass.

Where to look first: read TopicInterface and RpcInterface in src/, then Server/Type/WebSocketServer.php (the daemon) and Resources/doc/ (the docs are actually thorough). Run debug:config gos_web_socket and debug:container gos_web_socket to see the real service IDs and keys for your installed version — they changed across 2.x → 3.x.

Implementation Patterns

Keep Topics thin, delegate to domain services. A Topic is a transport adapter, not a place for business logic. Inject your existing service (NotificationService, MessageRepository) and have onPublish validate and hand off. This is what makes the websocket code testable — you can unit-test the Topic with a mocked ConnectionInterface and Topic.

Push from the HTTP side with the pusher. Anything happening in a controller, console command, or Doctrine event listener can reach connected clients:

use Gos\Bundle\WebSocketBundle\Pusher\PusherInterface;

class OrderController
{
    public function __construct(private PusherInterface $pusher) {}

    public function ship(Order $order)
    {
        $this->pusher->push(
            ['orderId' => $order->getId(), 'status' => 'shipped'],
            'orders'  // topic name
        );
    }
}

The pusher sends over WAMP (HTTP to the running daemon) or ZMQ depending on gos_web_socket.pusher config. For local dev the WAMP driver is fine; for anything with more than one app server, switch to the ZMQ driver so each PHP-FPM node can publish into the socket process without holding a WAMP connection open.

Target a specific user, not a topic. Inject ClientManipulatorInterface inside the daemon to map users ↔ connections:

foreach ($this->clientManipulator->getAll() as $client) {
    if ($client->getUser()->getId() === $userId) {
        $client->getConnection()->send(json_encode($payload));
    }
}

This is the pattern for private messages and "you have a new notification" badges.

Bridge Symfony events to the socket. A tiny listener on kernel.terminate (or a Messenger handler) that collects domain events and calls the pusher keeps websocket concerns out of your entities. Don't push inside postPersist synchronously — a slow/absent daemon will hang the request.

Use RPC for request/response, Topics for fan-out. Implement RpcInterface (getName() + call(ConnectionInterface $conn, $params)) when the client needs a result (e.g. "load last 50 messages on subscribe"). The JS side is socket.call('chat', 'history', {since: ts}, cb).

Session-based auth is the default path. Configure the session handler in gos_web_socket.clients / firewall name, then in onSubscribe read $connection->Session (a SessionProviderInterface implementation). This lets you authorize per-topic at subscribe time and reject with $connection->close() — do that before adding the connection to the topic.

Periodic services for heartbeats/presence. Implement PeriodicInterface and tag gos_web_socket.periodic; run() fires every tick. Use it to sweep stale presence entries or ping your DB, not for per-user logic.

Client-side pattern: subscribe once per page section, and always keep the returned subscription handle so you can unsubscribe() on teardown. Autobahn reconnects are your responsibility — wrap connect in a retry with backoff and re-subscribe on onopen.

Gotchas and Tips

  • It's a long-running process — code changes need a restart. No opcache invalidation, no container rebuild, no .env reload. Development means Ctrl-C + gos:websocket:server again. Plan for that in your workflow.
  • Database connections go stale. After minutes of idleness MySQL kills the connection and every subsequent query throws "server has gone away". Handle it in your periodic service ($em->getConnection()->close(); $em->getConnection()->connect();) or wrap queries in a reconnect retry.
  • No request context in the daemon. RequestStack is empty, TokenStorage is empty, kernel.request never fires. Anything you'd normally get from the request must come from $connection->Session or be passed in the message payload.
  • Sessions are read once, at handshake. If a user logs out or their roles change, the open connection keeps the old identity. For anything security-sensitive, re-check permissions on each onPublish, not just at subscribe.
  • Auth requires the session cookie on the handshake. Same-origin ws://host:8080 works; cross-origin or wss:// behind a proxy often doesn't, because the PHPSESSID cookie isn't sent. Serve the socket on a subdomain with the same cookie domain, or pass an explicit token in the subscribe payload.
  • Behind nginx you need Upgrade headers. proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; plus proxy_read_timeout well above your heartbeat interval, or connections die silently after 60s.
  • Origin checking. Ratchet's origin check rejects browser connections when the Origin header doesn't match the configured host — a classic "works with wscat, fails in Chrome" bug. Verify via debug:config gos_web_socket and the -vvv daemon log.
  • Scaling is manual. One process = one core. To run multiple, you need the ZMQ pusher (so app servers publish to a single bus), sticky sessions or a shared presence store, and a load balancer that speaks WebSocket. There is no built-in cluster mode.
  • Service IDs drift between versions. gos_web_socket.wamp.pusher vs gos_web_socket.pusher, gos_web_socket.client.manipulator vs ClientManipulatorInterface — always resolve by interface (PusherInterface, ClientManipulatorInterface) via constructor injection rather than by string ID.
  • WAMP v1 only. The bundled Autobahn client speaks WAMP 1; there's no WAMP v2 / router interop. If you need modern protocol semantics or PHP 8.2+ longevity guarantees, this is the main reason to consider Mercure or Centrifugo instead.
  • Last release 2022 and Ratchet is effectively unmaintained. It works, but pin your PHP version, test upgrades deliberately, and keep the daemon behind your own process supervisor (systemd/supervisord) with restart-on-failure — don't rely on the bundle for resilience.
  • Debugging: run the daemon in the foreground with -vvv; the log tells you handshake rejections, topic name mismatches and pusher failures. ConnectionInterface::$resourceId and $connection->WAMP->sessionId are your only identifiers — log them on open/close to trace client churn.
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