This guide walks you through a complete example: you define a Profile document, set up a
repository manager, and then insert, load, query, update and remove documents. Only the initial
setup differs between MongoDB and PostgreSQL; every step after it is identical on both.
Install the library together with the driver for your backend.
For PostgreSQL via Rango:
composer require patchlevel/odm patchlevel/rangoFor MongoDB:
composer require patchlevel/odm mongodb/mongodbA document is a plain PHP class marked with the #[Document] attribute. The collection name is the
first argument. One property carries the #[Id] attribute and becomes the document identifier.
use Patchlevel\ODM\Attribute\Document;
use Patchlevel\ODM\Attribute\Id;
use Patchlevel\ODM\Attribute\Index;
#[Document('profiles')]
#[Index('by_status', ['status' => 'asc'])]
final class Profile
{
/** @param list<Skill> $skills */
public function __construct(
#[Id]
public readonly string $id,
public string $name,
public Status $status,
public array $skills,
) {
}
}The document references two small value types and an enum:
enum Status: string
{
case ACTIVE = 'active';
case INACTIVE = 'inactive';
}
#[SkillNormalizer]
final readonly class Skill
{
public function __construct(
public string $value,
) {
}
}The enum and the Skill value object are turned into scalars by the hydrator. The #[SkillNormalizer]
attribute is a custom normalizer. Both are explained on the field mapping page.
The repository manager creates and caches one repository per document class. Build it with the
static create() factory and pass your database client. Pick the manager for your backend; the
repository you get back behaves the same either way.
For PostgreSQL via Rango:
use Patchlevel\ODM\Repository\RangoRepositoryManager;
use Patchlevel\Rango\Client;
$client = new Client($_ENV['POSTGRES_URI']);
$manager = RangoRepositoryManager::create($client);For MongoDB:
use MongoDB\Client;
use Patchlevel\ODM\Repository\MongoDBRepositoryManager;
$client = new Client($_ENV['MONGODB_URI']);
$manager = MongoDBRepositoryManager::create($client);From here on the code is the same for both backends:
$repository = $manager->get(Profile::class);Before storing documents, create the collection and its indexes:
$repository->createCollection();insert() accepts one or many documents and writes them in a single operation.
$repository->insert(
new Profile('r-1', 'Rango', Status::ACTIVE, [new Skill('php')]),
new Profile('r-2', 'Beans', Status::ACTIVE, [new Skill('node'), new Skill('js')]),
new Profile('r-3', 'Elsa', Status::INACTIVE, [new Skill('mongodb')]),
);Use find() to load a document by id, or get() if a missing document should raise an exception.
$profile = $repository->find('r-1'); // Profile|null
$profile = $repository->get('r-1'); // Profile, throws DocumentNotFound when missingfindBy() filters documents and returns an iterable. You can sort, limit and offset the result.
$profiles = iterator_to_array(
$repository->findBy(
filter: ['status' => Status::ACTIVE->value],
orderBy: ['name' => 'asc'],
limit: 10,
),
false,
);Filters and sorting accept the document property names, even when the stored field is renamed.
The querying section covers operators like $in and $or.
There is no Unit of Work, so changes are persisted only when you call update(). Remove documents
by id with remove().
$profile = $repository->get('r-2');
$profile->name = 'New Beans';
$repository->update($profile);
$repository->remove('r-3');You now have a working document store: a Profile document is mapped through attributes, persisted
through a repository, and queried with filters and sorting, all without a Unit of Work.