A document is a plain PHP class that Patchlevel ODM maps to a collection in your database. You mark
the class with the #[Document] attribute and one property with #[Id]. There is no base class to
extend and no interface to implement, so your documents stay free of framework coupling.
The #[Document] attribute takes the collection name. Exactly one property must carry the #[Id]
attribute, which becomes the document identifier and is stored as the _id field.
use Patchlevel\ODM\Attribute\Document;
use Patchlevel\ODM\Attribute\Id;
#[Document('profiles')]
final class Profile
{
/** @param list<Skill> $skills */
public function __construct(
#[Id]
public readonly string $id,
public string $name,
public Status $status,
public array $skills,
) {
}
}A document needs exactly one #[Id] property. The library throws NoIdPropertyFound when none is
present and MultipleIdPropertiesFound when more than one property is marked.
The #[Document] attribute also takes an optional second argument to store the document in a specific
database, for example #[Document('profiles', database: 'analytics')]. Otherwise the
document lives in the manager's default database.
The identifier is a string. The ODM always stores it under the reserved _id field, regardless of
the property name. If you rename the id property with a field mapping attribute,
the ODM still maps it to and from _id transparently.
$repository->insert(new Profile('r-1', 'Rango', Status::ACTIVE, [new Skill('php')]));
$profile = $repository->get('r-1');Properties are mapped by the hydrator. Scalars, enums, nested objects, arrays and value objects are all supported. Complex values are normalized into a storable representation and reconstructed on load.
enum Status: string
{
case ACTIVE = 'active';
case INACTIVE = 'inactive';
}How nested objects, enums and custom field names are stored is described on the field mapping page.
Indexes speed up queries and can enforce uniqueness. You declare them on the document with the
#[Index] attribute, and the repository synchronizes them with the database on demand. The attribute
is repeatable, so a document can carry as many indexes as it needs.
#[Index] takes a name and a map of property names to a sort direction (asc or desc). The
property names are mapped to their stored field names automatically.
use Patchlevel\ODM\Attribute\Document;
use Patchlevel\ODM\Attribute\Id;
use Patchlevel\ODM\Attribute\Index;
#[Document('profiles')]
#[Index('by_status', ['status' => 'asc'])]
#[Index('by_name', ['name' => 'asc'])]
final class Profile
{
public function __construct(
#[Id]
public readonly string $id,
public string $name,
public Status $status,
) {
}
}Set unique: true to enforce that no two documents share the same value for the indexed properties.
#[Document('profiles')]
#[Index('by_email', ['email' => 'asc'], unique: true)]
final class Profile
{
public function __construct(
#[Id]
public readonly string $id,
public string $email,
) {
}
}Inserting a document that violates a unique index fails with an InsertionFailed exception. Catch it
to detect duplicates.
Indexes are not created automatically when you insert documents. Call updateIndexes() to create the
declared indexes, or createCollection(), which creates the collection together with its indexes.
$repository->updateIndexes();
$repository->createCollection();By default updateIndexes() only adds missing indexes. Pass true to also drop indexes that are no
longer declared on the document. The primary key index is always preserved.
$repository->updateIndexes(dropUnknown: true);Run index synchronization as part of a deployment or migration step rather than on every request, so your collections stay in sync with the document definitions.