facile-it/mongodb-messenger-transport
A Symfony Messenger transport that uses MongoDB as its message queue backend. It lets applications send, store, and consume messages through MongoDB collections, offering an alternative to AMQP, Redis, or Doctrine transports for projects already running MongoDB.
1. Install and enable. The package is a Symfony bundle that plugs a MongoDB-backed transport into Messenger. It pulls in facile-it/mongodb-bundle, doctrine/mongodb-odm and mongodb/mongodb, so there is nothing else to wire by hand beyond the bundle registration.
composer require facile-it/mongodb-messenger-transport
// config/bundles.php
Facile\MongoDbMessenger\FacileMongoDbMessengerBundle::class => ['all' => true],
Version floor (as of 1.9.0): Symfony 5.4+ is required — 1.9.0 dropped support for Symfony < 5.4. If you are still on 5.3 or earlier, pin to 1.8.x and plan the framework upgrade before taking further releases. 1.9.0 introduces no new transport features or config keys; it is a compatibility release (framework floor + internal DI extension class swap), so the upgrade is a constraint change, not a code change. Check the composer.json of the tag for the exact PHP constraint before bumping — the constraint is the real gate, not this note.
2. Point it at a database. The transport reuses the facile-it/mongodb-bundle connection/document-manager configuration, so make sure a document manager exists (usually the facile_mongodb config block) with a URI that includes the database name. The transport creates its own documents inside that database; you do not need a separate client unless you want isolation.
3. Configure the transport. The DSN scheme is mongodb://; database comes from the URI path, collection from options.
# config/packages/messenger.yaml
framework:
messenger:
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
collection: messenger_messages
auto_setup: true # dev/staging only
redeliver_timeout: 3600
# write_concern: { w: 'majority' }
routing:
'App\Message\SendWelcomeEmail': async
# .env
MESSENGER_TRANSPORT_DSN=mongodb://mongo:27017/app?replicaSet=rs0&authSource=admin
4. First use case. The canonical day-one scenario is making a slow side effect (mail, webhook, PDF render) asynchronous:
$bus->dispatch(new SendWelcomeEmail($userId));
then run bin/console messenger:consume async -vv and confirm a document appears in app.messenger_messages and disappears after ack. That single round-trip exercises the whole pipeline: MongoDbTransportFactory → MongoDbTransport::send() → the message ODM document → atomic claim in get() → handler → ack().
Where to look first in vendor/facile-it/mongodb-messenger-transport/src:
Transport/MongoDbTransportFactory.php — which DSN schemes and options are actually supported (don't trust docs, this is a small package).Transport/MongoDbTransport.php — send/get/ack/reject, DelayStamp handling, keepalive, and count support.Document/ + Repository/ — the BSON shape of a queued message; this is what you'll query when debugging.DependencyInjection/ — how the factory is tagged messenger.transport_factory and which services you can decorate. Note that since 1.9.0 the bundle extension extends Symfony\Component\DependencyInjection\Extension\Extension (previously the HttpKernel one), which is the class you'll be reading if you ever override it.Separate collections per queue rather than one shared collection. The default design puts a queueName field on a shared collection, which works, but per-queue collections (messenger_async, messenger_bulk, messenger_failed) give you independent index tuning, independent depth metrics, and the ability to drop/truncate a queue without touching others. It also makes messenger:failed:* commands and dashboards far easier to reason about.
Failure transport as a second MongoDB transport. The transport itself is a queue, not a DLQ implementation — pair it with Messenger's standard retry/failure machinery:
framework:
messenger:
failure_transport: failed
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options: { collection: messenger_async }
retry_strategy:
max_retries: 5
delay: 1000
multiplier: 2
max_delay: 60000
failed:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options: { collection: messenger_failed }
Then messenger:failed:show, messenger:failed:retry <id>, messenger:failed:remove <id> all work unchanged, because they only rely on the TransportInterface/ListableReceiverInterface contract.
Design handlers for at-least-once delivery. The claim is atomic (find-and-modify style on deliveredAt/availableAt), but a worker crash between the handler's side effect and ack() re-delivers the message after redeliver_timeout. Make handlers idempotent: a unique index on a business key, an upsert, or a "processed messages" collection with a TTL. This is the single most important pattern for this transport.
Prioritisation and workers. Run more consumer processes against the collection with the hottest traffic, and use --time-limit=3600 --memory-limit=128M under Supervisor/systemd so long-lived ODM connections get recycled. On deploy, messenger:stop-workers so processes pick up new code rather than continuing to handle messages with old class definitions (critical with the native PHP serializer, see gotchas).
Delayed/retried jobs. Verify DelayStamp support in MongoDbTransport::send() for your pinned version before relying on it. If it is not honoured (or is implemented by simply setting a deliverable-at timestamp without a TTL index), keep delays inside Messenger's retry strategy and out of application code, and for long scheduled jobs dispatch from a cron/scheduler instead of using DelayStamp.
Operational visibility. There is no Prometheus exporter here; query MongoDB directly. Message depth and age-of-oldest are two aggregation pipelines you'll want in a dashboard:
db.messenger_async.aggregate([
{ $group: { _id: null, depth: { $sum: 1 }, oldest: { $min: "$createdAt" } } }
])
Also alert on documents where deliveredAt is set and old — that's stuck in-flight work, i.e. a worker died mid-handler.
Testing strategy. Use in-memory:// (or sync://) for unit and functional tests of handlers — this transport adds nothing to those. Reserve a real MongoDB (Testcontainers / a CI service container) for the few integration tests that assert redelivery, atomic claim under concurrency, and serialization round-trips. Asserting on the raw collection ("after dispatch, exactly one doc with this queueName") is a cheap and very effective smoke test.
Isolation from your app's ODM. If your application already uses Doctrine ODM heavily, give Messenger its own document manager/client in the facile_mongodb config so that a long-running consumer's identity map or a heavy app query can't interfere with the transport's writes, and so you can tune write concern for the queue independently.
HttpKernel\DependencyInjection\Extension deprecation; (c) you (or a compiler pass) type-hinted the old extension class — check DependencyInjection/ in your vendor tree if you subclass it.composer.json, read MongoDbTransport.php before upgrading, and be prepared to fork. Budget for that when choosing it over the Doctrine/AMQP/Redis transports. A 1.9.0 that only touches framework constraints is exactly the kind of release where an unreviewed minor bump surprises you later.auto_setup: true is a dev convenience, not a deploy step. In production run messenger:setup-transports from your release pipeline (or create indexes via a migration) and keep auto-setup off so a worker doesn't silently create a collection with the wrong options in a hot path.get() is a collection scan and worker throughput collapses as the backlog grows. Check with explain() on the query the transport issues.redeliver_timeout must exceed your p99 handler duration. Set it too low and a slow-but-healthy handler causes duplicate concurrent processing of the same message. Set it too high and genuinely stuck messages sit invisible for hours. This is a tuning knob you will revisit.failure_transport is null. Always configure one; MongoDB itself is a perfectly good DLQ for this.messenger.transport.native_php_serializer, queued payloads embed the PHP class name and serialized properties. Rename a property or a class between deploy and drain and old messages blow up on unserialize. Either drain queues before deploys that touch message classes, or switch to the Symfony serializer and version your message contracts.mongod with default write concern can lose acknowledged messages on an unclean shutdown. For anything resembling production, use a replica set with w: 'majority' (and accept the latency) or at least journaled writes. ?replicaSet=...&authSource=admin in the DSN is easily forgotten when moving from Docker Compose to a real cluster.transports.async.options are merged in. If a setting seems ignored, dump the TransportFactory and check which key it actually reads — the option names are not identical to the Doctrine transport's.mapping config is restrictive, you can end up with a "not a valid document" error at dispatch time. Keep the bundle's document prefix mapped and avoid naming your own documents under Facile\MongoDbMessenger.messenger:consume holds a persistent Mongo connection; behind a NAT/load balancer or with a replica set failover, the driver reconnects but a consumer can stall. Supervise with restarts, use --time-limit, and alert on "no messages consumed but queue depth > 0" rather than only on worker exit codes.serializer option, and substitute the document/repository services through the container if you need extra fields (tenant id, trace id) on the stored document. Doing so is unsupported API-wise; re-check after each upgrade — the 1.9.0 extension base-class swap is a live example of an internal class moving under you.How can I help you explore Laravel packages today?