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. The build identifier is therefore part of the relational boundary, not only version metadata.

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 rows from several snapshots 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 is a dependency graph

Foreign-key constraints still define ordering requirements. A child cannot be inserted before the parent row it references exists in the same build.

The important distinction is that only dependent work must be ordered. Independent branches can run at the same time.

For example:

Catalog
  |
  +-- Settings
  |
  +-- Products --------+
  |                    |
  +-- Tags -------------+-- Products and Tags can be persisted concurrently
                       |
                       +-- ProductTag rows after both sides exist

A safe build strategy is therefore:

  1. persist common parents required by foreign keys;
  2. group the remaining work by dependency level;
  3. run independent groups concurrently when the runtime and connection pool allow it;
  4. wait for every required branch;
  5. persist join/dependent rows whose parents are now present;
  6. call release() only after the complete graph is valid.

The package does not traverse the graph or schedule coroutines automatically. persist() and persistMany() are explicit persistence APIs; the application owns concurrency and failure coordination.

Sequential example

$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);

This sequential form is valid. A concurrent implementation may execute the independent settings, products and tags branches in parallel after their required parent rows exist, then persist productTags and release the build.

Next chapter: Garbage collection