CoolCMS API

SetCompositionService
in package

FinalYes

Product set composition: the `ProductSet` contains `ProductSet` tree over `cms_mod_eshop_set_item_relations`.

Provides the cycle guard for the save hook and the recursive decomposition into leaves (type_id = PRODUCT, then variant, then Store stock item). The Store bridge and the order reservation both read the decomposition from here; there is no second implementation.

IMPORTANT: everything goes through raw DBAL (Connection), never through a manager or find(). The save hooks call this service and EntityManager::clear() inside the generic save would detach managed entities. Data is small (units to tens of relations), so the traversal goes level by level (breadth-first): one query per level, not one per node. Recursion is always bounded by MAX_DEPTH and the set of visited nodes, so a cycle that got into the database (manual SQL) is stopped, not looped.

IMPORTANT: an inactive component is not counted. A component with deleted = 1 or activated = 0 (or a leaf whose variant has deleted = 1) must not enter the stock math, otherwise a bundle would offer availability backed by withdrawn goods and the order reservation would oversell it. Such a node is not expanded and is returned in unresolvedSetIds and in inactiveSetIds (fail-safe: the caller sees "on request", not 0 and not an invented number). inactiveSetIds is a subset of unresolvedSetIds; it exists so that the admin can name the withdrawn component instead of hiding it under a generic "unresolvable".

Table of Contents

Constants

MAX_DEPTH  : int = 10
Maximum nesting depth of sets (practical ceiling; a deeper tree is almost surely a data error).
CHILD  : string = 'cms_mod_eshop_product_sets_product_set_id1'
ITEMS  : string = 'cms_mod_eshop_set_item_relations'
PARENT  : string = 'cms_mod_eshop_product_sets_product_set_id'

Properties

$em  : EntityManagerInterface

Methods

__construct()  : mixed
ancestorsOf()  : array<int, int>
Returns all sets that directly or indirectly contain `$setId` (ancestors in the composition tree). The admin excludes them from the item picker so that a cycle cannot even be offered.
decompose()  : SetDecomposition
Decomposes a set recursively into leaves: physical variants and their stock items.
decomposeEach()  : array<int, SetDecomposition>
Decomposes N sets independently; each gets its own {@see SetDecomposition}.
decomposeMany()  : SetDecomposition
Decomposes several sets at once (cart/order); quantities are summed.
descendantsOf()  : array<int, true>
Returns all sets reachable from `$seedIds` (seeds included), level by level, with cycle protection and the depth ceiling.
wouldCreateCycle()  : bool
Tells whether assigning `$childIds` as items of set `$setId` would create a cycle (A contains B contains A).
buildDecomposition()  : SetDecomposition
Assembles the result from already loaded leaf states, without a single query, so that {@see decomposeEach()} can serve all sets of the batch with one `leafStates()` call.
childMap()  : array<int, array<int, int>>
Loads set items in a batch (one level of the tree).
childRows()  : array<int, array<string, mixed>>
collectSubgraph()  : array{0: array>, 1: list}
Loads the edges of the subgraph reachable from the seeds, level by level (one query per level), with cycle protection and the depth ceiling.
leafStates()  : array<int, array{variantId: ?int, stockItemId: ?int, usable: bool}>
Loads leaves in a batch: variant, its stock item and the variant's usability.
leavesToDecomposition()  : SetDecomposition
Resolves sales cards (type 1) to variants and stock items (one query plus in-memory assembly).
loadSubgraph()  : array{0: array>, 1: list, 2: array, 3: array, 4: array}
Loads the subgraph from the seeds and everything needed to traverse it. This is the only place where this family of methods touches the database (at most `MAX_DEPTH` edge queries plus one for set states).
normalizeSeeds()  : array<int, int>
propagate()  : array{0: array, 1: array, 2: array, 3: array}
Propagates quantities through the subgraph in an order where a node comes up only after all its parents (see {@see topologicalOrder()}), so contributions of all paths are summed.
setStates()  : array<int, array{typeId: int, usable: bool}>
Loads set states in a batch: type and usability (`deleted = 0` and `activated = 1`).
subgraphNodes()  : array<int, int>
Returns the subgraph nodes: the seeds plus everything reachable from them.
toInt()  : int
Casts a `mixed` DBAL value safely to int.
topologicalOrder()  : array<int, int>
Computes the topological order of the subgraph (Kahn): a node comes out only after all its parents. Computed once per batch and shared by every per-set propagation.

Constants

MAX_DEPTH

Maximum nesting depth of sets (practical ceiling; a deeper tree is almost surely a data error).

public int MAX_DEPTH = 10

Properties

Methods

__construct()

public __construct(EntityManagerInterface $em) : mixed
Parameters
$em : EntityManagerInterface

ancestorsOf()

Returns all sets that directly or indirectly contain `$setId` (ancestors in the composition tree). The admin excludes them from the item picker so that a cycle cannot even be offered.

public ancestorsOf(int $setId) : array<int, int>
Parameters
$setId : int
Return values
array<int, int>

decompose()

Decomposes a set recursively into leaves: physical variants and their stock items.

public decompose(int $setId[, int $quantity = 1 ]) : SetDecomposition

Counts multiply across levels: qty(leaf) is the sum over all paths of the product of count along the path, times $quantity. A leaf is a set with type_id = PRODUCT (it has an FK to a variant). A bundle without items or a product without a variant does not reach the result and is returned in unresolvedSetIds; a variant without a stock item is returned in unmappedVariantIds (the Store bridge uses it to see what is not mapped yet).

Parameters
$setId : int
$quantity : int = 1
Return values
SetDecomposition

decomposeEach()

Decomposes N sets independently; each gets its own {@see SetDecomposition}.

public decomposeEach(array<int, int> $quantities) : array<int, SetDecomposition>

Unlike decomposeMany(), which sums quantities (one cart is one reservation), this keeps them apart (catalog: the availability of set A must not affect set B).

The subgraph, set states and leaves are loaded by one set of queries for all sets and the per-set quantity propagation runs in memory over the shared topological order, so the SQL count is constant (at most MAX_DEPTH + 2) regardless of the number of sets. Looping decompose() per set would cost about 3N queries per list page.

IMPORTANT: the seed (the set being asked about) is exempt from the inactive-component filter, because the admin must see stock even for a deactivated or deleted set being edited. The filter applies to components only; whether the set itself is publishable is checked by the caller (front ProductSetEligibilityFilters).

Parameters
$quantities : array<int, int>

setId => number of pieces

Return values
array<int, SetDecomposition> —

keyed by set id (only sets with a positive quantity)

decomposeMany()

Decomposes several sets at once (cart/order); quantities are summed.

public decomposeMany(array<int, int> $quantities) : SetDecomposition

IMPORTANT: the expansion cannot go level by level with a set of already expanded sets. The composition is a directed acyclic graph, not a tree: the same set can hang at two depths (X contains B and M, M contains B). Remembering "B is already expanded" would count the second occurrence once and drop the rest into unresolvedSetIds, an understated quantity that oversells. So the traversal is topological (Kahn): a node is expanded only after all its parents are done, which sums the contributions of all paths. Nodes that never come out in topological order lie on a cycle (or behind it); they stay out of the result and are returned in unresolvedSetIds. A cycle can only come from manual SQL (the application rejects it, see wouldCreateCycle()).

Parameters
$quantities : array<int, int>

setId => number of pieces

Return values
SetDecomposition

descendantsOf()

Returns all sets reachable from `$seedIds` (seeds included), level by level, with cycle protection and the depth ceiling.

public descendantsOf(array<int, int> $seedIds) : array<int, true>
Parameters
$seedIds : array<int, int>
Return values
array<int, true> —

set of ids (key is the set id)

wouldCreateCycle()

Tells whether assigning `$childIds` as items of set `$setId` would create a cycle (A contains B contains A).

public wouldCreateCycle(int $setId, array<int, int> $childIds) : bool

Called from ProductSetManager::beforeSave, before the new items are in the database: the edges leaving $setId are therefore the given (in-memory) ones, the rest of the graph is read from the database. A self-reference ($setId among $childIds) is a cycle too.

Parameters
$setId : int
$childIds : array<int, int>
Return values
bool

buildDecomposition()

Assembles the result from already loaded leaf states, without a single query, so that {@see decomposeEach()} can serve all sets of the batch with one `leafStates()` call.

private buildDecomposition(array<int, int> $leafSets, array<int, array{variantId: ?int, stockItemId: ?int, usable: bool}> $leafStates, array<int, int> $unresolvedSetIds, array<int, int> $inactiveSetIds, array<int, true> $exempt) : SetDecomposition
Parameters
$leafSets : array<int, int>

setId => quantity

$leafStates : array<int, array{variantId: ?int, stockItemId: ?int, usable: bool}>
$unresolvedSetIds : array<int, int>
$inactiveSetIds : array<int, int>
$exempt : array<int, true>
Return values
SetDecomposition

childMap()

Loads set items in a batch (one level of the tree).

private childMap(array<int, int> $parentIds) : array<int, array<int, int>>
Parameters
$parentIds : array<int, int>
Return values
array<int, array<int, int>> —

parentId => [childId => count]

childRows()

private childRows(array<int, int> $parentIds) : array<int, array<string, mixed>>
Parameters
$parentIds : array<int, int>
Return values
array<int, array<string, mixed>>

collectSubgraph()

Loads the edges of the subgraph reachable from the seeds, level by level (one query per level), with cycle protection and the depth ceiling.

private collectSubgraph(array<int, int> $seedIds) : array{0: array>, 1: list}
Parameters
$seedIds : array<int, int>
Return values
array{0: array>, 1: list} —

[parentId => [childId => count], sets beyond the depth ceiling]

leafStates()

Loads leaves in a batch: variant, its stock item and the variant's usability.

private leafStates(array<int, int> $leafIds) : array<int, array{variantId: ?int, stockItemId: ?int, usable: bool}>

ProductVariant is a thin pivot without its own activated, so only deleted is tested.

Parameters
$leafIds : array<int, int>
Return values
array<int, array{variantId: ?int, stockItemId: ?int, usable: bool}>

leavesToDecomposition()

Resolves sales cards (type 1) to variants and stock items (one query plus in-memory assembly).

private leavesToDecomposition(array<int, int> $leafSets, array<int, int> $unresolvedSetIds, array<int, int> $inactiveSetIds, array<int, true> $exempt) : SetDecomposition
Parameters
$leafSets : array<int, int>

setId => quantity

$unresolvedSetIds : array<int, int>
$inactiveSetIds : array<int, int>
$exempt : array<int, true>

sets exempt from the inactive variant filter

Return values
SetDecomposition

loadSubgraph()

Loads the subgraph from the seeds and everything needed to traverse it. This is the only place where this family of methods touches the database (at most `MAX_DEPTH` edge queries plus one for set states).

private loadSubgraph(array<int, int> $seedIds) : array{0: array>, 1: list, 2: array, 3: array, 4: array}
Parameters
$seedIds : array<int, int>
Return values
array{0: array>, 1: list, 2: array, 3: array, 4: array} —

[edges, sets beyond the depth ceiling, set states, topological order, subgraph nodes]

normalizeSeeds()

private normalizeSeeds(array<int, int> $quantities) : array<int, int>
Parameters
$quantities : array<int, int>
Return values
array<int, int> —

setId => sum of positive quantities

propagate()

Propagates quantities through the subgraph in an order where a node comes up only after all its parents (see {@see topologicalOrder()}), so contributions of all paths are summed.

private propagate(array<int, int> $seedQuantities, array<int, array<int, int>> $edges, array<int, array{typeId: int, usable: bool}> $states, array<int, int> $order, array<int, true> $exempt) : array{0: array, 1: array, 2: array, 3: array}
Parameters
$seedQuantities : array<int, int>

setId => number of pieces

$edges : array<int, array<int, int>>

parentId => [childId => count]

$states : array<int, array{typeId: int, usable: bool}>
$order : array<int, int>

setId => position in the topological order

$exempt : array<int, true>

sets exempt from the inactive filter (the seed in decomposeEach())

Return values
array{0: array, 1: array, 2: array, 3: array} —

[leaves, unresolved, inactive, accumulated quantities per node]

setStates()

Loads set states in a batch: type and usability (`deleted = 0` and `activated = 1`).

private setStates(array<int, int> $ids) : array<int, array{typeId: int, usable: bool}>
Parameters
$ids : array<int, int>
Return values
array<int, array{typeId: int, usable: bool}>

subgraphNodes()

Returns the subgraph nodes: the seeds plus everything reachable from them.

private subgraphNodes(array<int, int> $seeds, array<int, array<int, int>> $edges) : array<int, int>
Parameters
$seeds : array<int, int>
$edges : array<int, array<int, int>>
Return values
array<int, int>

toInt()

Casts a `mixed` DBAL value safely to int.

private toInt(mixed $value) : int
Parameters
$value : mixed
Return values
int

topologicalOrder()

Computes the topological order of the subgraph (Kahn): a node comes out only after all its parents. Computed once per batch and shared by every per-set propagation.

private topologicalOrder(array<int, int> $nodes, array<int, array<int, int>> $edges) : array<int, int>

IMPORTANT: the order is purely structural (it knows neither quantities nor the inactive filter). If indegree were decreased only when a node is actually expanded, a blocked (inactive) branch would also freeze children that hang under another, healthy parent.

Parameters
$nodes : array<int, int>
$edges : array<int, array<int, int>>
Return values
array<int, int> —

setId => position (nodes on a cycle are not in the array)

On this page

Search results