centraldesktop/stomp
A PHP client for the STOMP messaging protocol, enabling applications to connect to brokers, subscribe to destinations, send messages, and handle acknowledgements. Useful for integrating PHP apps with STOMP-compatible queues and messaging systems.
The package is a non-Laravel, framework-agnostic STOMP client, so wiring it in is manual (there is no service provider or facade).
Install it:
composer require centraldesktop/stomp
Note: the last tagged release is from February 2015. You may need to composer require centraldesktop/stomp:dev-master or check the autoloader works on your PHP version before investing further.
Look first at the FuseSource\Stomp\Stomp class — it is the entire usable surface area. The companion FuseSource\Stomp\Frame class is what you get back from readFrame().
A minimal round-trip to confirm the broker connection works:
use FuseSource\Stomp\Stomp;
$stomp = new Stomp('tcp://localhost:61613');
$stomp->connect('admin', 'admin');
$stomp->send('/queue/demo', 'hello world', ['persistent' => 'true']);
$stomp->subscribe('/queue/demo');
$frame = $stomp->readFrame();
$stomp->ack($frame);
$stomp->disconnect();
The URI scheme (tcp://) and port (61613, the typical ActiveMQ STOMP port) are the parts that trip people up. If you are using the STOMP plugin on RabbitMQ, the port is 61613 as well, but the destination prefixes (/queue/…, /topic/…) and header semantics differ.
Your first real use case is almost always a producer: an OrderPlaced listener or a queued job that drops a message onto a destination that a non-PHP consumer (Java, Node, Go) is reading from.
Bind a shared connection in a service provider. The Stomp object holds an open socket, so treat it as a singleton and be explicit about reconnect handling:
// AppServiceProvider::register()
$this->app->singleton(Stomp::class, function ($app) {
$stomp = new Stomp(config('stomp.broker'));
$stomp->connect(config('stomp.user'), config('stomp.pass'));
return $stomp;
});
Do not share this singleton across parallel jobs when using pcntl-forking workers — each fork needs its own socket.
Producer pattern — send from an event listener or job. Keep sends small and set headers deliberately:
$stomp->send("/queue/{$queue}", json_encode($payload), [
'persistent' => 'true',
'content-type' => 'application/json',
'correlation-id' => (string) Str::uuid(),
'reply-to' => '/queue/replies',
]);
Consumer pattern — a long-running Artisan command. This is the idiomatic Laravel shape for a STOMP consumer; it gives you signals, logging, and restart hooks:
class ConsumeStomp extends Command
{
protected $signature = 'stomp:consume {queue}';
public function handle(Stomp $stomp): int
{
$stomp->subscribe($this->argument('queue'), ['ack' => 'client']);
$stomp->setReadTimeout(5);
while (true) {
$frame = $stomp->readFrame();
if ($frame === null) {
continue; // timeout, loop back
}
try {
$this->dispatch(new ProcessMessage($frame->body));
$stomp->ack($frame);
} catch (\Throwable $e) {
Log::error('stomp consumer failed', ['err' => $e->getMessage()]);
$stomp->nack($frame); // only valid on brokers that support NACK
}
}
return self::SUCCESS;
}
}
Selective consumption via headers. Instead of multiple queues, use a subscription header filter (ActiveMQ selector syntax):
$stomp->subscribe('/queue/orders', ['selector' => "type = 'refund'"]);
This is the cheapest way to fan out a single queue to multiple Laravel worker processes by responsibility.
Transactions for atomic multi-send. STOMP transactions are scoped to one connection and wrap sends/acks:
$tx = $stomp->begin();
$stomp->send('/queue/a', 'one', ['transaction' => $tx]);
$stomp->send('/queue/b', 'two', ['transaction' => $tx]);
$stomp->commit($tx); // or ->abort($tx)
Bridge to Laravel's queue system. A common pattern is to use Laravel's native queue:work for PHP-side jobs and reserve STOMP for cross-language traffic (Java consumers, ESB integrations). Do not try to make STOMP the backing store for Laravel's queue — writing a full Queue driver against it is more work than the package warrants.
connect() path on PHP 8.2+ before committing. There are no open dependents on Packagist, so you are effectively maintaining a fork in practice.send/readFrame and log frames yourself.readFrame() blocks until the timeout expires. The default read timeout is often effectively forever; always call setReadTimeout() and treat a null return as "no message, try again" rather than as an error.persistent => 'true' is honored by ActiveMQ but silently dropped by some brokers. Do not rely on it for exactly-once semantics./queue/ or /topic/. RabbitMQ's STOMP adapter is stricter and rejects /topic/ in some versions. Get the destination format right before chasing phantom delivery bugs.content-length. Without it, STOMP frames are NUL-terminated and any \x00 in the body will truncate your message. For JSON/text it's fine; for attachments always set the header.ack mode matters. ack => 'client' requires an explicit ack() per frame; 'auto' risks message loss if the process dies mid-handling. Use client for anything with side effects.connect() in a supervised command with exponential backoff, or listen for StompException in your loop and rebuild the connection.composer update will overwrite it.stomp-php (active fork, PHP 8 support, PSR-4) or a broker-native client before adopting this one for a new project.How can I help you explore Laravel packages today?