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

Mongodb Messenger Transport Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

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.

Gotchas and Tips

  • Upgrading 1.8 → 1.9.0 is a constraint change, not a behaviour change — but read the diff anyway. The transport, factory, options and document shape are untouched; the two changes are the Symfony 5.4 floor and the DI extension base class. The realistic failure modes are: (a) Composer refuses the bump because you're on Symfony 5.3-; (b) your CI treats deprecations as errors and suddenly goes green or your deprecation baseline shifts, because the extension no longer emits the 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.
  • Small package, thin community. 7 stars and zero dependents means no Stack Overflow corpus. Pin an exact version in 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.
  • Indexes decide whether this is fast or awful. At minimum you want a compound index covering the claim query (queue name + availability/delivery timestamps). Without it, every 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.
  • Duplicates are a feature, not a bug. Same as any at-least-once queue: crash between handler and ack, redelivery after timeout, retry after a partial failure. Idempotency lives in your handler.
  • No failure transport configured = silent loss. Messenger logs and drops a message that exhausts retries when failure_transport is null. Always configure one; MongoDB itself is a perfectly good DLQ for this.
  • Serializer choice is a deployment hazard. With 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.
  • BSON's 16 MB document limit is your real payload ceiling. Anything near it (large HTML templates, base64 blobs, full API responses) should be stored in GridFS/object storage and passed by reference. Compressing the serialized body is fine for medium payloads but doesn't remove the hard limit.
  • Durability depends on your MongoDB topology. A standalone 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.
  • The DSN is parsed by the bundle, not Messenger. Database name from the URI path, everything else from query/options; options passed under 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 conflicts. The bundle registers ODM mappings for its own document namespace in your document manager. If your app'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.
  • Watch worker lifecycle in containers. 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.
  • Extension points are limited but present. You can decorate the transport service, swap the serializer via the 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.
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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle