Skip to content

Stratified relation graphs

A scope can contain a root manager and several dependency managers.

$configs = [
    new StratifiedEntityConfig(CatalogManager::class, 'catalog', 'shopId', scopeRoot: true),
    new StratifiedEntityConfig(CatalogSettingsManager::class, 'catalog', 'shopId'),
    new StratifiedEntityConfig(ProductManager::class, 'catalog', 'shopId'),
    new StratifiedEntityConfig(TagManager::class, 'catalog', 'shopId'),
    new StratifiedEntityConfig(ProductTagManager::class, 'catalog', 'shopId'),
];

Include the strate in every relation key

#[ToOne(
    CatalogSettingsManager::class,
    [
        'id' => 'catalogId',
        'buildStrate' => 'buildStrate',
    ],
)]
private ?CatalogSettingsEntity $settings = null;

#[ToMany(
    ProductManager::class,
    [
        'id' => 'catalogId',
        'buildStrate' => 'buildStrate',
    ],
)]
private ?EntityCollection $products = null;

Without buildStrate in the mapping, a join can combine a current row with a relation from an older snapshot.

Many-to-many

Represent many-to-many relations with an explicit stratified join entity. The join entity owns two ToOne relations; each side exposes a ToMany relation to join rows. Include buildStrate in both mappings and enforce uniqueness with database constraints.

Database constraints and indexes

The database schema must enforce the same snapshot boundary as the ORM metadata. For each stratified table:

  • index (scope_id, build_strate) for released queries and garbage collection;
  • make relation targets unique on (id, build_strate) when a composite foreign key references both columns;
  • include build_strate in child foreign keys;
  • include build_strate in join-entity uniqueness constraints.

Example:

CREATE INDEX idx_product_scope
    ON product (shop_id, build_strate);

ALTER TABLE product
    ADD CONSTRAINT uniq_product_id_build UNIQUE (id, build_strate);

ALTER TABLE product_tag
    ADD CONSTRAINT fk_product_tag_product
    FOREIGN KEY (product_id, build_strate)
    REFERENCES product (id, build_strate)
    ON DELETE CASCADE;

ALTER TABLE product_tag
    ADD CONSTRAINT uniq_product_tag_link
    UNIQUE (product_id, tag_id, build_strate);

The table primary-key strategy must permit every snapshot row to coexist. Generate a fresh row id for every snapshot, as in the supplied fixtures, or use a schema whose composite primary key includes build_strate.

Persistence order

Persist parents before foreign-key children, then release only after the complete graph has been written.

$strate = $stratifiedPersist->createNewVersion(
    [
        CatalogManager::class,
        CatalogSettingsManager::class,
        ProductManager::class,
        TagManager::class,
        ProductTagManager::class,
    ],
    'catalog',
    $shopId,
);

$stratifiedPersist->persist($catalog, $strate);
$stratifiedPersist->persist($settings, $strate);
$stratifiedPersist->persistMany($products, $strate);
$stratifiedPersist->persistMany($tags, $strate);
$stratifiedPersist->persistMany($productTags, $strate);
$stratifiedPersist->release('catalog', $shopId, $strate);

The package does not traverse or persist a relation graph automatically. Every entity or collection must be persisted explicitly.

Next chapter: Garbage collection