Repository

A repository stores and loads documents of a single type. You obtain one from a repository manager by passing the document class. The manager creates the repository on first use and caches it, so calling get() repeatedly returns the same instance.

The manager you pick depends on your backend, but the repository it returns exposes the same methods on both MongoDB and PostgreSQL:

use Patchlevel\ODM\Repository\MongoDBRepositoryManager;
use Patchlevel\ODM\Repository\RangoRepositoryManager;

// PostgreSQL via Rango
$manager = RangoRepositoryManager::create($rangoClient);

// MongoDB
$manager = MongoDBRepositoryManager::create($mongoClient);

$repository = $manager->get(Profile::class);

See the databases page for how to create the client and manager for each backend.

No Unit of Work

Patchlevel ODM has no Unit of Work. The repository never tracks your documents and never persists changes on its own. A document is written only when you explicitly call insert() or update(). This keeps memory usage flat in long-running workers and prevents changes from leaking into the database from unrelated parts of the code.

Inserting

insert() accepts one or many documents. A single document is written with one operation, multiple documents are written in a batch.

$repository->insert(new Profile('r-1', 'Rango', Status::ACTIVE, [new Skill('php')]));

$repository->insert(
    new Profile('r-2', 'Beans', Status::ACTIVE, [new Skill('js')]),
    new Profile('r-3', 'Elsa', Status::INACTIVE, [new Skill('go')]),
);

Inserting a document whose id already exists fails with an InsertionFailed exception. The same exception is raised when a unique index is violated.

Updating

update() replaces the stored fields of existing documents. Because there is no change tracking, you pass the full document you want to persist.

$profile = $repository->get('r-1');
$profile->name = 'Rango Updated';

$repository->update($profile);

Loading by id

find() returns the document or null. get() returns the document or throws DocumentNotFound when it does not exist, which is convenient when the document is required.

$profile = $repository->find('r-1');   // Profile|null
$profile = $repository->get('r-1');    // Profile, throws DocumentNotFound when missing

Existence and counting

$repository->has('r-1');   // bool
$repository->count();      // int, number of documents in the collection

Iterating all documents

findAll() streams every document in the collection as an iterable.

foreach ($repository->findAll() as $profile) {
    echo $profile->name;
}

To filter, sort or paginate instead of loading everything, use findBy() and findOneBy() from the querying section below.

Querying

Besides loading documents by id, the repository can query a collection with filters, sorting and pagination. Queries use the document property names, even when a property is stored under a different field name, so you never deal with the raw storage layout.

Filtering

findBy() takes a filter array and returns an iterable of documents. Each key is a property name and each value is the value to match.

$active = iterator_to_array(
    $repository->findBy(['status' => 'active']),
    false,
);

findBy() returns a generator, so wrap it in iterator_to_array() when you need an array. Pass false as the second argument to reindex the result.

Operators

Filter values support the MongoDB-style query operators. Operator keys start with $ and are passed through untouched, while the property names around them are still mapped to their stored field names.

$selected = iterator_to_array(
    $repository->findBy(['id' => ['$in' => ['r-1', 'r-3']]]),
    false,
);

$result = iterator_to_array(
    $repository->findBy([
        '$or' => [
            ['status' => 'active'],
            ['name' => 'Rango'],
        ],
    ]),
    false,
);

The same operators work on both backends, because MongoDB and Rango share the same query API.

Sorting

Pass an orderBy array of property names mapped to asc or desc.

$sorted = iterator_to_array(
    $repository->findBy([], orderBy: ['name' => 'asc']),
    false,
);

Pagination

Use limit and offset to page through a result set.

$page = iterator_to_array(
    $repository->findBy(
        filter: ['status' => 'active'],
        orderBy: ['name' => 'asc'],
        limit: 10,
        offset: 20,
    ),
    false,
);

Fetching a single document

findOneBy() returns the first matching document or null. It accepts the same filter and an optional orderBy.

$profile = $repository->findOneBy(['name' => 'Beans']);

$newest = $repository->findOneBy([], orderBy: ['name' => 'desc']);

Filtering on nested properties

You can filter and sort on nested properties using dot notation. The path is resolved through the field mapping, so renamed fields are handled automatically.

$result = iterator_to_array(
    $repository->findBy(['personalData.name' => 'Rango']),
    false,
);

Every property in a filter or sort must exist on the document. An unknown path raises UnknownPropertyPath, which lists the properties that are available at that level.

Removing

remove() deletes documents by id and accepts one or many ids.

$repository->remove('r-1');
$repository->remove('r-2', 'r-3');

Managing the collection

The repository can create and drop its own collection. createCollection() also creates the indexes declared on the document.

$repository->createCollection();
$repository->dropCollection();

dropCollection() permanently deletes the collection and all of its documents.

Learn more