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

Rabbitmq Bundle Laravel Package

wlogic/rabbitmq-bundle

A lightweight RabbitMQ integration bundle for PHP applications, providing configuration-driven connections, message publishing and consuming helpers, and simple AMQP setup so you can add reliable background jobs and event messaging without boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first, because this is not a Laravel package. wlogic/rabbitmq-bundle is a Symfony 2 bundle: it ships a Bundle class, a DependencyInjection/Configuration.php tree, and compiler passes that wire services via tags (rabbitmq.consumer, rabbitmq.callback, rabbitmq.producer). Laravel will never discover any of that. Treat the repo as a well-structured reference implementation of php-amqplib and borrow the three classes that matter:

  • RabbitMq/Producer.php — thin wrapper over AMQPExchange::publish() with content-type/delivery-mode/priority setters.
  • RabbitMq/Consumer.php — callback + basic_consume loop, ack policy, memory limit, idle timeout.
  • RabbitMq/RpcClient.php / RpcServer.php — correlation-id / reply-to request-reply, which is the genuinely hard part to get right by hand.

Start with DependencyInjection/Configuration.php: it is the best documentation in the repo, listing every connection, exchange, queue, qos_options, and ssl option in one place, including which defaults are silently applied.

Minimal steps to get productive in a Laravel app:

  1. composer require php-amqplib/php-amqplib — you want the transport, not the bundle. If you intend to actually autoload the bundle's classes (they are plain PHP, no Symfony base classes), require the package but never register it.
  2. Port the YAML config shape into config/rabbitmq.php:
return [
    'connections' => [
        'default' => [
            'host' => env('RABBITMQ_HOST', 'localhost'),
            'port' => 5672,
            'user' => env('RABBITMQ_USER', 'guest'),
            'password' => env('RABBITMQ_PASSWORD', 'guest'),
            'vhost' => '/',
            'lazy' => true,
            'connection_timeout' => 3.0,
            'read_write_timeout' => 3.0,
            'heartbeat' => 15,   // the bundle default of 0 is a trap on long-idle workers
        ],
    ],
    'producers' => [
        'mail' => [
            'connection' => 'default',
            'exchange_options' => ['name' => 'mail', 'type' => 'topic'],
            'content_type' => 'application/json',
            'delivery_mode' => 2,
        ],
    ],
];
  1. Bind a singleton connection and a producer in a small service provider (see next section). The bundle's AMQPLazyConnection behaviour — connect on first publish(), not at boot — is worth reproducing by hand; eager connections in a service provider will slow every request and break php artisan commands that never touch the queue.
  2. First use case: fire-and-forget a job that is not a Laravel queue job — e.g. publish {"user_id": 42} to the mail topic exchange from a UserRegistered listener, and consume it from a dedicated long-running command under Supervisor. Keep this separate from Illuminate\Queue; mixing the two on the same connection is where things get painful.

Implementation Patterns

Producer as a container singleton, published through a thin domain service. Do not inject AMQPExchange or AMQPChannel into controllers. The bundle exists precisely so that application code calls $producer->publish($payload, $routingKey) and nothing else.

$this->app->singleton(AMQPStreamConnection::class, function ($app) {
    $c = config('rabbitmq.connections.default');
    return $c['lazy']
        ? new AMQPLazyConnection($c['host'], $c['port'], $c['user'], $c['password'], $c['vhost'], false, 'AMQPLAIN', null, 'en_US', $c['connection_timeout'], $c['read_write_timeout'], null, true, $c['heartbeat'])
        : new AMQPStreamConnection(/* ... */);
});

$this->app->bind(MailProducer::class, function ($app) {
    $conn = $app->make(AMQPStreamConnection::class);
    $ex   = new AMQPExchange($conn->channel());
    $ex->setName('mail');
    $ex->setType(AMQP_EX_TYPE_TOPIC);
    $ex->setFlags(AMQP_DURABLE);
    $ex->declareExchange();
    return new MailProducer($ex);
});

Declare topology once, at deploy time. The bundle gives you rabbitmq:setup-fabric; you need the Laravel equivalent. Write php artisan rabbitmq:declare that iterates the exchanges/queues arrays and declares exchanges, queues, and bindings idempotently, then run it in your deploy script. Never declare topology inside request-path producers.

Consumer command mirroring rabbitmq:consumer. The bundle's command flags (--memory-limit, --max-messages, --idle-timeout, --no-ack) map directly onto an Artisan command signature. Give yourself all four; you will need --max-messages to get graceful restarts under Supervisor and --memory-limit because PHP workers leak.

class ConsumeMail extends Command
{
    protected $signature = 'rabbitmq:consume:mail
        {--memory-limit=128 : MB before self-restart}
        {--max-messages=0 : stop after N messages}
        {--idle-timeout=0 : seconds before exiting when idle}';

    public function handle(): int
    {
        $channel = app(AMQPStreamConnection::class)->channel();
        $channel->set_qos(0, (int) config('rabbitmq.consumers.mail.qos.prefetch_count'), false);

        $queue = new AMQPQueue($channel);
        $queue->setName('mail');
        $queue->setFlags(AMQP_DURABLE);
        $queue->declareQueue();
        $queue->bind('mail', 'mail.*');

        $consumed = 0;
        $queue->consume(function (AMQPEnvelope $msg, AMQPQueue $q) use (&$consumed) {
            try {
                $this->dispatchNow(new SendMailJob(json_decode($msg->getBody(), true)));
                $q->ack($msg->getDeliveryTag());
            } catch (\Throwable $e) {
                report($e);
                $q->nack($msg->getDeliveryTag(), AMQP_NOPARAM); // or requeue once, then dead-letter
            }
            if ($this->shouldStop(++$consumed)) { return false; }
        }, AMQP_NOPARAM, $tag = null);   // empty tag => broker-generated, unique per connection
    }
}

Reuse the callback-registry idea instead of Symfony's tagged services. The bundle collects all services tagged rabbitmq.callback and registers them on the consumer. In Laravel, keep a consumers config array mapping queue name → handler class, resolve handlers from the container, and let one generic consume command serve all of them. This is the single most valuable structural borrow: it keeps one worker command instead of N near-identical ones.

RPC via RpcClient::addRequest() / getReplies() is the pattern to copy verbatim for synchronous request-reply. Two things to preserve: a per-request reply_to queue with an exclusive, auto-delete, server-named declaration, and correlation-ID matching on the reply. Do not roll this yourself with a shared reply queue — you will get cross-talk under concurrency.

Queue-size and lag monitoring: the bundle has no metrics. Because you own the connection, add an Artisan command that calls $channel->queue_declare($name, true, false) in passive mode to read message_count and consumer_count and pushes them to your metrics backend. Passive declare returns the counts without asserting arguments — this is the only safe way to inspect a queue you do not own.

Testing: wrap the producer in an interface and bind a fake in the test container that records messages. The consumer side is testable by extracting the callback body into an invokable handler class that takes a plain array payload — then you unit-test the handler with no broker at all. Reserve a real broker (Testcontainers / docker compose) for one or two integration tests.

Gotchas and Tips

  • Symfony config placeholders leak. The bundle's YAML uses %kernel.debug% and %kernel.environment%. If you copy config verbatim into config/rabbitmq.php, those strings become literal hostnames. Strip every %...% token.
  • prefetch_count defaults to 0, which means "unlimited" — the broker will hand a single worker every message it has. A worker that dies mid-batch then requeues thousands at once. Set prefetch_count to 1–10 per consumer, per queue, and match it to your job latency.
  • 406 PRECONDITION_FAILED is the #1 runtime error. Declaring an existing queue or exchange with different durable, auto_delete, or arguments closes the channel. The bundle's auto_setup_fabric hides this at boot and then explodes later from a different process. Pin the topology in one place and make sure it is byte-identical everywhere. To change args on a live queue, you must delete it (or declare a new name and migrate).
  • Heartbeat default is 0 (disabled). With read_write_timeout: 3 and a 30-second job, php-amqplib's own check can kill the connection. Either set heartbeat to roughly a third of read_write_timeout, or bump channel_rpc_timeout. Under Octane, connections are even longer-lived — never cache a connection across an Octane worker boot without a liveness check ($conn->isConnected()), because a silently dead TCP socket will accept a publish() and drop the message.
  • Unroutable messages vanish silently. basic_publish returns success even when no queue is bound. The bundle exposes the mandatory flag; you must handle basic.return (and basic.nack for confirms) to notice. Always give every exchange at least one binding, and consider publisher confirms with setConfirmCallback for anything you cannot afford to lose.
  • Durability is a four-way agreement: durable exchange + durable queue + delivery_mode: 2 + a confirm on publish. Miss any one and a broker restart loses the message. The bundle's producer defaults do not set delivery_mode: 2 unless you configure it.
  • Ack semantics. With auto_ack: false (the sane default), a SIGKILL on your Supervisor worker returns everything unacked to the queue — good — but if your handler acked before the side effect completed, the message is gone. Ack last, after the DB transaction commits. Prefer nack with requeue=false plus a dead-letter exchange over infinite requeue=true poison-message loops.
  • The bundle's Consumer requires a PSR-3 logger, which in Symfony is injected. In Laravel, app('log') satisfies Psr\Log\LoggerInterface, so you can pass it straight into the bundle's classes if you do autoload them — one of the few places the two frameworks line up cleanly.
  • getQueue() on the consumer returns the declared AMQPQueue; reusing it across consume() calls after a reconnect is unsafe. Build a fresh channel from the lazy connection on every reconnect.
  • Long-running consumers and Laravel's container: dispatchNow reuses the same app instance forever. Anything stateful (auth user, DB::reconnect, config overrides) leaks between messages. Call DB::purge() on reconnect and avoid $request-derived state in handlers.
  • Zero stars, zero dependents, one vendor. There is no community to ask and no guarantee the fork tracks php-amqplib/php-amqplib security fixes. Vendor-lock the exact commit, keep the borrowed classes behind your own interfaces so you can swap in vladimir-yuldashev/laravel-queue-rabbitmq or a bespoke transport without touching handlers, and read the diff between this fork and upstream before trusting it in a payment path. If all you need is a queue driver, use a maintained Laravel-native package; the value here is the Producer/Consumer/RpcClient shape, not the code.
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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource