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

Stomp Laravel Package

react/stomp

React/STOMP is a STOMP client for ReactPHP, connecting PHP apps to brokers like ActiveMQ and RabbitMQ with non-blocking, event-driven I/O. It supports subscriptions, acknowledgements, transactions, and heartbeats, so messaging fits smoothly into your ReactPHP event loop.

View on GitHub
Deep Wiki
Context7

Getting Started

react/stomp is a STOMP client built on ReactPHP's event loop. It is not a Laravel queue driver — Laravel ships AMQP, Redis, SQS, database and beanstalkd drivers, but nothing that speaks STOMP. So the package earns its place in exactly two situations:

  1. You already run a STOMP broker (ActiveMQ, Artemis, Apollo, or RabbitMQ with the rabbitmq_stomp plugin) and other services in the org (Java, .NET) publish/consume there.
  2. You are building a long-running ReactPHP daemon — typically a WebSocket fan-out server — that needs to push broker messages to browsers in real time.

If you just need queued jobs inside Laravel, use laravel-queue-rabbitmq or Redis instead. Reach for this package when you need to be a STOMP client, not when you need a queue.

Install and broker prep

composer require react/stomp
  • RabbitMQ: rabbitmq-plugins enable rabbitmq_stomp, then expose port 61613. Default credentials guest/guest only work from localhost.
  • ActiveMQ/Artemis: STOMP is on 61613 by default; Artemis needs the stomp acceptor added to broker.xml.

Minimal working client

$loop    = React\EventLoop\Loop::get();
$factory = new React\Stomp\Factory($loop);

$client = $factory->createClient([
    'host'     => env('STOMP_HOST', '127.0.0.1'),
    'port'     => (int) env('STOMP_PORT', 61613),
    'vhost'    => env('STOMP_VHOST', '/'),
    'login'    => env('STOMP_LOGIN', 'guest'),
    'passcode' => env('STOMP_PASSCODE', 'guest'),
]);

$client->connect()->then(function (React\Stomp\Client $client) {
    $client->subscribe('/queue/orders', function (React\Stomp\Frame $frame) use ($client) {
        echo $frame->body, PHP_EOL;
        $client->ack($frame);
    }, ['ack' => 'client']);
});

$loop->run();

Your first real use case: an Artisan command that consumes a queue and writes to the database, or a topic subscriber that dispatches a Laravel event. Write that before touching WebSockets — it exercises connect, subscribe, ack and error paths with the least moving parts.

Implementation Patterns

Daemon command with graceful shutdown. The loop owns the process, so this cannot extend Illuminate\Queue\Console\WorkCommand. Bind signals so queue:restart-style deploys don't orphan connections:

class ConsumeStomp extends Command
{
    protected $signature = 'stomp:consume {destination=/queue/orders}';

    public function handle()
    {
        $loop = React\EventLoop\Loop::get();
        $client = (new React\Stomp\Factory($loop))->createClient(config('stomp.connection'));

        $client->connect()->then(function ($client) {
            $client->subscribe($this->argument('destination'), function ($frame) use ($client) {
                try {
                    ProcessOrder::dispatchSync(json_decode($frame->body, true, 512, JSON_THROW_ON_ERROR));
                } finally {
                    $client->ack($frame);   // ack even on failure only if you have a DLQ strategy
                }
            }, ['ack' => 'client-individual']);
        });

        $loop->addSignal(SIGTERM, fn () => $client->disconnect());
        $loop->addSignal(SIGINT,  fn () => $client->disconnect());

        $loop->run();
    }
}

Publish from a one-shot script. For a controller or job that fires one message and exits, use React\Async\await() so you don't have to think in callbacks:

use function React\Async\await;

$client = (new React\Stomp\Factory(React\EventLoop\Loop::get()))->createClient(config('stomp.connection'));
await($client->connect());
$client->send('/topic/order.shipped', json_encode($payload), [
    'content-type' => 'application/json',
    'persistent'   => 'true',
]);

WebSocket fan-out (the canonical use case). Keep one STOMP client per process and fan messages out to connections held by Ratchet:

$factory->createClient($opts)->connect()->then(function ($client) use ($server) {
    $client->subscribe('/topic/pricing', function ($frame) use ($server) {
        foreach ($server->getConnections() as $conn) {
            $conn->send($frame->body);
        }
    });
});

Bridge to Laravel's event system. Inside the frame callback you have the full container — event(new OrderShipped($data)), Cache::put(...), Log::channel('stomp')->info(...) all work. Prefer dispatching a job over doing heavy work inline; see the blocking gotcha below.

Config publishing. Don't hardcode options. Create config/stomp.php reading env(), and instantiate a singleton factory in a service provider so tests can swap it.

Reconnect supervisor. Reconnect on connect() rejection and on unexpected socket close. Back off exponentially, and re-subscribe inside the reconnect callback — subscriptions do not survive a new connection:

$connect = function () use (&$connect, $client, $loop, $backoff) {
    $client->connect()->then(function ($c) {
        $c->subscribe('/queue/orders', $handler, ['ack' => 'client']);
    }, function (Throwable $e) use (&$connect, $loop, &$backoff) {
        $loop->addTimer($backoff = min($backoff * 2, 30), $connect);
    });
};

Acknowledgement modes. ack => 'auto' (default) is fire-and-forget — fine for telemetry, dangerous for order processing. Use 'client' or 'client-individual' for at-least-once delivery, and be aware that ack() is the only way the broker forgets the message.

Testing. Extract a StompPublisher interface with a send(string $destination, string $body): void method; unit-test against a fake. Use a docker-compose ActiveMQ or rabbitmq:3-management with the STOMP plugin for integration tests, and assert via a subscriber on the same destination rather than introspecting the broker.

Gotchas and Tips

Everything must yield. A synchronous 300ms Eloquent query inside a frame callback stalls every other connection on that loop. For a fan-out server this is the number one production bug. Either keep handlers to dispatch() calls, or run separate worker and publisher processes.

No built-in reconnection or heartbeat watchdog. The library connects once. Idle connections behind an ELB/NAT die silently and the loop happily keeps running. Add an application-level ping (send('/topic/health', ...) every N seconds) or a $loop->addTimer that tears down and reconnects after N minutes of silence.

Unacked messages accumulate server-side. If you subscribe with ack => 'client' and forget $client->ack($frame) on an exception path, the broker's prefetch window fills and consumption stops — with no error anywhere. Wrap handlers in try/finally deliberately; decide explicitly whether a failed message is acked, nacked (broker-specific ack values), or left unacked to redeliver.

unsubscribe() needs the same id header. STOMP 1.2 requires a subscription id; if you subscribe with ['id' => 'orders-worker'] you must pass the identical header to unsubscribe(). Mismatches fail silently on some brokers.

Destination naming is broker-specific. /queue/foo and /topic/foo map to queues/topics, but RabbitMQ's STOMP adapter also uses /exchange/<name>/<routing.key> for exchange routing. Getting this wrong yields a connection that works and receives nothing.

persistent: true is a header, not a config. A message published without it is dropped on broker restart. Set it explicitly on every send() that matters.

Promise version drift. react/stomp 1.x and 2.x differ in react/promise generation (React\Promise\Deferred vs. React\Promise\PromiseInterface return types) and PHP minimums. Pin the version and be aware that await() requires react/async, which is a separate install.

Keep payloads small. Bodies are strings held in memory in the frame parser. Put large artifacts in S3 and pass a URI in the body. Also set content-type explicitly — brokers and consumers treat untyped bodies inconsistently.

Memory creeps in long-running daemons. The container accumulates resolved instances, model event listeners, and query logs. Periodically DB::disconnect(), avoid Model::observe() registration inside handlers, and consider gc_collect_cycles() on a timer.

Debugging. STOMP frames are plain text, so tshark -i lo -f 'tcp port 61613' -A or a socat -v proxy shows you exactly what's on the wire. On the consumer side, log $frame->headers once — the message-id, subscription and ack headers are what you need to replay. The ActiveMQ web console and RabbitMQ management UI both show queue depth and unacked counts, which is how you diagnose the ack problem above.

Maintenance risk. 114 stars, zero listed dependents, low activity. Treat it as vendored infrastructure: read the ~1k lines once, keep a fork or a compatibility test in CI, and isolate it behind your own StompPublisher/StompConsumer interfaces so a swap to enqueue/stomp or a raw clue/socket-raw implementation is a one-file change.

Extension points. The Factory accepts your own LoopInterface, so you can share one loop across STOMP, Redis and an HTTP server in the same process. Subclassing React\Stomp\Client is possible but brittle — prefer decorating it with reconnect/retry/metrics behaviour rather than extending.

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.
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle