Sometimes several document types are variations of the same thing: an Image and a Video are both
Media. Patchlevel ODM can store a whole class hierarchy in a single collection and reconstruct the
right concrete class on load. This is single-collection inheritance, driven by a discriminator field.
Put #[Document] and #[DiscriminatorMap] on the root class and let the concrete classes extend it.
The map assigns a short, stable string to every concrete class. The subclasses inherit the collection
and the #[Id] property from the root, so they do not repeat the #[Document] attribute.
use Patchlevel\ODM\Attribute\DiscriminatorMap;
use Patchlevel\ODM\Attribute\Document;
use Patchlevel\ODM\Attribute\Id;
#[Document('media')]
#[DiscriminatorMap([
'image' => Image::class,
'video' => Video::class,
])]
abstract class Media
{
public function __construct(
#[Id]
public readonly string $id,
public string $title,
) {
}
}
final class Image extends Media
{
public function __construct(string $id, string $title, public int $width)
{
parent::__construct($id, $title);
}
}
final class Video extends Media
{
public function __construct(string $id, string $title, public int $duration)
{
parent::__construct($id, $title);
}
}Every stored document gets an extra field, _type, holding the discriminator value (image or
video). Change the field name with the second argument if _type clashes with a property:
#[DiscriminatorMap([...], field: '_kind')].
The repository for the root class is polymorphic. It accepts any subclass on write and returns the concrete class on read.
$repository = $manager->get(Media::class);
$repository->insert(
new Image('m-1', 'Landscape', 1920),
new Video('m-2', 'Trailer', 90),
);
$repository->find('m-1'); // Image
$repository->find('m-2'); // Video
foreach ($repository->findAll() as $media) {
// Image and Video mixed together
}You can filter by properties declared on the root and by properties that only exist on a subclass. Documents that do not have the field simply do not match.
$repository->findBy(['title' => 'Landscape']); // inherited field
$repository->findOneBy(['width' => 1920]); // Image-only fieldThe repository for a concrete class is scoped to that type. Every query, count and delete is restricted to its discriminator value, and writes reject documents of a sibling type.
$images = $manager->get(Image::class);
$images->count(); // only images
$images->findAll(); // only images
$images->find('m-2'); // null, m-2 is a video
$images->insert(new Video('m-3', 'Clip', 30)); // throws WrongClass_id space.DiscriminatorFieldConflict.#[Index] attributes are read from the root class, because the index belongs to the shared
collection. Declare hierarchy-wide indexes there.The discriminator map is validated when the metadata is built. Mapping a value to a class that does
not extend the root, or leaving the map empty, throws InvalidDiscriminatorMap. Loading a document
whose _type is missing or not in the map throws UnknownDiscriminatorValue, and persisting a
subclass that was left out of the map throws ClassNotInDiscriminatorMap.