mongodb/symfony-bundle
Official Symfony bundle for MongoDB that integrates clients and collections with Symfony's dependency injection container, supports autowiring and multiple connections, and adds configuration, profiling, and logging hooks to reduce boilerplate when using MongoDB in Symfony applications.
Install the bundle and the MongoDB PHP library:
composer require mongodb/symfony-bundle mongodb/mongodb
Enable MongoDB\Bundle\SymfonyBundle\MongoDBBundle in config/bundles.php if Flex does not do it for you:
return [
// ...
MongoDB\Bundle\SymfonyBundle\MongoDBBundle::class => ['all' => true],
];
Check which driver you actually got. As of bundle 0.3.1, the MongoDB Driver 2.0 extension (ext-mongodb) is supported alongside 1.x, so the version resolved on each machine can differ:
php -r 'echo phpversion("mongodb"), PHP_EOL;'
composer show mongodb/mongodb mongodb/symfony-bundle
Configure a client. The bundle's main job is to turn MongoDB client configuration into Symfony services:
# config/packages/mongodb.yaml
mongodb:
clients:
default:
uri: '%env(MONGODB_URI)%'
uriOptions: {}
driverOptions: {}
Put the connection string in .env.local or a secret:
MONGODB_URI=mongodb://app:password@localhost:27017/?authSource=admin
The first use case is usually replacing manual new MongoDB\Client(...) calls with dependency injection. Inject the default client and use it directly:
namespace App\Service;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
final class AuditLogger
{
public function __construct(private Client $client)
{
}
public function log(string $event, array $context): void
{
$this->client
->selectCollection('app', 'audit_log')
->insertOne([
'event' => $event,
'context' => $context,
'createdAt' => new UTCDateTime(),
]);
}
}
Check what the bundle actually registered before building assumptions into your app:
bin/console debug:config mongodb
bin/console debug:container 'MongoDB\Client'
bin/console debug:container --tag=container.service_subscriber
Start by reading the package README and the generated container configuration. The bundle is intentionally small: it wires clients, not an object-document mapper.
Wrap collections in repository services. Do not scatter selectCollection('app', 'users') across controllers and commands. Create a repository per aggregate or use case and inject the client into it:
namespace App\Repository;
use MongoDB\Client;
use MongoDB\Collection;
final class UserRepository
{
private Collection $collection;
public function __construct(Client $client)
{
$this->collection = $client->selectCollection('app', 'users');
}
public function findByEmail(string $email): ?array
{
$user = $this->collection->findOne(['email' => $email]);
return $user?->getArrayCopy();
}
public function create(array $user): string
{
return (string) $this->collection->insertOne($user)->getInsertedId();
}
}
This keeps collection names, database names, and query shapes in one place and makes unit testing easier because you can depend on your repository interface instead of MongoDB\Client. It also confines the blast radius when the underlying driver or library changes.
Pin the driver/library pair across environments. Since 0.3.1 widened compatibility to driver 2.0, add explicit constraints so CI, local Docker, and production cannot silently disagree:
{
"require": {
"ext-mongodb": "^1.20 || ^2.0",
"mongodb/mongodb": "^1.21 || ^2.0",
"mongodb/symfony-bundle": "0.3.1"
}
}
Verify both combinations your composer constraints permit, and mirror the extension version in your Docker base image. If you are not ready to move, tighten the constraint to ^1.20 rather than relying on the resolver.
Use multiple clients deliberately. If you configure more than one client, verify the service IDs and inject explicitly:
mongodb:
clients:
default:
uri: '%env(MONGODB_URI)%'
reporting:
uri: '%env(MONGODB_REPORTING_URI)%'
uriOptions:
readPreference: secondaryPreferred
use MongoDB\Client;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class ReportBuilder
{
public function __construct(
#[Autowire(service: 'mongodb.client.reporting')]
private Client $reportingClient,
) {
}
}
Confirm the actual service ID with debug:container; if the bundle does not register the alias you expect, add one in services.yaml.
Centralize database and collection selection. If the bundle does not already expose database/collection services in your version, keep those names in configuration parameters and resolve them in repositories:
parameters:
app.mongodb.database: '%env(MONGODB_DATABASE)%'
public function __construct(
Client $client,
private string $database,
) {
$this->collection = $client->selectCollection($database, 'users');
}
Use the driver directly for MongoDB-specific operations. This bundle does not hide the MongoDB PHP library. For atomic updates, aggregation pipelines, bulk writes, and transactions, use the library API:
$this->collection->updateOne(
['_id' => $id],
['$set' => ['updatedAt' => new UTCDateTime()]]
);
Keep writes and reads explicit about consistency. Read preference, write concern, and timeouts belong in client configuration or per-operation options. For reporting clients, use secondaryPreferred; for critical writes, keep the default primary read/write settings and configure w: majority where needed.
Use transactions only when the deployment supports them. MongoDB transactions require a replica set or sharded cluster. A standalone development server will fail at runtime, so test transaction paths against a realistic topology.
Instrument repositories instead of relying on a Symfony profiler panel. There is no Doctrine-style data collector here. Add logging around slow queries, use MongoDB's explain commands when debugging, and consider a MongoDB\Driver\Monitoring\CommandSubscriber if you need centralized command logging. Driver 2.0 is the best time to add this, because it is the layer that changed, not your repositories.
This is not Doctrine MongoDB ODM. There is no document mapping, no identity map, no lazy loading, no schema validation, and no migration system. You write the persistence layer yourself or adopt Doctrine MongoDB ODM separately. Choose this bundle when you want lightweight client integration, not a full ODM.
Treat the package as experimental. It is maintained under mongodb-labs, and the ecosystem around it is small. Pin the version in composer.json (0.3.1 at minimum), read the changelog before upgrades, and be prepared to inspect the bundle source when configuration behavior is unclear.
The 0.3.1 change is a compatibility widening, not a rewrite. It adds MongoDB Driver 2.0 support; nothing in the bundle API or configuration schema changed between 0.3.0 and 0.3.1. The real risk is the transitive ext-mongodb bump that suddenly becomes resolvable. A composer update that previously stopped at driver 1.x can now pull 2.0 with no bundle change in your diff — audit your lock file after upgrading.
Verify the driver on every runtime you deploy to. ext-mongodb is a C extension, not a Composer-only dependency. composer.json constraints protect the library, not the loaded extension. A host still compiled against 1.x will fail autoload or throw at the MongoDB\Client boundary with a class-not-found or signature error that points at your service wiring, not the extension. Add a deploy-time assertion:
if (version_compare(phpversion('mongodb'), '2.0.0', '<')) {
throw new \RuntimeException('MongoDB extension 1.x detected; expected 2.x.');
}
Exercise the BSON surface after the driver upgrade. ObjectId, UTCDateTime, Decimal128, and binary values are produced by the extension. Driver 2.0 removes previously deprecated behavior, so round-trip tests through your repositories — write a document, read it back, compare types — are worth more than a smoke-test ping. Existing documents serialized under 1.x may deserialize differently at the edges.
Autowiring the default client can fail if the client name is not default. The bundle may alias the default client to MongoDB\Client, but verify with debug:container. If needed, add an explicit alias:
services:
MongoDB\Client:
alias: mongodb.client.default
Connections are lazy. A bad URI or unavailable server may not fail at container boot. It often fails on the first query, after the default server selection timeout. Add a health check:
$client->selectDatabase('admin')->command(['ping' => 1])->toArray();
Set serverSelectionTimeoutMS in uriOptions for tests and health checks so failures return quickly.
Be careful with BSON types. ObjectId, UTCDateTime, Decimal128, and binary values do not behave like plain PHP scalars. Do not json_encode() raw BSON documents without converting them. Use MongoDB\BSON\toPHP() / toJSON() or a serializer, and decide early whether your domain uses string IDs or ObjectId instances.
Do not return large cursors from service boundaries. find() returns a cursor; calling toArray() loads everything into memory. Iterate with foreach for large result sets, or convert to a bounded array in the repository with an explicit limit.
Indexes are your responsibility. The bundle does not manage indexes. Create them in a console command, deployment script, or migration-like process. A selectCollection() call does not validate that the collection or index exists.
Watch out for transaction and topology assumptions. Transactions, change streams, and some read preferences require replica sets or sharded clusters. Local standalone MongoDB instances can make code work in development and fail in production, or vice versa.
Avoid logging full connection URIs. URIs often contain credentials. Log the host/database and sanitized options instead. Prefer environment variables and Symfony secrets for MONGODB_URI.
Use extension points sparingly but deliberately. You can decorate your own repositories, override or alias bundle services, add a compiler pass for custom collection injection, or register a command subscriber for observability. Keep those extensions outside the bundle so upgrades stay straightforward — particularly while the package is in mongodb-labs and driver majors are still moving.
How can I help you explore Laravel packages today?