kch/rabbitmq-bundle
kch/rabbitmq-bundle integrates RabbitMQ messaging into PHP applications, with configuration for broker connections and straightforward publishing and consuming of messages. A practical choice for adding asynchronous, queue-based processing to Symfony projects.
This is a Symfony bundle, not a Laravel package. It wraps php-amqplib/php-amqplib and provides Symfony DI configuration for connections, producers, consumers, batch consumers, and RPC. There is no Laravel service provider, no extra.laravel auto-discovery, and no Laravel queue driver. In a Laravel app you either wire its classes manually or, more realistically, use it as a reference implementation and use php-amqplib directly.
Decide first whether you need the bundle at all.
vladimir-yuldashev/laravel-queue-rabbitmq.Minimal steps if you install it:
composer require kch/rabbitmq-bundle
The last tagged release is from 2018 and the package has 1 star and 0 dependents. Expect PHP 7-era and Symfony 2/3/4-era constraints. Check conflicts before committing:
composer why-not kch/rabbitmq-bundle
composer show kch/rabbitmq-bundle --all
Where to look first:
DependencyInjection/Configuration.php — the real API documentation. Every producer/consumer/connection option is defined here.RabbitMq/BaseAmqp.php — shared connection/channel/exchange/queue declaration logic.RabbitMq/Producer.php — publish() behavior, routing keys, exchange options.RabbitMq/Consumer.php and RabbitMq/BatchConsumer.php — the consume loop, QoS, callback contract.Command/ConsumerCommand.php and Command/RpcServerCommand.php — the long-running worker patterns, including options like message limits and memory limits.DependencyInjection/KchRabbitMqExtension.php — confirm the config alias. Old oldsound/rabbitmq-bundle used old_sound_rabbit_mq; this fork may retain or rename it.First use case to implement: publish an "upload needs processing" message from a Laravel controller or job, then consume it in a long-running Artisan command. Mirror the bundle's YAML shape in config/rabbitmq.php so your team has one obvious place for topology.
// config/rabbitmq.php
return [
'connections' => [
'default' => [
'host' => env('RABBITMQ_HOST', '127.0.0.1'),
'port' => (int) env('RABBITMQ_PORT', 5672),
'user' => env('RABBITMQ_USER', 'guest'),
'password' => env('RABBITMQ_PASSWORD', 'guest'),
'vhost' => env('RABBITMQ_VHOST', '/'),
'heartbeat' => 30,
'read_write_timeout' => 60,
],
],
'producers' => [
'uploads' => [
'exchange' => ['name' => 'uploads', 'type' => 'direct', 'durable' => true],
'queue' => ['name' => 'uploads', 'durable' => true],
'routing_keys' => ['uploads'],
],
],
];
Publisher as a singleton, with reconnect awareness. The bundle's Producer expects a channel. In Laravel, build it in a service provider or a small wrapper class.
use OldSound\RabbitMqBundle\RabbitMq\Producer;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
final class RabbitMqPublisher
{
private ?Producer $producer = null;
public function publish(string $body, string $routingKey = 'uploads'): void
{
$producer = $this->producer();
$producer->publish($body, $routingKey, [
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
'content_type' => 'application/json',
]);
}
private function producer(): Producer
{
if ($this->producer !== null) {
return $this->producer;
}
$cfg = config('rabbitmq.connections.default');
$connection = new AMQPStreamConnection(
$cfg['host'], $cfg['port'], $cfg['user'], $cfg['password'], $cfg['vhost'],
false, 'AMQPLAIN', null, 'en_US', 3.0, $cfg['read_write_timeout'], null, false, $cfg['heartbeat']
);
$channel = $connection->channel();
$producer = new Producer();
$producer->setExchangeOptions(['name' => 'uploads', 'type' => 'direct', 'durable' => true]);
$producer->setQueueOptions(['name' => 'uploads', 'durable' => true]);
$producer->setRoutingKeys(['uploads']);
$producer->setChannel($channel);
return $this->producer = $producer;
}
}
Register it as a singleton, but reset it in Octane workers or after a connection failure. A stale channel is the most common production failure.
Consumer as an Artisan command. Use the bundle's Consumer class but drive it from php artisan, not a Symfony command.
use OldSound\RabbitMqBundle\RabbitMq\Consumer;
use OldSound\RabbitMqBundle\RabbitMq\ConsumerInterface;
use PhpAmqpLib\Message\AMQPMessage;
class ConsumeUploads extends Command
{
protected $signature = 'rabbit:consume-uploads {--max-messages=0} {--memory-limit=128}';
public function handle(): int
{
$consumer = new Consumer();
$consumer->setExchangeOptions(['name' => 'uploads', 'type' => 'direct', 'durable' => true]);
$consumer->setQueueOptions(['name' => 'uploads', 'durable' => true]);
$consumer->setQosOptions(0, 10, false); // prefetch 10
$consumer->setCallback(new class implements ConsumerInterface {
public function execute(AMQPMessage $msg)
{
$payload = json_decode($msg->body, true, 512, JSON_THROW_ON_ERROR);
ProcessUpload::dispatchSync($payload);
return ConsumerInterface::MSG_ACK;
}
});
$consumer->consume((int) $this->option('max-messages'));
return self::SUCCESS;
}
}
The bundle's consumer typically acknowledges after the callback returns successfully. If the callback throws, the message remains unacknowledged and will be redelivered after the connection drops. That is usually the behavior you want, but it means your handler must be idempotent.
Batch consumers for database-heavy work. Use BatchConsumer when the per-message overhead of an Eloquent insert is too high. Set a batch size and idle timeout, collect messages into an array, then flush with a bulk insert or transaction. Acknowledge the batch only after the flush succeeds. This is the single biggest throughput win in this style of RabbitMQ integration.
RPC pattern. The bundle provides RpcClient and RpcServer. Use it for synchronous request/reply between services, not for internal Laravel calls where HTTP or a database query is simpler. The client manages correlation IDs and reply queues. Set explicit timeouts; a hung RPC server will otherwise block your web request until the connection times out.
Topology as code. Translate the bundle's YAML arrays into a RabbitMqTopology service that declares exchanges, queues, bindings, and dead-letter arguments idempotently. Run it on deploy in non-production or let operations own it. Do not rely on auto_setup_fabric in production unless you deliberately want the application to mutate broker topology at runtime.
Laravel queue bridge, if you insist. A custom Laravel queue connector can map Queue::push() to the bundle's Producer. You will need to serialize the job class and payload yourself, because the bundle knows nothing about Laravel job serialization. Consumers then deserialize and call dispatchSync() or dispatch(). This is more work than using a maintained Laravel driver and should only be done if you need the bundle's RPC or batch abstractions.
Operational pattern. Run consumers under Supervisor with one process per queue and a prefetch that matches your concurrency. Use --max-messages to recycle workers periodically, which mitigates memory leaks in long-running PHP processes. Keep the consumer callback small; push business logic into services that are independently testable.
It is not a Laravel package. The bundle class extends Symfony's Bundle and expects a Symfony kernel, DI container, and config loader. There is no php artisan rabbitmq:* out of the box; every command and service binding is manual. The config alias may be kch_rabbit_mq or the historical old_sound_rabbit_mq; verify in the extension class before copying YAML examples from old blog posts.
Abandonment risk is real. Last release 2018, 1 star, 0 dependents. It likely does not support PHP 8.1+, Symfony 6/7, or modern php-amqplib. Do not add it to a new Laravel 11 app without checking composer why-not. If you only need the producer/consumer classes, consider copying the handful of MIT-licensed files into your app and owning them, rather than installing the Symfony bundle.
Connection and channel lifetime. AMQP connections are not request-scoped. In PHP-FPM, a singleton publisher that lives across requests can accumulate stale channels. In Octane, explicitly close and rebuild the connection on worker recycle. In queue workers, rebuild after any AMQPConnectionClosedException or heartbeat miss.
Heartbeats and timeouts. Set heartbeat > 0 and read_write_timeout at least twice the heartbeat. If the broker closes idle connections and your consumer does not reconnect, the worker appears alive but processes nothing. Wrap consume() in a reconnect loop with backoff.
Prefetch/QoS. Without qos_options.prefetch_count, one consumer can pull a large number of messages and starve other workers. Start with 1–10 for slow handlers and tune upward for batch consumers.
Durability and publisher confirms. Durable exchange/queue plus delivery_mode: 2 is necessary but not sufficient. The bundle's publish() does not confirm delivery by default. For critical messages, enable confirm_select() on the channel and wait for pending acks, or accept that messages can be lost on broker failure or network partition. This is a common surprise.
Serialization contract. The producer and consumer must agree on encoding. If you publish JSON, encode explicitly and set content_type. If you configure a serializer service, both sides must use the same one. The bundle will not guess.
Long-running consumer hygiene. Reconnect the Laravel database connection periodically (DB::purge(), DB::reconnect()), call gc_collect_cycles(), and track memory_get_usage() against your limit. Eloquent and the event dispatcher leak memory in daemons if you never recycle.
Graceful shutdown. Use pcntl_async_signals(true) and a signal handler that sets a stop flag. Check the flag between messages or via the consumer's idle timeout. Supervisor should send SIGTERM, wait, then SIGKILL after stopwaitsecs. Without this, deploys leave unacked messages and duplicate work.
Idempotency is mandatory. RabbitMQ is at-least-once. A consumer can process the same message twice after a crash or requeue. Use a unique message ID and a deduplication table, or make the handler naturally idempotent.
Dead lettering. Configure x-dead-letter-exchange in the queue arguments via the bundle's queue_options.arguments. Without it, poison messages loop forever or disappear. Pair this with a retry queue and a max-retry header.
Debugging. Use the RabbitMQ management UI and rabbitmqctl list_queues name messages consumers, rabbitmqctl list_consumers. Enable the bundle's logger option if it exists in your version, and log the message body plus routing key at the consumer boundary. For topology problems, inspect what was actually declared, not what your config says.
Extension points. The bundle's per-producer/consumer class option lets you subclass Producer or Consumer for custom behavior. The callback option accepts a service ID under Symfony DI; in Laravel, pass a callable or an object implementing ConsumerInterface. If you need retries with backoff, implement them in the callback or via dead-letter queues, not by sleeping inside the consumer.
Alternatives worth considering. vladimir-yuldashev/laravel-queue-rabbitmq for Laravel-native queue integration, php-amqplib/php-amqplib directly for full control, or enqueue/amqp-lib for a higher-level abstraction. The value in this package is its clear producer/consumer/RPC separation, which you can replicate in a few hundred lines of Laravel-owned code with a maintainable dependency footprint.
How can I help you explore Laravel packages today?