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

Thruway Laravel Package

voryx/thruway

Thruway is a PHP implementation of the WAMP v2 protocol, providing a router and client built on ReactPHP. It enables WebSocket-based pub/sub and RPC for real-time PHP applications, with authentication and flexible transport support.

Deep Wiki
Context7

Getting Started

Thruway is a WAMP v2 implementation for PHP built on ReactPHP: it gives you PubSub (topics) and RPC (procedures) over WebSockets. It ships both halves — Thruway\Connection (client, on top of Pawl) and Thruway\Peer\Router (router, on top of Ratchet) — so you can point it at Crossbar/other WAMP routers or run your own.

composer require voryx/thruway

Smallest useful thing — a client subscribing and publishing:

use Thruway\ClientSession;
use Thruway\Connection;

$connection = new Connection([
    'realm' => 'realm1',
    'url'   => 'ws://127.0.0.1:9090',
]);

$connection->on('open', function (ClientSession $session) {
    $session->subscribe('app/orders/created', function ($args, $argsKw, $details) {
        // $args[0] is whatever the publisher put in position 0
    });

    $session->publish('app/orders/created', [['id' => 42]], [], ['acknowledge' => true]);
});

$connection->open(); // blocks: this runs the ReactPHP loop

Smallest useful thing — a router:

use Thruway\Peer\Router;
use Thruway\Transport\RatchetTransportProvider;

$router = new Router();
$router->addTransportProvider(new RatchetTransportProvider('0.0.0.0', 9090));
$router->start();

Where to look first, in order:

  1. Thruway\ClientSession — subscribe, publish, register, call, getSessionId, getLoop. This is 90% of your day.
  2. Thruway\Connection — the lifecycle (open/close events, options array, authenticator).
  3. Thruway\Peer\Router + Thruway\Transport\RatchetTransportProvider — only if you host the router.
  4. Thruway\Logging\Logger — set this immediately, before anything else. See Gotchas.

The realistic first use case in a Laravel app: a long-running Artisan command (Supervisor-managed) that registers WAMP procedures and subscribes to topics, while browser clients talk to the same router with AutobahnJS. You are not using this from a normal FPM HTTP request — the connection is a persistent socket with an event loop attached.


Implementation Patterns

1. Wrap the router/client in an Artisan command, not a service provider

There is no first-party Laravel integration package. Wire it yourself:

class ThruwayServeCommand extends Command
{
    protected $signature = 'thruway:serve {--realm=realm1} {--port=9090}';

    public function handle(): int
    {
        $router = new Router();
        $router->addTransportProvider(
            new RatchetTransportProvider('0.0.0.0', (int) $this->option('port'))
        );

        $this->registerModules($router); // auth, custom RouterModuleInterface modules

        $router->start(); // blocking; Supervisor restarts it
        return self::SUCCESS;
    }
}

Put connection options in config/thruway.php and inject them; don't hardcode URLs. Run under Supervisor with --timeout=0-style settings (no PHP max_execution_time issues in CLI, but you do need restart-on-deploy since code is loaded once).

2. PubSub as an event fan-out from Eloquent to the browser

The clean pattern is a Laravel event listener that publishes onto WAMP, plus a thin publisher held in the container:

class WampPublisher
{
    public function __construct(private Connection $connection) {}

    public function publish(string $topic, array $args, array $kwargs = []): PromiseInterface
    {
        return $this->connection->getSession()->publish($topic, $args, $kwargs, [
            'acknowledge' => true,   // required if you want the promise to mean anything
        ]);
    }
}
// app/Listeners/PublishOrderCreated.php
public function handle(OrderCreated $event): void
{
    $this->wamp->publish('app/orders/created', [
        ['id' => $event->order->id, 'total' => $event->order->total],
    ], ['tenant' => $event->order->tenant_id]);
}

Browser side (AutobahnJS) subscribes to app/orders/created and gets the same payload. WAMP argument/kwarg separation is genuinely useful here: positional args for the "shape" of the event, kwargs for filters (tenant, user_id).

Targeted delivery uses options rather than app-level filtering:

$session->publish('app/chat', [$msg], [], [
    'exclude'     => [$session->getSessionId()], // don't echo to self
    'eligible'    => $sessionIds,                 // only these sessions
    'exclude_me'  => true,
    'disclose_me' => true,                        // reveal publisher identity to subscribers
]);

3. RPC: register procedures on the PHP worker, call them from anywhere

$session->register('app.orders.cancel', function ($args, $argsKw, $details) {
    $order = Order::findOrFail($args[0]);

    if (! $order->isCancellable()) {
        throw new WampErrorException('app.error.not_cancellable', [$order->id]);
    }

    $order->cancel();

    return new CallResult([$order->id], ['status' => 'cancelled']); // args + kwargs
});

Rules that matter:

  • Return a plain scalar/array → it becomes the positional argument list. return [$a, $b] gives the caller two args, not one array.
  • Return new Thruway\CallResult($args, $kwargs) when you need keyword results.
  • Throw Thruway\Exception\WampErrorException($uri, $args, $kwargs) to send a proper WAMP ERROR frame with an application error URI. Anything else surfaces as a generic error and may log noisily.
  • Registered procedures can return a React promise — useful for async I/O without blocking the loop.
  • Caller identity lives in $details->caller only if the registration was made with ['disclose_caller' => true] and the router allows it.

Client side:

$session->call('app.orders.cancel', [42], [], [], 5) // 5s timeout
    ->then(
        fn ($result) => $result,          // $result is an array of args
        fn ($error)  => report($error)    // Thruway\ClientSession::call error
    );

4. Subscription groups for bulk lifecycle management

When a session needs to attach/detach a set of topics as a unit (e.g., "user entered the dashboard"), use Thruway\Subscription\SubscriptionGroup rather than tracking individual subscription IDs. In 0.5+/0.6 subscribe() and register() return promises resolving to a Subscription/Registration, so grab the handle with ->then() when you need it:

$group = new SubscriptionGroup('dashboard');
$session->subscribe('app.orders/created', $handler)->then(
    fn ($subscription) => $subscription->addToGroup($group)
);

5. Authentication

Router side:

$authMgr = new AuthenticationManager();
$authMgr->addAuthProvider(new WampCraAuthProvider([
    'service-worker' => config('thruway.secret'),
]));
$router->setAuthenticationManager($authMgr);

Client side, pass the authenticator through the options array:

$connection = new Connection([
    'realm'         => 'realm1',
    'url'           => 'ws://127.0.0.1:9090',
    'authid'        => 'service-worker',
    'authenticator' => new ClientWampCraAuthenticator('service-worker', $secret),
]);

TicketAuthProvider/ClientTicketAuthenticator is the simpler alternative when you just want to validate a bearer token you already issued from Laravel. For a ws:// endpoint exposed to browsers, terminate TLS at nginx and use wss:// on the client side — WAMP-CRA over plain ws:// sends the challenge response in the clear.

6. Share WAMP data with your domain, not the other way around

Keep Thruway at the edges: ClientSession handlers should do the minimum (validate, dispatch a job, publish). Don't run a Model::find() storm or a synchronous third-party HTTP call inside a handler — you're on the React event loop and every subscriber on that session waits.

7. Testing

Boot the router and client in one process and drive the loop manually instead of shelling out to a server:

$router = new Router();
$router->addTransportProvider(new RatchetTransportProvider('127.0.0.1', 9091));
$router->start(false); // attach to the default loop without running it

$connection = new Connection(['realm' => 'realm1', 'url' => 'ws://127.0.0.1:9091']);
$connection->on('open', function (ClientSession $session) use ($connection) {
    // assertions...
    $connection->close();
});
$connection->open(); // runs the loop, which already has the router on it

Gotchas and Tips

Logging is off by default. Thruway\Logging\Logger ships with a NullLogger. Every "my registration silently does nothing" story starts here. One line, early in your boot:

Logger::set(new \Monolog\Logger('thruway', [new \Monolog\Handler\StreamHandler('php://stderr')]));

publish() resolves truthlessly without acknowledge. The promise resolves as soon as the frame is written unless you pass ['acknowledge' => true]. If you're using the promise to detect delivery failure, you need that option — and even then it only confirms router acceptance, not subscriber processing.

The default logger hides protocol-level close reasons. When a session dies, you get close with a $reason that maps to WAMP close codes (wamp.error.no_such_realm, wamp.error.not_authorized, …). Log it verbatim; it's the fastest path to auth/realm misconfiguration.

Blocking calls freeze everything. sleep(), ->wait(), sync HTTP clients, DB::transaction with lock waits — all of it stalls the loop, and every session multiplexed on that process stalls with it. Use $session->getLoop()->addTimer() for delays, and push heavy work to a queue (dispatch() then publish from the worker).

Never resolve Connection from the container inside an HTTP request. It's a stateful socket; binding it as a singleton in a ServiceProvider and using it from FPM gives you a connection that may be closed, half-open, or shared across requests. Keep it inside the CLI worker/command.

Reconnection is your job. After a reconnect, the router has forgotten you: re-subscribe and re-register on every open, not just the first one. Put subscriptions/registrations in a bootSession(ClientSession $session) method called from the open handler — that's the only version that survives a network blip. Watch for duplicate registration if you reconnect without the old session being cleaned up.

Return shape is the #1 source of "why is my caller getting weird args". return ['a' => 1] gives one positional argument (an array); return [1, 2] gives two. Use CallResult for kwargs, and return null → empty args.

Uncaught exceptions in handlers can take down the worker. Wrap handlers; convert domain failures to WampErrorException so the caller gets a meaningful ERROR and the process keeps running.

Everything crosses the wire as JSON. No PHP objects, no DateTime instances (serialize explicitly), no resource handles. Binary payloads need base64 or a WAMP binary option — don't assume it "just works".

Result/argument IDs are 53-bit integers. Fine in PHP, but avoid hand-crafting IDs; let Thruway allocate them.

RatchetTransportProvider binds 0.0.0.0 and has no built-in rate limiting. Put it behind nginx (proxy_read_timeout raised well above your ping interval, or idle sockets get cut at 60s), and if you scale to multiple routers, remember WAMP state is per-router: you need sticky sessions or a cross-router bridge, or a subscription to the same topic on each instance.

Maintenance reality check. Last release was Feb 2022. It works, but you're pinning ratchet/pawl, react/event-loop, and ratchet/rfc6455 transitively — expect PHP 8.x deprecation noise (dynamic properties) and audit the lockfile when you bump. If you only need PubSub and are already on Laravel, Reverb/Pusher is the lower-risk default; reach for Thruway when you specifically want RPC to PHP workers over the same socket or WAMP interop with Crossbar/Autobahn.

Extension points worth knowing: implement Thruway\Module\RouterModuleInterface with initModule(Router $router) + getSubscribedEvents() to add router-side behavior (session tracking, procedure ACLs) without forking; custom AbstractAuthProvider subclasses for Laravel token validation; and the wamp.registration.* / wamp.session.* meta topics for admin dashboards — check which meta events your version actually emits before designing around them.

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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator