florianajir/rabbitmq-sql-bundle
A Symfony bundle that consumes messages from RabbitMQ queues and persists them into SQL databases, giving you a simple, configurable bridge between your message broker and relational storage.
Reality check first: florianajir/rabbitmq-sql-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/rabbitmq-sql.php, and no php artisan vendor:publish. It targets Symfony 2/3-era apps, Doctrine ORM (doctrine/orm 2.x), and php-amqplib. The last release was May 2016, with 2 stars and zero dependents — treat it as reference code, not a dependency.
Minimal path to evaluate it:
composer require florianajir/rabbitmq-sql-bundle
Then, in a Symfony-style config.yml (the bundle registers its own extension namespace), look for something shaped like:
rabbitmq_sql:
connections:
default:
host: "%rabbitmq_host%"
port: 5672
user: "%rabbitmq_user%"
password: "%rabbitmq_password%"
vhost: "/"
queues:
orders:
connection: default
# entity/mapping config the worker uses to persist payloads
Where to look first, in this order:
composer.json — check the symfony/* and doctrine/orm version constraints. On Laravel 10/11/12 you're on Symfony 6/7 components; the bundle's Bundle class and container extension will likely fight you on ContainerBuilder API differences, return types, and TreeBuilder signatures.Command/*Worker*Command.php) — this is the actual entry point. Everything you care about (consumer loop, ack logic, hydration) is behind it.DependencyInjection/Configuration.php — the authoritative list of config keys. Don't trust the README (it's from 2016).First use case that justifies it: you have a RabbitMQ topic producing structured JSON (e.g. order.created) and you want a long-running worker that inserts parent + child rows (order + order lines) in one transaction, resolving foreign keys automatically. That "persist a payload with relationships" is the only genuinely differentiating feature here — plain consumers give you the message, not the graph persistence.
Practical recommendation: don't composer require it into a Laravel app. Either (a) stand it up as a separate Symfony worker microservice that shares your MySQL/Postgres database with the Laravel app, or (b) lift the ~200–400 lines of mapping logic into a Laravel console command. Option (a) is cleaner; option (b) is more realistic given the age.
This is the lowest-friction way to actually use the bundle. Your Laravel app publishes; the Symfony worker consumes and writes; both point at the same DB.
// Laravel side — publisher stays 100% Laravel, no bundle involvement
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
$channel = (new AMQPStreamConnection(
config('services.rabbitmq.host'), 5672,
config('services.rabbitmq.user'), config('services.rabbitmq.password')
))->channel();
$channel->queue_declare('orders', false, true, false, false);
$channel->basic_publish(
new AMQPMessage(json_encode($payload), [
'content_type' => 'application/json',
'delivery_mode' => 2, // persistent
'message_id' => (string) Str::uuid(),
'application_headers'=> ['schema_version' => 1],
]),
'',
'orders'
);
The contract between the two sides is the JSON shape plus schema_version. Keep it versioned from day one; you cannot change the worker's mapping quickly later.
The relationship support works when your payload mirrors the entity graph. Design your messages to be insert-shaped, not event-shaped:
{
"id": 9001,
"customer_id": 42,
"placed_at": "2026-01-14T10:22:31Z",
"lines": [
{ "sku": "ABC-1", "qty": 2, "unit_price": "19.99" },
{ "sku": "XYZ-9", "qty": 1, "unit_price": "5.00" }
]
}
If your producer emits deltas ({"event":"line_added","order_id":9001,...}) you'll be writing a projector on top of this bundle anyway — at which point Laravel's own queue + Eloquent upserts is the better tool. Use this bundle for full-graph snapshots, not event sourcing.
The single most important integration tip. The consumer loop should be:
persist() + flush().$msg->ack().If the process dies between 3 and 4, the broker redelivers and you insert twice. That's fine — provided you have idempotency (below). If you ack before commit, you lose data silently, which is worse.
Since 2016-era Doctrine code won't have upsert logic, enforce dedupe in the database:
ALTER TABLE orders ADD UNIQUE KEY uniq_message (source_message_id);
ALTER TABLE order_lines ADD UNIQUE KEY uniq_line (order_id, line_no);
Carry message_id from the AMQP properties into a column. Then wrap the persist call in a try/catch for Doctrine\DBAL\Exception\UniqueConstraintViolationException and treat it as success-and-ack. Retrofitting this into the bundle's persister service is usually a 20-line change — a legitimate reason to fork.
If you must stay single-runtime, use the bundle's mapper as a library:
class PersistOrderGraph implements ShouldQueue
{
use InteractsWithQueue, Queueable;
public function handle(OrderGraphMapper $mapper): void
{
DB::transaction(function () use ($mapper) {
$mapper->persist($this->payload); // ported from the bundle
});
}
}
...and swap the queue driver to rabbitmq via vladimir-yuldashev/laravel-queue-rabbitmq. You lose the bundle's consumer loop but keep its hydration logic, which is the part with actual IP in it.
Long-running PHP + Doctrine needs a restart policy that isn't always:
[program:rabbitmq-sql-worker]
command=php bin/console rabbitmq:worker --queue=orders --memory-limit=256M
numprocs=1
autostart=true
autorestart=true
stopwaitsecs=30
stdout_logfile=/var/log/worker.out.log
stderr_logfile=/var/log/worker.err.log
numprocs=1 if the bundle does any ordering-sensitive work; otherwise scale horizontally and rely on the idempotency keys.
It will not install cleanly on a modern stack. The 2016 release pins Symfony 2/3 and Doctrine 2.5-era APIs. Expect Bundle::getContainerExtension() return-type errors, deprecated TreeBuilder::root() calls, and Doctrine\ORM\EntityManager::flush($entity) signatures that were removed in ORM 3. Budget half a day for the fork, or don't start.
Doctrine ≠ Eloquent. There is no path to persisting into Laravel models. If your domain lives in Eloquent models with HasMany/BelongsTo, this bundle's relationship support is irrelevant to you — it resolves Doctrine associations, not Eloquent relations. This is the most common reason people bounce off it.
Memory leaks in the consumer loop. Doctrine's identity map grows unboundedly in a long-running worker. If the bundled command doesn't call $em->clear() per message (2016 code often doesn't), you'll OOM at ~10k messages. Fork and add:
$this->em->clear();
gc_collect_cycles();
and wire --memory-limit into the loop condition.
FK ordering is implicit and fragile. Relationship persistence depends on the payload arriving in an order the hydrator can resolve. Self-referencing trees (category → parent category) or circular refs will either throw or silently null the FK. Flatten such payloads before publishing.
No dead-letter handling out of the box. Older consumers call $msg->nack() with no requeue and no DLX, which means poison messages vanish. Declare a DLX on the queue (x-dead-letter-exchange) at declare time — you can't add it later without recreating the queue, which means draining it first.
Config cache obscures errors. On the Symfony side, bin/console cache:clear and check var/cache/dev/appDevDebugProjectContainer.xml for what the bundle actually registered. Half the "it does nothing" reports are a typo'd config key silently ignored because TreeBuilder wasn't strict.
Debugging: turn on doctrine.dbal.logging and doctrine.orm.sql_logger in dev — the fastest way to see whether the issue is "message never arrived" or "hydrator produced an entity with no fields set." Log the raw $msg->body before hydration; you'd be surprised how often the publisher and consumer disagree on JSON encoding.
Extension points worth knowing: the persister/hydrator service and the console command are the two seams. Forking and replacing the command (to add clear(), metrics, and structured logging) while keeping the hydrator is the sane split. Don't try to subclass the bundle's services — they're almost certainly instantiated directly, not via interfaces.
Security/maintenance: no releases in ~9 years means no CVE patches for transitive deps (php-amqplib in particular has had advisories). Pin and audit, or vendor the code into app/Support/ and drop the composer dependency entirely. For a 400-line worker, vendoring is honestly the right call.
When to walk away: if your need is "consume JSON and insert rows," Laravel 11's Queue::connection('rabbitmq') plus a DB::transaction and upsert() is ~40 lines, fully supported, and you'll understand every line. This bundle only earns its keep if you have an existing Symfony/Doctrine app and want the graph-persistence behavior for free.
How can I help you explore Laravel packages today?