voryx/thruway-bundle
Symfony bundle integrating Thruway, a WAMP v2 client/router, to add real-time RPC and Pub/Sub to Symfony apps. Expose PHP services over WebSocket, publish and subscribe to events, and build reactive features with minimal configuration.
Read this first: voryx/thruway-bundle is a Symfony 2 bundle, not a Laravel package. There is no service provider, facade, or php artisan vendor:publish path here. If you're on a Laravel stack, you cannot composer require this and expect it to boot — the value is entirely in its Symfony DI wiring (a Bundle class, services.yml extension, compiler passes, and a thruway:router console command). Treat this document as guidance for (a) the Symfony 2 app you may still own, and (b) the pattern you'd reimplement on Laravel's broadcasting stack.
Where to look first: the Resources/config/ directory (the service definitions that wire the router), the Command/ directory (the long-running thruway:router command), and the Annotation/ folder (@Register, @Subscribe, @Topic). That trio contains 90% of what you'll touch day-to-day.
Minimal install (Symfony 2 target):
composer require voryx/thruway-bundle
// app/AppKernel.php
public function registerBundles()
{
$bundles = [
// ...
new Thruway\Bundle\ThruwayBundle(),
];
return $bundles;
}
# app/config/config.yml
thruway:
realm: "realm1"
router:
ip: "0.0.0.0"
listen_port: 9090
Run the router as a long-running daemon — never via php-fpm or a web request. It blocks and holds a WebSocket listener open:
php bin/console thruway:router
First use case — expose a service method as a callable RPC procedure:
namespace AppBundle\Wamp;
use Thruway\Bundle\Annotation\Register;
class Calculator
{
/** @Register("com.example.add") */
public function add($args, $kwargs, $session)
{
return [$args[0] + $args[1]]; // result must be an array of positional values
}
}
Consume it from the browser using autobahn.js (the reference WAMP client):
const conn = new autobahn.Connection({ url: 'ws://localhost:9090/', realm: 'realm1' });
conn.onopen = (session) => {
session.call('com.example.add', [2, 3]).then(r => console.log(r)); // [5]
};
conn.open();
First-use pitfall to plan for: the @Register/@Subscribe annotations are discovered via the Symfony container and cached. In dev you'll need to clear the cache after adding an annotation (php bin/console cache:clear); otherwise the procedure silently won't exist on the router.
1. RPC services as thin adapters over domain services. Don't put business logic in the annotated method — keep the annotated class as a boundary that calls an injected domain service. The annotated method signature is fixed ($args, $kwargs, $session), and returning arrays that are the wrong shape is the most common source of confused clients.
class OrderWampService
{
private $orderService;
public function __construct(OrderService $orderService) { $this->orderService = $orderService; }
/** @Register("com.example.order.status") */
public function status($args, $kwargs, $session)
{
$order = $this->orderService->find($args[0]);
return [['id' => $order->getId(), 'state' => $order->getState()]];
}
}
2. Pub/sub for server→client fan-out. Subscribe-in and publish-out are both annotation-driven; the Topic object returned/held is how you push events. Broadcast on order state changes:
/** @Subscribe("com.example.order.created") */
public function onOrderCreated($args, $kwargs, $session)
{
// handle inbound event
}
Publish outbound from anywhere with the topic object the bundle injects:
/** @Topic("com.example.order.updated") */
private $topic;
public function pushUpdate(Order $order)
{
$this->topic->publish([$order->toArray()]);
}
3. Auth via session hooks. The bundle exposes SessionAuthenticateEvent / AuthorizationManager so you can gate realms and topics. Attach a listener that checks a token from the WAMP welcome details. This is where you centralize "can this session subscribe to com.example.order.updated".
4. Use an external Crossbar.io router for anything serious. The thruway:router command is fine for a single-node demo. Crossbar is the production-grade WAMP router; point the bundle's router config at it and stop worrying about router scaling/failover. The bundle then behaves as a client integration only.
5. Wire the Symfony event dispatcher both ways. The bundle bridges WAMP messages to kernel events. If you want to react to an application event with a broadcast, listen on your domain event and call $topic->publish(...) — don't build a parallel event system.
6. Laravel translation pattern. On Laravel you don't port this package; you port its topology. RPC → a controller/job endpoint; pub/sub → Broadcast::on()/channels with Reverb or a Pusher-compatible service; auth → Laravel broadcast channel authorization (Broadcast::channel). The one thing WAMP gives you that Laravel's stack doesn't natively is bidirectional RPC over the same connection — if you genuinely need that, use the voryx/thruway client directly inside an Octane worker behind a narrow internal interface, not this Symfony bundle.
7. Supervise the worker. Wrap thruway:router with systemd/supervisord. The event loop does not self-heal — a crashed router drops all sessions.
composer require will likely fail outright, and forcing an install leaves unpatched transitive dependencies on a network-facing port.[null]-ish result — always return [$value] or [['key' => $value]].@Register/@Subscribe/@Topic requires clearing the Symfony cache so the compiler pass re-discovers it. This is the top cause of "my procedure isn't there."router.ip/listen_port only matter when using the built-in thruway:router; if you point the bundle at an external Crossbar instance, those settings are inert and the URL lives in your client config. Don't waste a debugging afternoon assuming they're still in play.thruway:router output directly — the Symfony web debug toolbar does nothing for a console worker. Confirm the router emits the WAMP WELCOME frame before chasing handler bugs.SessionAuthenticateEvent (auth) and the realm config are the two you'll customize in real deployments. Everything else is glue you'd fork rather than configure if the dependency is frozen.How can I help you explore Laravel packages today?