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 Bundle Laravel Package

facile-it/mongodb-bundle

Symfony bundle that integrates Doctrine MongoDB ODM into your app, wiring document managers, repositories, and mappings through simple configuration so you can persist and query MongoDB documents using Symfony's service container.

View on GitHub
Deep Wiki
Context7

Getting Started

facile-it/mongodb-bundle is a thin Symfony DI wrapper around the official mongodb/mongodb library. It is not Doctrine ODM: you get a configured MongoDB\Client, MongoDB\Database and (optionally) named MongoDB\Collection services, and you write queries by hand. That is the whole value proposition — no UnitOfWork, no hydrator, no schema.

Install

pecl install mongodb            # or your distro package; ext-mongodb is mandatory
composer require facile-it/mongodb-bundle mongodb/mongodb

Check config/bundles.php — if Flex didn't auto-register it, add Facile\MongoDbBundle\FacileMongoDbBundle::class => ['all' => true].

Watch the version alignment matrix before you pin anything:

  • Bundle 2.0 (current) requires Symfony 5.4+ and explicitly supports Symfony 8.0. If you are still on Symfony 4.x you must stay on the 1.x line — 2.0 is a hard cut, not a deprecation.
  • mongodb/mongodb 2.x requires ext-mongodb 2.x and PHP 8.1+; 1.x is the older pairing.
  • MongoDB server 5.0+ is what the project actually tests against as of 2.0 (CI for <5.0 was dropped). Older servers will usually still work, but you're off the supported path — relevant on legacy managed instances.

Configure

# config/packages/mongodb.yaml
mongodb:
  connections:
    default:
      uri: '%env(resolve:MONGODB_URI)%'       # e.g. mongodb://mongo:27017/?replicaSet=rs0
      database: '%env(resolve:MONGODB_DATABASE)%'
      options: {}                              # connectTimeoutMS, serverSelectionTimeoutMS, ...
MONGODB_URI=mongodb://mongo:27017
MONGODB_DATABASE=app

The exact key names have shifted between releases — run bin/console config:dump-reference mongodb against your pinned version before copying any snippet from a blog post.

Where to look first

  1. bin/console debug:container mongodb and bin/console debug:autowiring mongodb — confirms whether MongoDB\Client / MongoDB\Database are actually aliased in your version and how the connections are named.
  2. The bundle README's config reference (it's short; the bundle is ~a few hundred lines of DI glue).
  3. The mongodb/mongodb library docs — 90% of your day-to-day API knowledge lives there, not in the bundle.

First use case — inject the database, write a document:

final class AuditLogger
{
    public function __construct(private MongoDB\Database $db) {}

    public function log(string $event, array $context): void
    {
        $this->db->selectCollection('audit')->insertOne([
            'event'      => $event,
            'context'    => $context,
            'occurredAt' => new MongoDB\BSON\UTCDateTime(),
        ]);
    }
}

If nothing is autowirable, fall back to explicit arguments in services.yaml:

services:
  MongoDB\Database:
    alias: mongodb.connection.default.database   # verify the id with debug:container

Implementation Patterns

Always put a repository/DAO layer between Symfony and the driver. This is the single highest-leverage decision. It keeps MongoDB\BSON\* types and ArrayObject-ish documents out of controllers, and it makes the code testable, since MongoDB\Collection is awkward to fake at the call site.

final class UserRepository
{
    public function __construct(private MongoDB\Collection $users) {}

    public function findById(string $id): ?User
    {
        $doc = $this->users->findOne(
            ['_id' => new MongoDB\BSON\ObjectId($id)],
            ['typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array']],
        );
        return $doc ? User::fromDocument($doc) : null;
    }

    public function rename(string $id, string $name): void
    {
        $this->users->updateOne(
            ['_id' => new MongoDB\BSON\ObjectId($id)],
            ['$set' => ['name' => $name, 'updatedAt' => new MongoDB\BSON\UTCDateTime()]],
        );
    }
}

Wire named collections via the bundle's collection config, then alias per type-hint. If the bundle's collection factory isn't available in your version, register your own:

services:
  app.mongo.users:
    factory: ['@MongoDB\Database', 'selectCollection']
    arguments: ['users']

  MongoDB\Collection $usersCollection: '@app.mongo.users'

  App\Repository\UserRepository:
    arguments: ['$usersCollection']

Force root: array in a shared type map. The default root is MongoDB\Model\BSONDocument (an ArrayObject), which behaves differently from a plain array in isset, array_map, and serializer code. Passing typeMap explicitly per call, or setting it globally on the client options, removes an entire class of "why is this {} in my JSON" bugs. Map to domain DTOs yourself in fromDocument() — this is your "hydrator."

Use findOneAndUpdate with ReturnDocument::AFTER for read-modify-write rather than a findOne + updateOne pair:

$updated = $collection->findOneAndUpdate(
    ['_id' => $id, 'status' => 'pending'],
    ['$set' => ['status' => 'done'], '$inc' => ['attempts' => 1]],
    ['returnDocument' => MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER],
);
if ($updated === null) { /* no match — not an error, but handle it */ }

Aggregation lives in the repository, not in Twig/controllers. aggregate() returns a lazy MongoDB\Driver\Cursor; call ->toArray() or foreach and map to DTOs. Keep $lookup rare — if you need three of them, the data model is probably wrong for MongoDB.

Transactions require a replica set. $client->startSession() + $session->withTransaction(...) is the pattern, but against a standalone mongo Docker container it throws. If you need them, put replicaSet in the URI and initialize with rs.initiate() in your dev/test fixture.

Indexes are your "migrations." There is no schema and no migration tool. Add a console command (or a post-deploy script) that runs idempotent createIndex() / createIndexes() calls for every collection. Do this early — a missing index is the most common production incident with this stack.

Log slow queries with a command subscriber, not by wrapping the client:

final class SlowQueryLogger implements MongoDB\Driver\Monitoring\CommandSubscriber
{
    public function __construct(private LoggerInterface $logger) {}
    public function commandStarted(MongoDB\Driver\Monitoring\CommandStartedEvent $e): void { /* start timer */ }
    public function commandSucceeded(MongoDB\Driver\Monitoring\CommandSucceededEvent $e): void { /* log if > threshold */ }
    public function commandFailed(MongoDB\Driver\Monitoring\CommandFailedEvent $e): void {}
}

Register it via a compiler pass or a kernel.event_subscriber-style boot hook calling $client->getManager()->addSubscriber($logger) in dev/test only.

Testing strategy: unit-test repositories against a mocked MongoDB\Collection (PHPUnit can bypass the Manager-requiring constructor), and keep a small integration suite against a real MongoDB via Docker. If mocking gets painful, define your own UserStore interface, implement it with the collection, and fake the interface in tests — the thin bundle makes this cheap. Pin your CI Mongo image to 5.0+ to match what the bundle itself tests.

Gotchas and Tips

  • This is not ODM. No identity map, no cascade, no lazy proxies, no change tracking, no schema validation. Two findOne calls return two distinct PHP values even for the same _id. Don't port Doctrine habits (flush/persist) — every write is an explicit operation.
  • 2.0.0 is a breaking cut on the Symfony side. Added Symfony 8.0 support, removed everything below 5.4. If you're mid-migration on Symfony 4.x, stay on the 1.x line and don't let a composer update float you across the major. The DI surface and config keys are unchanged in 2.0 as far as the release notes go — the break is the platform floor, not the API.
  • MongoDB server 5.0 is now the tested floor (CI for older servers was dropped in 2.0). Nothing in the driver will refuse a 4.x server, but you're on your own; if you're stuck on an old Atlas/shared tier, verify before upgrading.
  • BSON types leak into JSON. ObjectId, UTCDateTime, Decimal128, and binary are not what symfony/serializer or json_encode expects. Convert at the DTO boundary: (string) $objectId, $utcDateTime->toDateTime()->format(DATE_ATOM). Do this in your fromDocument()/toDocument() mappers, not in the view layer.
  • BSONDocument is not an array. Default hydration gives you an ArrayObject; $doc['field'] works, $doc->field does not, and array_key_exists needs getArrayCopy(). Setting typeMap.root = array sidesteps all of it.
  • typeMap root hydration is limited. You can map to a class, but it's reflection-based, doesn't call constructors, ignores private/typed-uninitialized properties, and doesn't recurse into nested documents unless you configure fieldPaths. For anything non-trivial, hydrate manually.
  • _id is immutable. An $set on _id throws MongoDB\Driver\Exception\BulkWriteException. Same for updating the shard key. Model this in your update methods.
  • findOneAndUpdate returns null on no match (with BEFORE and no upsert). null is a legitimate "nothing matched", not an exception — decide explicitly whether to throw a 404/409.
  • Handle duplicate keys. Catch MongoDB\Driver\Exception\DuplicateKeyException (code 11000) or inspect BulkWriteException::getWriteResult()->getWriteErrors() and map to HTTP 409. Without a unique index you'll never see this, so create the indexes.
  • aggregate() is lazy. A Cursor that's never iterated never hits the server. Forgetting toArray() is a classic "my aggregation returns nothing" bug.
  • % in credentials breaks YAML/env resolution. Use %env(resolve:...)% and URL-encode the password, or the DSN gets mangled.
  • Transactions and mongodb+srv need infrastructure: replica set, correct authSource, working DNS SRV records. Test the exact connection string in CI, not just locally.
  • Long-running workers (Messenger, consumers): set serverSelectionTimeoutMS, connectTimeoutMS, and socketTimeoutMS deliberately, and be prepared to catch ConnectionTimeoutException on the first command after idle. Don't hold a MongoDB\Collection inside a serializable message payload.
  • Always resolve the client from the container — never new MongoDB\Client() in a service. Otherwise your URI options, command subscribers, and profiler instrumentation silently don't apply.
  • Debugging: bin/console debug:container mongodb, debug:autowiring MongoDB, and a ping in a health controller ($client->selectDatabase('admin')->command(['ping' => 1])) covers 95% of "the connection is broken" reports.
  • Maturity signal: 38 stars, single vendor, no dependents. It's a thin factory layer, so lock-in is low — but pin the version (^2.0 on Symfony 5.4–8.x, ^1.0 if you're stuck on Symfony 4), read the changelog before minor bumps (the config keys and service ids have moved historically), and keep your whole integration surface inside one repository package so swapping to hand-rolled client registration is an afternoon's work if you ever need to.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin