MongoDB
MongoDB client setup, DSN format, connection behaviour, index management with PHP attributes, and bulk operations.
MongoDB
vortos-persistence-mongo provides MongoDB integration for the read side. It registers a shared MongoDB\Client instance and exposes the database name as a container parameter.
What Gets Registered
MongoDB\Client::class — shared, built from MONGODB_URL
vortos.persistence.mongo.database_name — container parameter
MongoIndexAttributeScanner — populated with repo classes at compile timeEnvironment Variables
MONGODB_URL=mongodb://root:secret@read_db:27017
MONGO_DB_NAME=myapp_readsConfiguration
$config
->readDsn($_ENV['MONGODB_URL'])
->readDatabase($_ENV['MONGO_DB_NAME']);The database name is kept separate from the DSN so it can be overridden per environment without changing the connection string.
MongoDB\Client Is NOT Lazy
Unlike Doctrine DBAL, MongoDB\Client establishes a connection immediately on construction. If MongoDB is unreachable at container boot time, the application fails to start. Use health checks in your deployment pipeline to verify MongoDB is ready before starting the application.
DSN Format
mongodb://[user:pass@]host[:port][/database][?options]Examples
# Local development
MONGODB_URL=mongodb://localhost:27017
# With authentication
MONGODB_URL=mongodb://root:secret@read_db:27017
# Replica set
MONGODB_URL=mongodb://user:pass@host1:27017,host2:27017/?replicaSet=rs0
# MongoDB Atlas
MONGODB_URL=mongodb+srv://user:pass@cluster0.abc123.mongodb.netRegister Your Read Repositories
Register your read repository class in config/services.php — the framework creates and injects a configured MongoStore at compile time:
$services->set(UserReadRepository::class);
$services->set(OrderReadRepository::class);MongoReadRepositoryAutowirePass detects every class with #[MongoCollection] and:
- Creates a named
MongoStoreservice wired to the correct collection, database, andMongoDB\Client - Injects the store as the
$storeconstructor argument of the repository - Tags the store with
vortos.read_repository(used by tracing and metrics compiler passes) - Registers the repository class name with
MongoIndexAttributeScannersovortos:mongo:syncfinds it
Collections Are Created Automatically
MongoDB creates a collection the first time you write to it or call createIndex() on it. You do not need to pre-create collections the way you create tables in SQL.
Index Management
Why indexes are not part of migrations
SQL schema changes need versioning because they are destructive and ordered — dropping a column cannot be undone. MongoDB's createIndex() is idempotent — calling it on an existing index is a no-op. There is nothing to track and nothing to roll back.
Mixing MongoDB indexes into the SQL migration system would couple infrastructure concerns that have nothing in common.
Declaring indexes with PHP attributes
Add #[MongoCollection] and repeatable #[MongoIndex] directly on the repository class — the same class that uses the indexes for queries:
use Vortos\PersistenceMongo\Read\MongoStore;
use Vortos\PersistenceMongo\Schema\Attribute\MongoCollection;
use Vortos\PersistenceMongo\Schema\Attribute\MongoIndex;
#[MongoCollection('users')]
#[MongoIndex(key: ['email' => 1], unique: true)]
#[MongoIndex(key: ['createdAt' => -1, '_id' => -1])]
#[MongoIndex(key: ['deletedAt' => 1], sparse: true)]
#[MongoIndex(key: ['expiresAt' => 1], expireAfterSeconds: 3600)]
final class UserReadRepository implements UserReadRepositoryInterface
{
public function __construct(private readonly MongoStore $store) {}
// ...
}#[MongoIndex] parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
key | array | required | Field → direction (1 asc, -1 desc, 'text') |
unique | bool | false | Enforce uniqueness |
sparse | bool | false | Only index documents where the field exists |
expireAfterSeconds | int|null | null | TTL — MongoDB auto-deletes after N seconds |
name | string|null | null | Explicit index name (auto-named if omitted) |
MongoReadRepositoryAutowirePass registers each #[MongoCollection]-annotated repository class with MongoIndexAttributeScanner at compile time. At runtime, the scanner reflects over the class and reads these attributes — no glob paths, no provider files.
Applying indexes with vortos:mongo:sync
php bin/console vortos:mongo:sync# Preview without writing to MongoDB
php bin/console vortos:mongo:sync --dry-runExample output:
Vortos MongoDB Index Sync
users
✔ email (unique)
✔ createdAt DESC, _id DESC
✔ deletedAt (sparse)
✔ 3 index(es) ensured across 1 collection(s).Deploy pipeline
# 1. Apply SQL schema changes (transactional, versioned)
php bin/console vortos:migrate --force
# 2. Apply MongoDB desired index state (idempotent, no tracking)
php bin/console vortos:mongo:syncStore _id as String UUID
Always use string UUIDs for _id, not MongoDB ObjectId:
// CORRECT — consistent with write-side UuidV7 identity
$this->collection()->insertOne([
'_id' => $event->userId, // string UUID
'email' => $event->email,
]);
// WRONG — ObjectId is incompatible with UuidV7 identity
$this->collection()->insertOne([
'_id' => new \MongoDB\BSON\ObjectId(),
'email' => $event->email,
]);Bulk Operations
bulkUpsert(array $documents): void
$userReadRepository->bulkUpsert([
['_id' => 'user-1', 'email' => 'alice@example.com', 'status' => 'active'],
['_id' => 'user-2', 'email' => 'bob@example.com', 'status' => 'active'],
]);Uses replaceOne with upsert: true for each document. All documents must have _id.
bulkDelete(array $ids): void
$userReadRepository->bulkDelete(['user-1', 'user-2']);Single deleteMany with $in filter.
Advanced Queries via store->collection()
Access the raw MongoDB\Collection for queries findByCriteria() cannot express:
// Aggregation pipeline
public function getStatsByRole(): array
{
return iterator_to_array(
$this->store->collection()->aggregate([
['$group' => ['_id' => '$role', 'count' => ['$sum' => 1]]],
['$sort' => ['count' => -1]],
])
);
}Multiple Databases
use MongoDB\Client;
use Vortos\PersistenceMongo\Connection\MongoClientFactory;
$services->set('mongodb.analytics_client', Client::class)
->factory([MongoClientFactory::class, 'fromDsn'])
->arg('$dsn', $_ENV['ANALYTICS_MONGODB_URL'])
->public();
$services->set(AnalyticsReadRepository::class)
->arg('$client', service('mongodb.analytics_client'))
->arg('$databaseName', $_ENV['ANALYTICS_DB_NAME']);
$services->set(UserReadRepository::class); // auto-wired to the default client