hermod/laravel-wamp

A modern WAMP client for Laravel 12+

Maintainers

Package info

github.com/sparrinellodev/hermod

pkg:composer/hermod/laravel-wamp

Statistics

Installs: 11

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.0.2 2026-04-26 17:43 UTC

This package is auto-updated.

Last update: 2026-04-26 17:46:46 UTC


README

Latest Version PHP Version License

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 NotificaOrdineSpe­dito 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:

  1. Detects the disconnection in the listening loop
  2. Waits for the initial delay (default: 1 second)
  3. Attempts to reconnect
  4. If it fails, it applies exponential backoff (1s → 2s → 4s → 8s → 16s → max 30s)
  5. 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).