Vortos
Persistence

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 time

Environment Variables

MONGODB_URL=mongodb://root:secret@read_db:27017
MONGO_DB_NAME=myapp_reads

Configuration

config/persistence.php
$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.net

Register Your Read Repositories

Register your read repository class in config/services.php — the framework creates and injects a configured MongoStore at compile time:

config/services.php
$services->set(UserReadRepository::class);
$services->set(OrderReadRepository::class);

MongoReadRepositoryAutowirePass detects every class with #[MongoCollection] and:

  1. Creates a named MongoStore service wired to the correct collection, database, and MongoDB\Client
  2. Injects the store as the $store constructor argument of the repository
  3. Tags the store with vortos.read_repository (used by tracing and metrics compiler passes)
  4. Registers the repository class name with MongoIndexAttributeScanner so vortos:mongo:sync finds 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:

src/User/Infrastructure/Persistence/Mongo/UserReadRepository.php
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:

ParameterTypeDefaultDescription
keyarrayrequiredField → direction (1 asc, -1 desc, 'text')
uniqueboolfalseEnforce uniqueness
sparseboolfalseOnly index documents where the field exists
expireAfterSecondsint|nullnullTTL — MongoDB auto-deletes after N seconds
namestring|nullnullExplicit 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-run

Example 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:sync

Store _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

config/services.php
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

On this page