hermod / laravel-wamp
A modern WAMP client for Laravel 12+
Requires
- php: ^8.2
- amphp/amp: ^3.0
- amphp/websocket-client: ^2.0
- rybakit/msgpack: ^0.10.0
- spomky-labs/cbor-php: ^3.2
Requires (Dev)
- laravel/pint: ^1.0
- nunomaduro/larastan: ^3.9
- orchestra/testbench: ^10.0
- pestphp/pest: ^3.0
- phpstan/phpstan: ^2.0
Suggests
- ext-msgpack: Required to use the MessagePack serializer
README
Hermod is a modern WAMP v2 client for Laravel 12+, built on AMPHP v3 and PHP Fibers. Born as an actively maintained alternative to Thruway, it supports RPC (Caller/Callee) and PubSub with JSON serialization, MessagePack, and CBOR.
In Norse mythology, Hermod is the messenger of the gods — the one who carries messages between realms.
Requirements
- PHP 8.2+
- Laravel 12.x+
- A WAMP v2 router (e.g., Crossbar.io)
Installation
composer require hermod/laravel-wamp
Publish the configuration:
php artisan vendor:publish --tag=hermod-config
Configuration
Add the variables to your .env:
# Connection WAMP_URL=ws://localhost:8080/ws WAMP_REALM=realm1 WAMP_SERIALIZER=json # Authentication (anonymous by default) WAMP_AUTH_METHOD=anonymous # Automatic reconnect WAMP_RECONNECT=true WAMP_RECONNECT_MAX=5 WAMP_RECONNECT_DELAY=1 WAMP_RECONNECT_MAX_DELAY=30 WAMP_RECONNECT_MULTIPLIER=2 # Heartbeat WAMP_HEARTBEAT=true WAMP_HEARTBEAT_INTERVAL=30
Ticket Authentication
WAMP_AUTH_METHOD=ticket WAMP_AUTH_ID=myuser WAMP_AUTH_TICKET=my-secret-token
Authentication with WAMP-CRA
WAMP_AUTH_METHOD=wampcra WAMP_AUTH_ID=myuser WAMP_AUTH_SECRET=my-secret
Multiple connections in config/hermod.php
'connections' => [ 'default' => [ 'transport' => 'websocket', 'url' => env('WAMP_URL', 'ws://localhost:8080/ws'), 'realm' => 'realm1', 'serializer' => 'json', 'auth' => ['method' => 'anonymous'], 'reconnect' => ['enabled' => true, 'max_attempts' => 5], 'heartbeat' => ['enabled' => true, 'interval' => 30], ], 'local_tcp' => [ 'transport' => 'rawsocket', 'url' => 'tcp://localhost:8081', 'realm' => 'realm1', 'serializer' => 'msgpack', 'auth' => ['method' => 'anonymous'], 'reconnect' => ['enabled' => true, 'max_attempts' => 5], 'heartbeat' => ['enabled' => true, 'interval' => 30], ], 'local_unix' => [ 'transport' => 'rawsocket', 'url' => 'unix:///var/run/crossbar/router.sock', 'realm' => 'realm1', 'serializer' => 'cbor', 'auth' => ['method' => 'anonymous'], 'reconnect' => ['enabled' => true, 'max_attempts' => 5], 'heartbeat' => ['enabled' => true, 'interval' => 30], ], 'secure' => [ 'transport' => 'websocket', 'url' => env('WAMP_SECURE_URL', 'wss://router.example.com/ws'), 'realm' => 'secure_realm', 'serializer' => 'cbor', 'auth' => [ 'method' => 'wampcra', 'authid' => env('WAMP_AUTH_ID'), 'secret' => env('WAMP_AUTH_SECRET'), ], 'reconnect' => ['enabled' => true, 'max_attempts' => 10], 'heartbeat' => ['enabled' => true, 'interval' => 20], ], ],
Usage Examples
RPC — Procedure Registration (Callee)
Without Authentication (Anonymous)
In your AppServiceProvider:
use Hermod\Laravel\Events\WampServeStarted; use Illuminate\Support\Facades\Event; class AppServiceProvider extends ServiceProvider { public function register(): void { Event::listen(WampServeStarted::class, function (WampServeStarted $event) { // Simple procedure — add two numbers $event->client->register('com.myapp.somma', function (array $args): int { return $args[0] + $args[1]; }); // Procedure with kwargs — personalized greeting $event->client->register('com.myapp.saluta', function (array $args, array $kwargs): string { $nome = $kwargs['nome'] ?? 'Utente'; $titolo = $kwargs['titolo'] ?? ''; return "Ciao, {$titolo} {$nome}!"; }); // Procedure with access to the Laravel DB $event->client->register('com.myapp.utente.trova', function (array $args): array { $user = \App\Models\User::find($args[0]); return $user?->toArray() ?? []; }); // Procedure with error handling $event->client->register('com.myapp.divisione', function (array $args): float { if ($args[1] === 0) { throw new \InvalidArgumentException('Divisione per zero non consentita.'); } return $args[0] / $args[1]; }); }); } }
Start the worker:
php artisan wamp:serve
With Ticket Authentication
WAMP_AUTH_METHOD=ticket WAMP_AUTH_ID=backend-service WAMP_AUTH_TICKET=supersecrettoken123
The AppServiceProvider remains identical — authentication is transparent.
php artisan wamp:serve
# The client will automatically authenticate with the configured ticket
With WAMP-CRA authentication
WAMP_AUTH_METHOD=wampcra WAMP_AUTH_ID=backend-service WAMP_AUTH_SECRET=my-hmac-secret
php artisan wamp:serve
# The client will automatically calculate the HMAC-SHA256 signature
With specific connection
php artisan wamp:serve --connection=secure
RPC — Procedure Call (Caller)
Synchronous call
use Hermod\Laravel\Facades\Wamp; // In a controller, job, command, etc. Wamp::connect(); // Call with positional arguments $somma = Wamp::call('com.myapp.somma', [3, 5]); // → 8 // Call with kwargs $saluto = Wamp::call('com.myapp.saluta', [], ['nome' => 'Mario', 'titolo' => 'Dr.']); // → "Ciao, Dr. Mario!" // Call with error handling try { $risultato = Wamp::call('com.myapp.divisione', [10, 0]); } catch (\Hermod\Exceptions\RpcException $e) { Log::error("Errore RPC: {$e->getMessage()}", ['wamp_error' => $e->wampError]); } Wamp::disconnect();
Asynchronous call
use Hermod\Laravel\Facades\Wamp; use function Amp\Future\await; Wamp::connect(); // Single asynchronous call $future = Wamp::callAsync('com.myapp.somma', [10, 20]); $result = $future->await(); // → 30 // Parallel calls — executed simultaneously $futures = [ 'somma' => Wamp::callAsync('com.myapp.somma', [1, 2]), 'saluto' => Wamp::callAsync('com.myapp.saluta', [], ['nome' => 'Mario']), 'utente' => Wamp::callAsync('com.myapp.utente.trova', [42]), ]; $risultati = await($futures); // $risultati['somma'] → 3 // $risultati['saluto'] → "Ciao, Mario!" // $risultati['utente'] → ['id' => 42, 'name' => '...'] Wamp::disconnect();
In a Laravel Route
use Hermod\Laravel\Facades\Wamp; use Illuminate\Support\Facades\Route; Route::get('/calcola/{a}/{b}', function (int $a, int $b) { Wamp::connect(); $result = Wamp::call('com.myapp.somma', [$a, $b]); Wamp::disconnect(); return response()->json(['risultato' => $result]); }); Route::get('/utente/{id}', function (int $id) { Wamp::connect(); $utente = Wamp::call('com.myapp.utente.trova', [$id]); Wamp::disconnect(); if (empty($utente)) { return response()->json(['error' => 'Utente non trovato'], 404); } return response()->json($utente); });
From terminal (debug)
# Call with positional arguments php artisan wamp:call com.myapp.somma 3 5 # Calling with kwargs in JSON php artisan wamp:call com.myapp.saluta --kwargs='{"nome":"Mario","titolo":"Dr."}' # Call on specific connection php artisan wamp:call com.myapp.somma 10 20 --connection=secure # Output atteso: # ✓ Risultato (12.5ms): # 30
PubSub — Publisher
Fire and Forget Publication
use Hermod\Laravel\Facades\Wamp; Wamp::connect(); // Positional array → args Wamp::publish('com.myapp.notifiche', ['messaggio importante']); // Associative array → kwargs automatically Wamp::publish('com.myapp.ordini', ['ordine_id' => 123, 'stato' => 'spedito']); // Args and kwargs combined Wamp::publish('com.myapp.eventi', [1, 2, 3], ['extra' => 'info']); Wamp::disconnect();
Publication with acknowledgement
use Hermod\Laravel\Facades\Wamp; Wamp::connect(); // Waiting for PUBLISHED confirmation from the router $publicationId = Wamp::publishWithAck( 'com.myapp.notifiche', ['messaggio' => 'Ordine confermato!'] )->await(); Log::info("Messaggio pubblicato", ['publication_id' => $publicationId]); Wamp::disconnect();
From a Laravel Job
namespace App\Jobs; use Hermod\Laravel\Facades\Wamp; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; class NotificaOrdineSpedito implements ShouldQueue { use Queueable; public function __construct( private readonly int $ordineId, private readonly string $corriere, ) {} public function handle(): void { Wamp::connect(); Wamp::publish('com.myapp.ordini.spediti', [], [ 'ordine_id' => $this->ordineId, 'corriere' => $this->corriere, 'timestamp' => now()->toISOString(), ]); Wamp::disconnect(); } }
Da un Observer Eloquent
namespace App\Observers; use App\Models\Ordine; use Hermod\Laravel\Facades\Wamp; class OrdineObserver { public function updated(Ordine $ordine): void { if ($ordine->isDirty('stato')) { Wamp::connect(); Wamp::publish('com.myapp.ordini.aggiornati', [], [ 'ordine_id' => $ordine->id, 'stato_nuovo' => $ordine->stato, 'stato_vecchio' => $ordine->getOriginal('stato'), ]); Wamp::disconnect(); } } }
PubSub — Subscriber
Subscribing to a topic
In your AppServiceProvider:
use Hermod\Laravel\Events\WampServeStarted; Event::listen(WampServeStarted::class, function (WampServeStarted $event) { // Simple subscription $event->client->subscribe('com.myapp.notifiche', function (array $args, array $kwargs): void { Log::info('Notifica ricevuta', [ 'args' => $args, 'kwargs' => $kwargs, ]); }); // Subscription with business logic $event->client->subscribe('com.myapp.ordini.spediti', function (array $args, array $kwargs): void { $ordineId = $kwargs['ordine_id'] ?? null; $corriere = $kwargs['corriere'] ?? 'sconosciuto'; if ($ordineId) { \App\Models\Ordine::find($ordineId)?->update([ 'notifica_spedizione_inviata' => true, ]); Log::info("Ordine {$ordineId} spedito con {$corriere}"); } }); // Dynamic subscription and unsubscription $subscription = $event->client->subscribe('com.myapp.temporaneo', function (array $args): void { Log::debug('Evento temporaneo ricevuto', $args); }); // You can unsubscribe later // $event->client->unsubscribeById($subscription); });
Start the worker subscriber:
php artisan wamp:serve
Automatic Laravel Event
Every WAMP EVENT received automatically dispatches a Laravel event,
without having to register an explicit handler:
use Hermod\Laravel\Events\WampEventReceived; // In EventServiceProvider or AppServiceProvider Event::listen(WampEventReceived::class, function (WampEventReceived $event): void { // Filter by topic match ($event->topic) { 'com.myapp.ordini.spediti' => dispatch(new \App\Jobs\ProcessaSpedizione( ordineId: $event->kwargs['ordine_id'], )), 'com.myapp.notifiche' => \App\Models\Notifica::create([ 'contenuto' => $event->args[0] ?? '', 'ricevuta_at' => now(), ]), default => Log::debug("Evento WAMP non gestito: {$event->topic}"), }; });
Combining RPC and PubSub in the same worker
use Hermod\Laravel\Events\WampServeStarted; Event::listen(WampServeStarted::class, function (WampServeStarted $event) { // ── RPC Procedures ──────────────────────────────────── $event->client->register('com.myapp.utente.crea', function (array $args, array $kwargs): array { $user = \App\Models\User::create([ 'name' => $kwargs['nome'], 'email' => $kwargs['email'], ]); // Publish event after user creation $event->client->publish('com.myapp.utenti.creati', [], [ 'user_id' => $user->id, 'email' => $user->email, ]); return $user->toArray(); }); $event->client->register('com.myapp.statistiche', function (): array { return [ 'utenti_totali' => \App\Models\User::count(), 'ordini_attivi' => \App\Models\Ordine::where('stato', 'attivo')->count(), ]; }); // ── PubSub Subscriptions ────────────────────────────── $event->client->subscribe('com.myapp.cache.invalida', function (array $args, array $kwargs): void { $chiave = $kwargs['chiave'] ?? '*'; \Illuminate\Support\Facades\Cache::forget($chiave); Log::info("Cache invalidata per chiave: {$chiave}"); }); $event->client->subscribe('com.myapp.broadcast', function (array $args, array $kwargs): void { // Forward the WAMP event as a Laravel broadcast event \Illuminate\Support\Facades\Broadcast::on('general') ->event(new \App\Events\WampBroadcast($kwargs)) ->sendNow(); }); });
Automatic Reconnect
Reconnect is configured in config/hermod.php and works transparently
during listen(). When the connection drops, Hermod:
- Detects the disconnection in the listening loop
- Waits for the initial delay (default: 1 second)
- Attempts to reconnect
- If it fails, it applies exponential backoff (1s → 2s → 4s → 8s → 16s → max 30s)
- After a successful reconnect, it automatically re-registers all procedures and subscriptions
// Custom configuration for critical environment // config/hermod.php 'reconnect' => [ 'enabled' => true, 'max_attempts' => 10, // maximum 10 attempts 'base_delay' => 0.5, // starts from 0.5 seconds 'max_delay' => 60.0, // 60-second cap 'multiplier' => 2.0, // doubles with every failure ],
Artisan Commands
# Start a WAMP worker (Callee + Subscriber) php artisan wamp:serve php artisan wamp:serve --connection=secure php artisan wamp:serve --realm=myrealm php artisan wamp:serve --serializer=cbor # Make an RPC call from the terminal php artisan wamp:call com.myapp.somma 3 5 php artisan wamp:call com.myapp.saluta --kwargs='{"nome":"Mario"}' php artisan wamp:call com.myapp.somma 10 20 --connection=secure
Serializers
| Driver | Subprotocol | Stato | Note | .env |
|---|---|---|---|---|
json |
wamp.2.json |
✅ Stabile | Default, nessuna dipendenza extra | WAMP_SERIALIZER=json |
cbor |
wamp.2.cbor |
✅ Stabile | Richiede spomky-labs/cbor-php |
WAMP_SERIALIZER=cbor |
msgpack |
wamp.2.msgpack |
✅ Stabile | Richiede rybakit/msgpack |
WAMP_SERIALIZER=msgpack |
Authentication
| Method | Use case | Required configuration |
|---|---|---|
anonymous |
Local development, closed networks | none |
ticket |
API key, static JWT | authid, ticket |
wampcra |
Secure authentication with HMAC | authid, secret |
Roadmap
| Version | Content |
|---|---|
| v0.1 | RPC Core — Caller, Callee, JSON, CBOR ✅ |
| v0.2 | PubSub — Publisher, Subscriber ✅ |
| v0.3 | Auth — WAMP-CRA, Ticket, Reconnect ✅ |
| v1.0 | MessagePack, RawSocket, Complete documentation ✅ |
Testing
# All tests ./vendor/bin/pest # With coverage ./vendor/bin/pest --coverage --min=80 # For specific suite ./vendor/bin/pest --filter=Auth ./vendor/bin/pest --filter=Reconnect ./vendor/bin/pest --filter=PubSub ./vendor/bin/pest --filter=Rpc ./vendor/bin/pest --filter=RawSocket ./vendor/bin/pest --filter=Msgpack
Contribute
Read CONTRIBUTING.md for guidelines.
License
Hermod is open source software released under the [MIT] License (LICENSE.md).