kunalvarma05/laravel-rabbitmq
Laravel RabbitMQ integration package for publishing and consuming messages with configurable connections, queues, and exchanges. Provides a simple Laravel-friendly API and artisan commands for working with RabbitMQ in your applications.
Install and wire up
composer require kunalvarma05/laravel-rabbitmq
php artisan vendor:publish --provider="KunalVarma05\RabbitMQ\RabbitMQServiceProvider"
On Laravel 5.5+ the provider/facade are auto-discovered. On older versions register KunalVarma05\RabbitMQ\RabbitMQServiceProvider in config/app.php and alias the facade ('RabbitMQ' => KunalVarma05\RabbitMQ\Facades\RabbitMQ::class).
The published config/rabbitmq.php is the first file to read. It holds host, port, username, password, and vhost, almost always fed from .env:
RABBITMQ_HOST=127.0.0.1
RABBITMQ_PORT=5672
RABBITMQ_USERNAME=guest
RABBITMQ_PASSWORD=guest
RABBITMQ_VHOST=/
Where to look first
config/rabbitmq.php — connection settings and any default queue/exchange names.src/RabbitMQ.php and src/Facades/RabbitMQ.php — the public surface. The facade proxies a single manager class; read its method signatures before writing anything, because the package is thin and you will spend most of your time one layer down in php-amqplib.src/Connection.php / src/Channel.php (or equivalent) — the wrapper objects that expose queue(), exchange(), publish(), consume(), close(). This is where the actual AMQP calls happen.First use case: publish to a queue and drain it
use KunalVarma05\RabbitMQ\Facades\RabbitMQ;
// Producer: declare (idempotent) then publish
$channel = RabbitMQ::connection()->channel();
$channel->queue('orders.created', /* durable */ true);
$channel->publish(json_encode(['id' => 42]), 'orders.created');
Then confirm it landed before writing the consumer:
rabbitmqctl list_queues name messages_ready messages_unacknowledged
# or open the management UI at http://localhost:15672
A one-shot consumer that proves the read path:
$channel->consume('orders.created', function ($message) {
logger($message->body);
$message->ack(); // if the package doesn't auto-ack
});
Once publish → queue → consume works end to end, wrap the consumer in an Artisan command (see next section) rather than leaving it in a route or tinker session.
The facade is a global, stateless-looking entry point, but it holds a live TCP connection. Don't call it from controllers. Bind one thin service in the container and inject it:
namespace App\Messaging;
use KunalVarma05\RabbitMQ\Facades\RabbitMQ;
class Publisher
{
public function publish(string $routingKey, array $payload, string $exchange = ''): void
{
$channel = RabbitMQ::connection()->channel();
$channel->publish(
json_encode($payload + ['published_at' => now()->toIso8601String()]),
$routingKey,
$exchange
);
}
}
This gives you one place to add correlation IDs, retries, and headers, and one place to fake in tests.
The package blocks inside consume(), so the consumer belongs in a long-lived command, not a job:
class ConsumeOrders extends Command
{
protected $signature = 'rabbit:consume-orders';
protected $description = 'Drain the orders.created queue';
public function handle(): int
{
$channel = RabbitMQ::connection()->channel();
$channel->queue('orders.created', true);
$channel->qos(0, 10); // prefetch — do this *before* consume
$channel->consume('orders.created', function ($message) {
try {
ProcessOrder::dispatchSync(json_decode($message->body, true));
$message->ack();
} catch (\Throwable $e) {
report($e);
$message->nack(false); // requeue=false -> route to DLX or drop
}
});
return self::SUCCESS;
}
}
Run it with:
[program:rabbit-orders]
command=php /var/www/artisan rabbit:consume-orders
numprocs=2
autostart=true
autorestart=true
stopwaitsecs=30
Use a named exchange with a routing key when more than one consumer needs the same event; publish directly to a queue only for pure work-queue semantics. Declare exchanges and bindings explicitly at boot (a deploy script or a rabbit:declare command) so that a consumer can never race a missing binding and silently drop messages.
The package moves strings; it does not impose a schema. Always ship JSON with a stable shape:
['event' => 'order.created', 'version' => 1, 'data' => [...], 'id' => (string) Str::uuid()]
The id doubles as your idempotency key — RabbitMQ is at-least-once, and a redelivered message after a nack(requeue: true) is normal, not exceptional.
For domain events you want other services to hear, register a listener that forwards to your Publisher. Keep the Laravel event synchronous and the AMQP publish inside it only if you can tolerate the latency; otherwise wrap it in dispatchAfterResponse() or a queued listener so a broker outage doesn't fail the HTTP request.
Because everything funnels through the facade, RabbitMQ::shouldReceive('connection') is enough to unit-test producers — but it's brittle. Prefer swapping your own Publisher interface in the container with a fake that records messages:
$this->mock(Publisher::class, fn ($m) => $m->shouldReceive('publish')->once());
Never point the test suite at a real broker; the package has no in-memory driver.
This is not a Laravel queue driver. It does not plug into queue:work, Horizon, or ShouldQueue. If you want RabbitMQ as the backend for Laravel's queue system, you need vladimir-yuldashev/laravel-queue-rabbitmq, not this. Confusing the two is the most common mistake made with this package.
Long-running consumers leak. php-amqplib keeps the socket, channel, and message frames in memory. In a consume() loop:
DB::reconnect() periodically, or your MySQL connection will time out and throw mid-message;--max-messages in your own command) and let Supervisor restart it;No built-in reconnect. If the broker restarts, consume() throws AMQPConnectionClosedException and the process exits. Either wrap the call in a retry loop with backoff, or rely on Supervisor's autorestart=true. Heartbeats (default 60s) only help if your handler isn't blocking long enough to miss them — long handlers need pcntl_alarm or a shorter prefetch.
basic_qos must be called before consume. Setting prefetch after the consume call has no effect. If you see a single worker eating the entire queue while others idle, this is why.
Ack discipline. If the package does not auto-ack in your usage, a forgotten $message->ack() leaves messages in unacknowledged forever, and they only come back when the channel closes. Watch messages_unacknowledged in rabbitmqctl list_queues — non-zero and static means a hung consumer.
Redeclaration mismatches are fatal. Declaring a queue with durable=false in dev and durable=true in prod against the same broker gives PRECONDITION_FAILED - inequivalent arg. Pick your flags once, put them in config, and use the same values everywhere. Delete and recreate the queue in dev rather than tweaking the declaration.
config:cache freezes env. After php artisan config:cache, env('RABBITMQ_HOST') inside a consumer returns null because env() only reads the real environment. The package config should call env() itself (it does), but never call env() from your own consumer code — use config('rabbitmq.host').
vhost / is a slash. It's fine in the config array, but if you ever hand-build a connection URI (e.g. for the management API or rabbitmqadmin), / must be percent-encoded as %2F.
No publisher confirms by default. publish() returning without an exception does not mean the broker persisted the message. If you need durability guarantees, enable confirms on the channel and wait_for_pending_acks(), or accept fire-and-forget semantics and design consumers to be idempotent instead.
Message size. The default frame_max is 131072 bytes. Large payloads get split across frames and can trip consumers; keep messages under a few hundred KB and put blobs in S3 with a pointer in the body.
Debugging. The management plugin is worth the two minutes it takes to enable — you can inspect queues, purge them, publish a test message into your consumer, and read the exact PRECONDITION_FAILED text instead of guessing. For CLI, rabbitmqctl list_bindings and rabbitmqctl list_consumers answer "why is nothing arriving" faster than any log.
Version pinning and upkeep. The package is small, low-traffic, and lightly maintained. Read composer.json before adopting: check the php-amqplib constraint against your PHP version (PHP 8.1+ support in older tags is spotty), pin an exact version, and be prepared to fork. If you only need publish/consume, dropping to php-amqplib directly is a couple hundred lines and removes the maintenance risk entirely.
Extension points. The service provider binds a singleton connection from config/rabbitmq.php; you can rebind it in a AppServiceProvider::register() to point at multiple brokers (one container key per connection) if you need more than one vhost or cluster. There is no formal extension API beyond that — the wrapper classes are concrete, so subclassing is your only hook.
How can I help you explore Laravel packages today?