Patchlevel ODM maps document properties to storage through patchlevel/hydrator. Scalars, enums, nested objects and value objects are normalized into a storable shape on write and reconstructed on read. This page shows how the stored field names are chosen and how to customize them.
By default a property is stored under its own name. The only exception is the #[Id] property, which
is always stored under the reserved _id field no matter what the property is called.
use Patchlevel\ODM\Attribute\Document;
use Patchlevel\ODM\Attribute\Id;
#[Document('profiles')]
final class Profile
{
public function __construct(
#[Id]
public readonly string $id, // stored as _id
public string $name, // stored as name
public Status $status, // stored as status
) {
}
}Use the hydrator's #[NormalizedName] attribute to store a property under a different field name.
This is useful for keeping a stable storage schema while renaming properties in code.
use Patchlevel\Hydrator\Attribute\NormalizedName;
use Patchlevel\ODM\Attribute\Document;
use Patchlevel\ODM\Attribute\Id;
#[Document('profiles')]
final class Profile
{
public function __construct(
#[Id]
public readonly string $id,
#[NormalizedName('_name')]
public string $name,
) {
}
}You still filter and sort by the property name (name), not by the stored field (_name). The ODM
translates property paths to field paths for you, as described in querying.
Nested objects are normalized recursively. Renamed fields on nested objects are respected, and you can filter on them with dot notation.
use Patchlevel\Hydrator\Attribute\NormalizedName;
final readonly class PersonalData
{
public function __construct(
#[NormalizedName('_name')]
public string $name,
#[NormalizedName('_age')]
public int $age,
) {
}
}
#[Document('profiles')]
final class Profile
{
public function __construct(
#[Id]
public readonly string $id,
#[NormalizedName('_personal_data')]
public PersonalData $personalData,
) {
}
}A filter on the nested property uses the property path, which is mapped to the stored field path:
$result = iterator_to_array(
$repository->findBy(['personalData.name' => 'Rango']),
false,
);For value objects with their own representation, write a normalizer and attach it as an attribute. The normalizer converts the object to a storable value and back.
use Patchlevel\Hydrator\Normalizer\InvalidType;
use Patchlevel\Hydrator\Normalizer\NormalizerWithContext;
#[Attribute(Attribute::TARGET_CLASS)]
final class SkillNormalizer implements NormalizerWithContext
{
/** @param array<string, mixed> $context */
public function normalize(mixed $value, array $context = []): mixed
{
if ($value === null) {
return null;
}
if (!$value instanceof Skill) {
throw new InvalidType();
}
return $value->value;
}
/** @param array<string, mixed> $context */
public function denormalize(mixed $value, array $context = []): mixed
{
if ($value === null) {
return null;
}
if (!is_string($value)) {
throw new InvalidType();
}
return new Skill($value);
}
}Attach the normalizer to the value object, then use it like any other property:
#[SkillNormalizer]
final readonly class Skill
{
public function __construct(
public string $value,
) {
}
}The hydrator ships normalizers for enums, dates and arrays out of the box. See the hydrator documentation for the full list.