CoolCMS API

RegionRepository extends OrmRepository
in package
uses TreeTrait

Abstract ORM repository with dynamic-includes support.

Every concrete repository extends this class.

Table of Contents

Constants

HREF_BATCH_SIZE  : int = 1000
Number of rows in one bulk href `UPDATE`.
HREF_LOOKUP_BATCH_SIZE  : int = 10000
Number of values in one `IN (...)` collision query (every batch is a table scan).
REGION_POSTCODES_TABLE  : string = 'cms_system_region_postcodes'
REGION_TEXTS_TABLE  : string = 'cms_system_region_texts'
REGIONS_TABLE  : string = 'cms_system_regions'
TYPE_COUNTRY  : string = 'country'
Country node type (level 1); matched through the index `(country_code, type, ...)`.
TYPE_MUNICIPALITY  : string = 'municipality'
Node type an advert may be attached to (a tree leaf).

Properties

$db  : EntityManagerInterface
$entityManager  : EntityManagerInterface
$entityUpdater  : EntityUpdater
$managerProvider  : ManagerProvider
$transactionManager  : TransactionManager
$treeLockTimeout  : int
Timeout of `GET_LOCK` in seconds. A move on about 10 000 nodes takes tens of ms, so 10 s covers a queue of several operations; beyond it the result is `TREE_LOCKED`.

Methods

__construct()  : mixed
beginTransaction()  : void
buildTreeLockName()  : string
Returns the advisory tree lock name: `tree:<table truncated to 26 chars>:<md5(table + scope)>`.
closeTreeGap()  : Result
Closes the numbering gap after a deleted subtree: all bounds after the right bound of the deleted node shift left by its width. Ancestors (lft before the gap) only lose `rgt`, nodes after the gap lose both bounds.
commit()  : void
computeChildrenReorder()  : Result
Computes the bound shifts of child subtrees for a new order; a pure function ({@see reorderTreeChildren()}) testable without a database ({@see \Tests\Unit\Api\TreeReorderChildrenTest}).
computeTotalCount()  : int
Counts the total number of records with applied filters.
degenerateTreeNodeResult()  : Result|null
Guard contract for a degenerate node: a valid nested-set node has width `W = rgt - lft + 1 >= 2` (leaf = `lft`, `rgt = lft + 1`). Anything smaller (typically `lft = rgt = 0` on a node whose bounds were never computed) is a corrupted node, and a tree operation on it must fail, not "repair" the tree by shifting foreign intervals.
delete()  : Result
deleteRelations()  : Result
Deletes rows from a relation table by parent FK column.
findAncestorOrSelfIds()  : array<string|int, int>
Returns the region itself and all its ancestors in the nested set (region id plus all parent ids).
findHrefCollisionRows()  : array<int, array{id: int, languageId: int, level: int, href: string, base: string}>
Returns the region text rows (all languages) whose href or its "base" (href without the own suffix `-{official_code}` / `-{region_id}`) is among `$values`: the collision set for {@see RegionHrefBuilder}. The whole table is checked; the builder drops rows of its own subtree by id. `href` has no index, so every batch is one table scan, hence large batches and all languages in one query.
findHrefSubtree()  : array{ancestors: list}>, subtree: list}>}
Returns the basis for regenerating hrefs ({@see RegionHrefBuilder}): the subtree root plus descendants with texts (ordered by `lft`, parent before child) and the root's ancestors with texts.
findLocality()  : array{node: array, names: list>, postcodes: list}|null
Returns one node for selection validation and address splitting: type, level, activity, whether it is a leaf, the name and country name in all languages (the caller picks a language with a fallback), the main postcode and all postcodes of the municipality.
findSuggestByName()  : array<int, array<string, mixed>>
Suggest, name branch: exact match, then prefix, then word start; inside a group the preferred country, `suggest_weight`, deeper level (municipality before district and region), name.
findSuggestByPlace()  : array<int, array<string, mixed>>
Suggest, municipality parts (`region_postcodes.place_name`, for example a Brno part under 602 00). Complements the name branch; places named like the municipality are skipped (the name branch found those). Of several matching places for one (postcode, municipality) pair the shortest goes into the label.
findSuggestByPostcode()  : array<int, array<string, mixed>>
Suggest, postcode branch: one item per (postcode, municipality) pair. `289 12` belongs to several municipalities, so several items; Brno `602 00` has ten rows (municipality parts), so one item.
get()  : BaseEntity|null
Finds a single entity by ID with dynamic includes.
getAll()  : array{0: array, 1: int}
getBy()  : BaseEntity|null
Finds a single entity by filters with dynamic includes.
getEntityClass()  : class-string
Returns the entity class name.
getTransactionLevel()  : int
getTreeSubtree()  : Result
Loads the subtree of a node for deletion: ids of all nodes (including the subtree root) plus the `lft`/`rgt` bounds for closing the gap afterwards ({@see closeTreeGap()}).
insertRelations()  : Result
Inserts rows into a relation table.
insertTreeNode()  : Result
Computes `lft`/`rgt`/`level` of a newly inserted node (after `save()`, when the DB already has its `parent_id` but `lft`/`rgt` are zero). It is inserted as the last sibling:
inTreeLock()  : Result
Runs the operation under the advisory tree lock (table plus scope) and releases it in `finally`.
isInTransaction()  : bool
moveSubtree()  : Result
Moves the subtree of a node under another parent. Processes the whole subtree (not only the root), updating `lft`/`rgt`/`level` of every subtree node and `parent_id` of the root.
moveTreeNode()  : Result
Swaps a node with its adjacent sibling (move up/down). Shifts the whole subtrees of both siblings; each node keeps its width `rgt - lft`.
reorderTreeChildren()  : Result
Reorders the direct children of a parent into the given order (whole subtrees) in one operation under the tree lock with one UPDATE limited to the parent interval.
rollback()  : void
save()  : Result
selectRelationIds()  : array<int, int>|null
Returns ids of target records linked to the parent in a relation table (for example gallery asset ids).
setTransactionManager()  : void
setTreeLockTimeout()  : void
Changes the tree lock timeout; for concurrency tests and the CLI, not for normal operation.
updateColumns()  : Result
Updates a table using pre-prepared data from a mapper.
updateHrefs()  : int
Writes changed hrefs in bulk (existing text rows only, in batches).
updateRelations()  : Result
Updates rows in a relation table.
applyFilters()  : void
applyIdentifierFilter()  : void
applyIncludes()  : array<int, string>
Applies includes as LEFT JOINs for hydration.
applyOrdering()  : void
dumpDbError()  : void
entityHasMethod()  : bool
Checks whether the entity class has the given method (association).
fetchCompleteEntities()  : array<string|int, mixed>
Second phase: pure hydration.
fetchIdentifiers()  : array<int, array<string|int, mixed>>
fetchSimple()  : array<string|int, mixed>
getAssociationFromMethod()  : string
Derives the association name from a getter method.
getClassMetadata()  : ClassMetadata<string|int, object>
Returns the Doctrine metadata for the entity.
getDefaultIncludes()  : array<string|int, string>
Default includes for this entity.
getEntityManager()  : EntityManagerInterface
Returns the EntityManager.
getJoinedAliases()  : array<string, true>
Returns all aliases already joined in the QueryBuilder.
getPrimaryKey()  : string
Returns the entity primary key.
getPrimaryKeyField()  : string
Returns the primary key field name.
getRelationField()  : string|null
Returns the field name of the relation to the target entity.
getTableAlias()  : string|null
Reads the table alias from the `TableAlias` attribute, escaped against reserved keywords.
hydrateByIdentifiers()  : array<int, mixed>
Loads entities by PK and hydrates includes while avoiding the cartesian explosion caused by parallel ToMany joins.
isJoined()  : bool
Checks whether the alias is already joined in the QueryBuilder.
mergeWithDefaults()  : array<string|int, string>
Merges explicit includes with the defaults.
sortResultsByIdentifiers()  : array<int, mixed>
buildDistinctIdentityExpression()  : string
Returns the expression whose distinct values are the distinct rows of the root entity.
buildScopeCondition()  : array{0: string, 1: array}
Turns the scope filter into an SQL fragment (`AND col <=> :scope_col`) plus parameters.
checkMoveExpectation()  : Result|null
Verifies the move contract against the current tree state; called under the lock with locking reads, so the verified state is the state the write is computed from.
closeTreeGapUnderLock()  : Result
extractIdsFromScalarResult()  : array<int, int|string|array<string, mixed>>
Converts the scalar result of `fetchIdentifiers()` (`{alias}_{field}` keys) to the format the hydrator expects (scalar for a single PK, `[field => value]` for a composite one).
fetchExpectedSiblingLft()  : int|null
Returns `lft` of the expected sibling if it is a child of `$parentId` today; otherwise `null` (deleted, moved under another parent, outside the scope).
fetchNodeLevel()  : int
Returns the level of a node by id (helper for {@see insertTreeNode()}).
hydrateHrefNodes()  : array<int, array{id: int, parentId: ?int, type: ?string, officialCode: ?string, texts: array}>
identityProjectionAlias()  : string
Alias of an `IDENTITY()` projection of an association identifier. Deliberately outside the `{alias}_{relation}` namespace JoinHelper uses for JOIN aliases.
insertTreeNodeUnderLock()  : Result
localityCoordinateSql()  : string
Coordinates of node `r`, otherwise the mean of the municipalities in the subtree (0 means not filled). The column name is a constant from the caller, not input.
moveSubtreeUnderLock()  : Result
moveTreeNodeUnderLock()  : Result
quoteIdentifier()  : string
rawSqlParam()  : mixed
Returns the value for a bare SQL parameter (no DBAL type).
releaseTreeLock()  : void
Releases one level of the tree lock. A failure is only logged: a failed release must not overwrite the operation result; the connection lock disappears with the connection anyway.
reorderTreeChildrenUnderLock()  : Result
siblingWindow()  : array{0: string, 1: array}|Result
Returns the SQL window between the expected neighbors (`lft` of the previous and the next one), or `TREE_STALE` when they do not follow each other. `null` means no neighbor on that side (start/end of the parent).
suggestContextJoins()  : string
Joins the parent, grandparent (from the region level down) and country of the item with their texts. The country is matched through `country_code` (index `idx_country_type_*`), not through the nested-set bounds: that `JOIN` was "range checked for each record" and doubled the query time.
suggestLeafCondition()  : string
Advert form: only a tree leaf of the municipality type. The type value is a constant, not input.
suggestParams()  : array{lang: int, minLevel: int, prefer: int}
toInt()  : int
toString()  : string

Constants

HREF_BATCH_SIZE

Number of rows in one bulk href `UPDATE`.

private int HREF_BATCH_SIZE = 1000

HREF_LOOKUP_BATCH_SIZE

Number of values in one `IN (...)` collision query (every batch is a table scan).

private int HREF_LOOKUP_BATCH_SIZE = 10000

REGION_POSTCODES_TABLE

private string REGION_POSTCODES_TABLE = 'cms_system_region_postcodes'

REGION_TEXTS_TABLE

private string REGION_TEXTS_TABLE = 'cms_system_region_texts'

TYPE_COUNTRY

Country node type (level 1); matched through the index `(country_code, type, ...)`.

private string TYPE_COUNTRY = 'country'

TYPE_MUNICIPALITY

Node type an advert may be attached to (a tree leaf).

private string TYPE_MUNICIPALITY = 'municipality'

Properties

$entityManager

protected EntityManagerInterface $entityManager

$treeLockTimeout

Timeout of `GET_LOCK` in seconds. A move on about 10 000 nodes takes tens of ms, so 10 s covers a queue of several operations; beyond it the result is `TREE_LOCKED`.

private int $treeLockTimeout = 10

Methods

buildTreeLockName()

Returns the advisory tree lock name: `tree:<table truncated to 26 chars>:<md5(table + scope)>`.

public static buildTreeLockName(string $table, array<string, scalar|null> $scope) : string

The hash also carries the whole table name, so truncating the readable part (the lock name limit is 64 chars) cannot merge two trees. The scope is normalized (key order, values as strings) so that ['menu_id' => 5] and ['menu_id' => '5'] from an HTTP payload lock the same tree.

Parameters
$table : string
$scope : array<string, scalar|null>
Return values
string

closeTreeGap()

Closes the numbering gap after a deleted subtree: all bounds after the right bound of the deleted node shift left by its width. Ancestors (lft before the gap) only lose `rgt`, nodes after the gap lose both bounds.

public closeTreeGap(string $table, array{left: string, right: string} $columns, int $lft, int $rgt[, array<string, scalar|null> $scope = [] ]) : Result
Parameters
$table : string

table name

$columns : array{left: string, right: string}

column names

$lft : int

left bound of the deleted subtree

$rgt : int

right bound of the deleted subtree

$scope : array<string, scalar|null> = []

column => value (scope filter)

Return values
Result

computeChildrenReorder()

Computes the bound shifts of child subtrees for a new order; a pure function ({@see reorderTreeChildren()}) testable without a database ({@see \Tests\Unit\Api\TreeReorderChildrenTest}).

public static computeChildrenReorder(array<int, array{id: int, lft: int, rgt: int}> $children, array<int, int> $orderedIds, int $firstLft) : Result

The children must fill the interval contiguously (first.lft = $firstLft, each next one starts after the previous). A child's new place is $firstLft plus the sum of widths of the children placed before it in the new order; the shift of its whole subtree is new lft minus old lft.

Parameters
$children : array<int, array{id: int, lft: int, rgt: int}>

children in the current lft order

$orderedIds : array<int, int>

child ids in the new order

$firstLft : int
Return values
Result —

data: array{shifts: array<int, array{lft: int, rgt: int, shift: int}>} only children with a non-zero shift; TREE_STALE = the id set does not match, TREE_NODE_CORRUPTED = children are not contiguous or degenerate

computeTotalCount()

Counts the total number of records with applied filters.

public computeTotalCount(array<string|int, mixed> $filters) : int
Parameters
$filters : array<string|int, mixed>
Return values
int

degenerateTreeNodeResult()

Guard contract for a degenerate node: a valid nested-set node has width `W = rgt - lft + 1 >= 2` (leaf = `lft`, `rgt = lft + 1`). Anything smaller (typically `lft = rgt = 0` on a node whose bounds were never computed) is a corrupted node, and a tree operation on it must fail, not "repair" the tree by shifting foreign intervals.

public static degenerateTreeNodeResult(int $id, int $lft, int $rgt) : Result|null

Returns null when the node is fine; otherwise a failed Result (ResultCode::TREE_NODE_CORRUPTED, messages in the results domain).

A pure function without a database, testable from the contract (TreeDegenerateNodeGuardTest).

Parameters
$id : int
$lft : int
$rgt : int
Return values
Result|null

deleteRelations()

Deletes rows from a relation table by parent FK column.

public deleteRelations(string $table, string $column, array<string|int, mixed> $parentIds) : Result
Parameters
$table : string

Table name

$column : string

FK column name

$parentIds : array<string|int, mixed>

Parent IDs to delete

Return values
Result

findAncestorOrSelfIds()

Returns the region itself and all its ancestors in the nested set (region id plus all parent ids).

public findAncestorOrSelfIds(int $regionId) : array<string|int, int>

Used for the Bazaar/Comcat watchdog matching ("the advert's region lies in the subtree of the watchdog's region"). The watchdog fan-out and rel insertion always run over one advert, so the advert's region is a scalar: the module manager receives this constant id set as a parameter instead of a correlated JOIN on cms_system_regions. The match uses the canonical form over both nested-set bounds (n.lft >= a.lft AND n.rgt <= a.rgt).

Parameters
$regionId : int
Return values
array<string|int, int> —

ordered from root to leaf; an empty array means the region does not exist

findHrefCollisionRows()

Returns the region text rows (all languages) whose href or its "base" (href without the own suffix `-{official_code}` / `-{region_id}`) is among `$values`: the collision set for {@see RegionHrefBuilder}. The whole table is checked; the builder drops rows of its own subtree by id. `href` has no index, so every batch is one table scan, hence large batches and all languages in one query.

public findHrefCollisionRows(array<int, string> $values) : array<int, array{id: int, languageId: int, level: int, href: string, base: string}>
Parameters
$values : array<int, string>
Return values
array<int, array{id: int, languageId: int, level: int, href: string, base: string}>

findHrefSubtree()

Returns the basis for regenerating hrefs ({@see RegionHrefBuilder}): the subtree root plus descendants with texts (ordered by `lft`, parent before child) and the root's ancestors with texts.

public findHrefSubtree(int $regionId) : array{ancestors: list}>, subtree: list}>}

The subtree is read through lft/rgt with a locking read (FOR UPDATE): it runs in the move or save transaction and must see the last committed state, not the transaction snapshot (like TreeTrait). A node whose bounds are not yet computed (on insert afterSave runs before insertTreeNode) is its own subtree; the ancestors are then taken through the parent's bounds, which are valid.

Parameters
$regionId : int
Return values
array{ancestors: list}>, subtree: list}>}

findLocality()

Returns one node for selection validation and address splitting: type, level, activity, whether it is a leaf, the name and country name in all languages (the caller picks a language with a fallback), the main postcode and all postcodes of the municipality.

public findLocality(int $regionId) : array{node: array, names: list>, postcodes: list}|null

Coordinates: the node's own center; when missing (currently only the country Poland), the mean of the municipality coordinates in the subtree. The own center takes precedence: for a region or district it is the seat/centroid from GeoNames, whereas the municipality mean pulls toward the densely populated part of the area.

Parameters
$regionId : int
Return values
array{node: array, names: list>, postcodes: list}|null

findSuggestByName()

Suggest, name branch: exact match, then prefix, then word start; inside a group the preferred country, `suggest_weight`, deeper level (municipality before district and region), name.

public findSuggestByName(RegionSuggestQuery $query) : array<int, array<string, mixed>>

The item path is parent and grandparent from the region level down (the country is not part of the path, it goes into country_name).

Parameters
$query : RegionSuggestQuery
Return values
array<int, array<string, mixed>>

findSuggestByPlace()

Suggest, municipality parts (`region_postcodes.place_name`, for example a Brno part under 602 00). Complements the name branch; places named like the municipality are skipped (the name branch found those). Of several matching places for one (postcode, municipality) pair the shortest goes into the label.

public findSuggestByPlace(RegionSuggestQuery $query) : array<int, array<string, mixed>>
Parameters
$query : RegionSuggestQuery
Return values
array<int, array<string, mixed>>

findSuggestByPostcode()

Suggest, postcode branch: one item per (postcode, municipality) pair. `289 12` belongs to several municipalities, so several items; Brno `602 00` has ten rows (municipality parts), so one item.

public findSuggestByPostcode(RegionSuggestQuery $query) : array<int, array<string, mixed>>
Parameters
$query : RegionSuggestQuery
Return values
array<int, array<string, mixed>>

get()

Finds a single entity by ID with dynamic includes.

public get(int $id[, string|array<string|int, mixed> $includes = [] ]) : BaseEntity|null

Delegates to getAll() with a PK filter and limit 1 and shares its two-phase fetch, which avoids the LIMIT+JOIN problem (a join multiplies rows, so LIMIT 1 would cut collection members).

Parameters
$id : int
$includes : string|array<string|int, mixed> = []

e.g. ['translations', 'routes.translations'] or '*' for all

Return values
BaseEntity|null

getAll()

public getAll([array<string|int, mixed>|null $filters = [] ][, array<string|int, mixed>|null $orderBys = null ][, int|null $offset = null ][, int|null $limit = null ][, array<string|int, mixed>|null $columns = null ][, bool $computeTotalCount = false ][, string|array<string|int, mixed> $includes = [] ]) : array{0: array, 1: int}
Parameters
$filters : array<string|int, mixed>|null = []
$orderBys : array<string|int, mixed>|null = null
$offset : int|null = null
$limit : int|null = null
$columns : array<string|int, mixed>|null = null
$computeTotalCount : bool = false
$includes : string|array<string|int, mixed> = []
Return values
array{0: array, 1: int} —

[entities (or $columns rows), total count]

getBy()

Finds a single entity by filters with dynamic includes.

public getBy([array<string|int, mixed>|null $filters = null ][, string|array<string|int, mixed> $includes = [] ]) : BaseEntity|null

Delegates to getAll() with limit 1 and shares its two-phase fetch, which avoids the LIMIT+JOIN problem (a join multiplies rows, so LIMIT 1 would cut collection members).

Parameters
$filters : array<string|int, mixed>|null = null
$includes : string|array<string|int, mixed> = []

e.g. ['translations', 'routes.translations'] or '*' for all

Return values
BaseEntity|null

getEntityClass()

Returns the entity class name.

public getEntityClass() : class-string
Return values
class-string

getTransactionLevel()

public getTransactionLevel() : int
Return values
int

getTreeSubtree()

Loads the subtree of a node for deletion: ids of all nodes (including the subtree root) plus the `lft`/`rgt` bounds for closing the gap afterwards ({@see closeTreeGap()}).

public getTreeSubtree(string $table, array{id: string, left: string, right: string} $columns, int $id[, array<string, scalar|null> $scope = [] ]) : Result

Ids are ordered by rgt ascending: a descendant always has a smaller rgt than its ancestor, so deleting in this order goes from the leaves and does not violate a self-reference FK (parent_id).

IMPORTANT: it takes no lock itself. The bounds must stay valid until closeTreeGap(), so the whole delete is locked by the service (TreeTrait::deleteTreeNode()). The read is locking so that the bounds do not come from a stale transaction snapshot.

Parameters
$table : string

table name

$columns : array{id: string, left: string, right: string}

column names

$id : int
$scope : array<string, scalar|null> = []

column => value (scope filter)

Return values
Result —

data: array{ids: int[], lft: int, rgt: int}

insertRelations()

Inserts rows into a relation table.

public insertRelations(string $table, array<int, array<string, mixed>> $rows) : Result
Parameters
$table : string

Table name

$rows : array<int, array<string, mixed>>

Array of ['column_name' => value] rows

Return values
Result

insertTreeNode()

Computes `lft`/`rgt`/`level` of a newly inserted node (after `save()`, when the DB already has its `parent_id` but `lft`/`rgt` are zero). It is inserted as the last sibling:

public insertTreeNode(string $table, array{id: string, left: string, right: string, parent: string, level: string} $columns, int $id[, array<string, scalar|null> $scope = [] ]) : Result
  • find the largest rgt among siblings (same parent_id) and place the node right after it: lft = maxRgt + 1, rgt = maxRgt + 2,
  • without siblings: under a parent right after its lft (first child), at the root after the last root (max rgt of the whole scope tree),
  • before placing, open a gap of width 2 in the numbering (everything with lft/rgt

    = the new lft shifts by 2) to keep the nested set contiguous,

  • level = parent level + 1 (root = 1).
Parameters
$table : string

table name

$columns : array{id: string, left: string, right: string, parent: string, level: string}

column names

$id : int
$scope : array<string, scalar|null> = []

column => value (scope filter)

Return values
Result

inTreeLock()

Runs the operation under the advisory tree lock (table plus scope) and releases it in `finally`.

public inTreeLock(string $table, array<string, scalar|null> $scope, callable(): Result $operation) : Result

Re-entrance: MariaDB (since 10.0.2) holds GET_LOCK recursively with a per-connection counter, so a repeated GET_LOCK of the same name on the same connection returns 1 immediately, and another connection gets the lock only after the same number of RELEASE_LOCK calls (checked by tests/Manual/System/tree_move_subtree_harness.php). A nested call (manager -> repository, service deleteTreeNode() -> closeTreeGap()) therefore needs no PHP counter, acquire and release only have to be paired strictly. A PHP counter would add state outside the connection (after a reconnect it would "hold" a lock the server already dropped).

A nested operation releases its level at its own end even while the outer transaction still runs (typically save() -> insertTreeNode()). Consistency until the commit is held by the locking read (see the trait docblock); standalone manager operations hold the lock across the commit (TreeTrait::treeTransaction()).

Parameters
$table : string
$scope : array<string, scalar|null>

column => value

$operation : callable(): Result
Return values
Result

moveSubtree()

Moves the subtree of a node under another parent. Processes the whole subtree (not only the root), updating `lft`/`rgt`/`level` of every subtree node and `parent_id` of the root.

public moveSubtree(string $table, array{id: string, left: string, right: string, parent: string, level: string} $columns, int $id, int|null $newParentId[, array<string, scalar|null> $scope = [] ][, int|null $afterSiblingId = null ][, TreeMoveExpectation|null $expectation = null ]) : Result

Algorithm (temporary offset):

  1. extract the subtree (lft/rgt += BIG),
  2. close the gap at the original position,
  3. open space at the new parent,
  4. place the subtree (shift back + level diff).

Must run in a transaction (held by the manager).

With $expectation the repository verifies under the lock, before the first write, that the node is still under the expected parent and that prev/next are today's neighbors under newParentId; on mismatch it returns TREE_STALE and writes nothing. Without it the move runs unchecked.

Parameters
$table : string

table name

$columns : array{id: string, left: string, right: string, parent: string, level: string}

column names

$id : int
$newParentId : int|null

null moves to the root level (after the other roots)

$scope : array<string, scalar|null> = []

column => value

$afterSiblingId : int|null = null

null = as last child, 0 = as first child, >0 = after that sibling

$expectation : TreeMoveExpectation|null = null
Return values
Result

moveTreeNode()

Swaps a node with its adjacent sibling (move up/down). Shifts the whole subtrees of both siblings; each node keeps its width `rgt - lft`.

public moveTreeNode(string $table, array{id: string, left: string, right: string, parent: string} $columns, int $id, int $direction[, array<string, scalar|null> $scope = [] ]) : Result
Parameters
$table : string

table name

$columns : array{id: string, left: string, right: string, parent: string}

column names

$id : int
$direction : int

-1 = up (before the previous sibling), +1 = down

$scope : array<string, scalar|null> = []

column => value (scope filter)

Return values
Result

reorderTreeChildren()

Reorders the direct children of a parent into the given order (whole subtrees) in one operation under the tree lock with one UPDATE limited to the parent interval.

public reorderTreeChildren(string $table, array{id: string, left: string, right: string, parent: string} $columns, int|null $parentId, array<int, int> $orderedIds[, array<string, scalar|null> $scope = [] ]) : Result

The parent width does not change (children only swap places), so ancestors, siblings of the parent and the rest of the tree are untouched; level and parent_id do not change either. It replaces N calls of moveSubtree() (for 200 municipalities of a district that would be 200 moves with five UPDATEs each across the whole table).

Under the lock it verifies that the set of children of the parent is exactly $orderedIds (otherwise TREE_STALE: a child was added, removed or moved in the meantime) and that the children fill the parent interval contiguously (otherwise TREE_NODE_CORRUPTED: a broken tree is not reordered). When the order does not change nothing is written. The result carries data.changed (number of children that changed place).

Parameters
$table : string
$columns : array{id: string, left: string, right: string, parent: string}

column names

$parentId : int|null

null is the root level of the scope tree

$orderedIds : array<int, int>

child ids in the new order

$scope : array<string, scalar|null> = []

column => value (scope filter)

Return values
Result

save()

public save(T $entity[, array<string|int, string> $skipFields = [] ]) : Result
Parameters
$entity : T
$skipFields : array<string|int, string> = []

Field names to skip during UPDATE (e.g. ['userRoles'])

Return values
Result

selectRelationIds()

Returns ids of target records linked to the parent in a relation table (for example gallery asset ids).

public selectRelationIds(string $table, string $parentColumn, int $parentId, string $targetColumn) : array<int, int>|null

Soft-removed rows are read too, as they still belong to the same parent. On a database error it returns null; the caller decides, and a foreign id must then not be accepted.

Parameters
$table : string
$parentColumn : string
$parentId : int
$targetColumn : string
Return values
array<int, int>|null

setTreeLockTimeout()

Changes the tree lock timeout; for concurrency tests and the CLI, not for normal operation.

public setTreeLockTimeout(int $seconds) : void
Parameters
$seconds : int

updateColumns()

Updates a table using pre-prepared data from a mapper.

public updateColumns(array<string, mixed> $set, array<int, array<string, array{column: string, value: mixed}>> $where, string $table) : Result
Parameters
$set : array<string, mixed>

['column_name' => value|SqlValue]

$where : array<int, array<string, array{column: string, value: mixed}>>

[[ 'paramName' => ['column'=>col, 'value'=>val], ... ], ... ]

$table : string

table name

Return values
Result

updateHrefs()

Writes changed hrefs in bulk (existing text rows only, in batches).

public updateHrefs(array<int, array{id: int, languageId: int, href: string}> $changes) : int
Parameters
$changes : array<int, array{id: int, languageId: int, href: string}>
Return values
int —

count of written rows

updateRelations()

Updates rows in a relation table.

public updateRelations(string $table, array<int, array{set: array, where: array}> $updates) : Result
Parameters
$table : string

Table name

$updates : array<int, array{set: array, where: array}>

Array of ['set' => [col => val], 'where' => [col => val]]

Return values
Result

applyFilters()

protected applyFilters(QueryBuilder $qb, array<string|int, mixed>|null $filters, class-string $entityClass, string $fromTableAlias) : void
Parameters
$qb : QueryBuilder
$filters : array<string|int, mixed>|null
$entityClass : class-string
$fromTableAlias : string

applyIdentifierFilter()

protected applyIdentifierFilter(QueryBuilder $qb, array<int, array<string|int, mixed>> $identifiers, array<int, string> $identifierFields, string $fromTableAlias) : void
Parameters
$qb : QueryBuilder
$identifiers : array<int, array<string|int, mixed>>
$identifierFields : array<int, string>
$fromTableAlias : string

applyIncludes()

Applies includes as LEFT JOINs for hydration.

protected applyIncludes(QueryBuilder $qb, array<string|int, mixed> $includes, string $alias[, array<string|int, mixed> &$joinedAliases = [] ][, class-string|null $entityClass = null ]) : array<int, string>

Hydration only: no WHERE and no WITH conditions. Filters are applied in the first phase (fetchIdentifiers()) over ids; the second phase only loads the full collections for the selected entities.

Parameters
$qb : QueryBuilder

Query builder

$includes : array<string|int, mixed>

Includes, e.g. ['translations', 'routes.translations']

$alias : string

Alias of the root entity

$joinedAliases : array<string|int, mixed> = []

Reference tracking already-joined aliases

$entityClass : class-string|null = null

Root entity for resolving includes (default: root repo entity)

Return values
array<int, string> —

Aliases for the SELECT

applyOrdering()

protected applyOrdering(QueryBuilder $qb, array<string|int, mixed>|null $orderBys, string $fromTableAlias) : void
Parameters
$qb : QueryBuilder
$orderBys : array<string|int, mixed>|null
$fromTableAlias : string

dumpDbError()

protected dumpDbError(Throwable $e) : void
Parameters
$e : Throwable

entityHasMethod()

Checks whether the entity class has the given method (association).

protected entityHasMethod(string $entityClass, string $method) : bool
Parameters
$entityClass : string
$method : string

Method name, e.g. 'getTranslations'

Return values
bool

fetchCompleteEntities()

Second phase: pure hydration.

protected fetchCompleteEntities(class-string $entityClass, string $fromTableAlias, array<int, array<string|int, mixed>> $identifiers, array<string|int, mixed>|null $columns, array<string|int, mixed> $includes[, array<string|int, mixed>|null $filters = null ]) : array<string|int, mixed>

Delegates to the shared hydrateByIdentifiers() of OrmRepository, which loads to-many branches separately via WHERE parent_id IN (:ids) (no cartesian explosion from parallel to-many joins).

Parameters
$entityClass : class-string
$fromTableAlias : string
$identifiers : array<int, array<string|int, mixed>>
$columns : array<string|int, mixed>|null
$includes : array<string|int, mixed>
$filters : array<string|int, mixed>|null = null
Return values
array<string|int, mixed>

fetchIdentifiers()

protected fetchIdentifiers(class-string $entityClass, string $fromTableAlias, array<string|int, mixed>|null $filters, array<string|int, mixed>|null $orderBys, int|null $offset, int|null $limit) : array<int, array<string|int, mixed>>
Parameters
$entityClass : class-string
$fromTableAlias : string
$filters : array<string|int, mixed>|null
$orderBys : array<string|int, mixed>|null
$offset : int|null
$limit : int|null
Return values
array<int, array<string|int, mixed>> —

scalar rows keyed {alias}_{field}

fetchSimple()

protected fetchSimple(class-string $entityClass, string $fromTableAlias, array<string|int, mixed>|null $filters, array<string|int, mixed>|null $orderBys, int|null $offset, int|null $limit, array<string|int, mixed>|null $columns) : array<string|int, mixed>
Parameters
$entityClass : class-string
$fromTableAlias : string
$filters : array<string|int, mixed>|null
$orderBys : array<string|int, mixed>|null
$offset : int|null
$limit : int|null
$columns : array<string|int, mixed>|null
Return values
array<string|int, mixed>

getAssociationFromMethod()

Derives the association name from a getter method.

protected getAssociationFromMethod(string $method) : string

E.g. 'getTranslations' → 'translations'.

Parameters
$method : string
Return values
string

getClassMetadata()

Returns the Doctrine metadata for the entity.

protected getClassMetadata() : ClassMetadata<string|int, object>
Return values
ClassMetadata<string|int, object>

getDefaultIncludes()

Default includes for this entity.

protected getDefaultIncludes() : array<string|int, string>

Override in a child, or place the DefaultIncludes attribute on the entity.

Return values
array<string|int, string>

getEntityManager()

Returns the EntityManager.

protected getEntityManager() : EntityManagerInterface
Return values
EntityManagerInterface

getJoinedAliases()

Returns all aliases already joined in the QueryBuilder.

protected getJoinedAliases(QueryBuilder $qb) : array<string, true>
Parameters
$qb : QueryBuilder
Return values
array<string, true>

getPrimaryKey()

Returns the entity primary key.

protected getPrimaryKey() : string
Return values
string

getPrimaryKeyField()

Returns the primary key field name.

protected getPrimaryKeyField() : string
Return values
string

getRelationField()

Returns the field name of the relation to the target entity.

protected getRelationField(class-string $entityClass, string $targetEntity) : string|null
Parameters
$entityClass : class-string
$targetEntity : string
Return values
string|null

getTableAlias()

Reads the table alias from the `TableAlias` attribute, escaped against reserved keywords.

protected getTableAlias(class-string $entityClass) : string|null

The attribute value is used verbatim as the QueryBuilder identification variable and FROM alias (e.g. ->createQueryBuilder('order')), and the query lexer treats a bare reserved word as a keyword token instead of an identifier, which breaks the parser. Escaping here, the single choke point that find() and getAll() funnel through, protects every present and future entity without hand-picking alias strings per #[TableAlias] declaration.

Parameters
$entityClass : class-string
Return values
string|null

hydrateByIdentifiers()

Loads entities by PK and hydrates includes while avoiding the cartesian explosion caused by parallel ToMany joins.

protected hydrateByIdentifiers(class-string $entityClass, string $fromTableAlias, array<string|int, int|string|array<string, mixed>> $ids, array<string|int, mixed> $includes[, array<string|int, mixed>|null $columns = null ][, array<string|int, mixed>|null $filters = null ]) : array<int, mixed>

The root query holds only ToOne chains (no DISTINCT / temp table). Every ToMany branch is loaded separately via WHERE parent_id IN (:ids) and the collections are then stitched back onto the parent entities in PHP.

Filters (if passed) propagate into the ToMany branches only when they opt in with BaseFilter::setApplyToCollections(true): such a filter with the dot-path A.B.C is applied to every branch whose path is a prefix of it (with matching trimming). Other filters, and those with isPhaseOneOnly() === true, stay purely in phase 1, so included collections are loaded complete. See filtersForBranch().

Parameters
$entityClass : class-string

Root entity

$fromTableAlias : string

alias of the root entity in the query

$ids : array<string|int, int|string|array<string, mixed>>

PK list: either scalar (single-column PK), or [field => value] (composite)

$includes : array<string|int, mixed>

Dot-notation includes, e.g. ['routes.translations', 'parameterRels.parameter']

$columns : array<string|int, mixed>|null = null

Column restriction on the root entity

$filters : array<string|int, mixed>|null = null

Phase 1 filters (for propagation into ToMany branches)

Return values
array<int, mixed> —

entities (or column rows when $columns is given)

isJoined()

Checks whether the alias is already joined in the QueryBuilder.

protected isJoined(QueryBuilder $qb, string $alias) : bool
Parameters
$qb : QueryBuilder
$alias : string
Return values
bool

mergeWithDefaults()

Merges explicit includes with the defaults.

protected mergeWithDefaults(string|array<string|int, string> $includes) : array<string|int, string>

Passing '*' loads every association of the entity.

Parameters
$includes : string|array<string|int, string>

Explicit includes, or '*' for all

Return values
array<string|int, string> —

Merged includes

sortResultsByIdentifiers()

protected sortResultsByIdentifiers(array<string|int, mixed> $results, array<int, array<string|int, mixed>> $identifiers, class-string $entityClass, string $fromTableAlias) : array<int, mixed>
Parameters
$results : array<string|int, mixed>
$identifiers : array<int, array<string|int, mixed>>
$entityClass : class-string
$fromTableAlias : string
Return values
array<int, mixed>

buildDistinctIdentityExpression()

Returns the expression whose distinct values are the distinct rows of the root entity.

private buildDistinctIdentityExpression(ClassMetadata<string|int, object> $metadata, string $alias) : string

IMPORTANT: every identifier component must be counted. Counting only the first one makes a compound identity (CategoryText = languageId + parent) return the number of distinct languages instead of rows. DQL has no multi-column COUNT(DISTINCT), so the components are joined with a separator; an association component goes through IDENTITY() (its FK), a scalar one directly. CONCAT is portable (the SQLite platform renders ||), and the separator cannot occur inside integer keys.

Parameters
$metadata : ClassMetadata<string|int, object>
$alias : string
Return values
string

buildScopeCondition()

Turns the scope filter into an SQL fragment (`AND col <=> :scope_col`) plus parameters.

private buildScopeCondition(array<string, scalar|null> $scope) : array{0: string, 1: array}
Parameters
$scope : array<string, scalar|null>
Return values
array{0: string, 1: array}

checkMoveExpectation()

Verifies the move contract against the current tree state; called under the lock with locking reads, so the verified state is the state the write is computed from.

private checkMoveExpectation(Connection $conn, string $t, string $idCol, string $leftCol, string $parentCol, string $scopeSql, array<string, scalar|null> $scopeParams, int $id, int|null $currentParentId, int|null $newParentId, TreeMoveExpectation $expectation) : Result|null

Adjacency is verified by sibling ORDER (no sibling under newParentId with lft between prev and next), not by the arithmetic prev.rgt + 1 == next.lft: it is just as strict on a healthy tree, the moved node excludes itself (id <> :drag, also for a move within the same parent), and the edges need no special case for the root level (start = no sibling before next, end = no sibling after prev).

Returns null when the contract holds; otherwise INVALID_ARGUMENT (nonsensical input combination) or TREE_STALE (state changed or the client saw an incomplete list).

Parameters
$conn : Connection
$t : string
$idCol : string
$leftCol : string
$parentCol : string
$scopeSql : string
$scopeParams : array<string, scalar|null>
$id : int
$currentParentId : int|null
$newParentId : int|null
$expectation : TreeMoveExpectation
Return values
Result|null

closeTreeGapUnderLock()

private closeTreeGapUnderLock(string $table, array{left: string, right: string} $columns, int $lft, int $rgt, array<string, scalar|null> $scope) : Result
Parameters
$table : string
$columns : array{left: string, right: string}
$lft : int
$rgt : int
$scope : array<string, scalar|null>
Return values
Result

extractIdsFromScalarResult()

Converts the scalar result of `fetchIdentifiers()` (`{alias}_{field}` keys) to the format the hydrator expects (scalar for a single PK, `[field => value]` for a composite one).

private extractIdsFromScalarResult(array<int, array<string|int, mixed>> $identifiers, array<int, string> $identifierFields, string $fromTableAlias) : array<int, int|string|array<string, mixed>>
Parameters
$identifiers : array<int, array<string|int, mixed>>
$identifierFields : array<int, string>
$fromTableAlias : string
Return values
array<int, int|string|array<string, mixed>>

fetchExpectedSiblingLft()

Returns `lft` of the expected sibling if it is a child of `$parentId` today; otherwise `null` (deleted, moved under another parent, outside the scope).

private fetchExpectedSiblingLft(Connection $conn, string $t, string $idCol, string $leftCol, string $parentCol, string $scopeSql, array<string, scalar|null> $scopeParams, int $siblingId, int|null $parentId) : int|null
Parameters
$conn : Connection
$t : string
$idCol : string
$leftCol : string
$parentCol : string
$scopeSql : string
$scopeParams : array<string, scalar|null>
$siblingId : int
$parentId : int|null
Return values
int|null

fetchNodeLevel()

Returns the level of a node by id (helper for {@see insertTreeNode()}).

private fetchNodeLevel(Connection $conn, string $table, string $levelCol, string $idCol, string $scopeSql, array<string, scalar|null> $scopeParams, int $id) : int
Parameters
$conn : Connection
$table : string
$levelCol : string
$idCol : string
$scopeSql : string
$scopeParams : array<string, scalar|null>
$id : int
Return values
int

hydrateHrefNodes()

private hydrateHrefNodes(array<int, array<string, mixed>> $rows) : array<int, array{id: int, parentId: ?int, type: ?string, officialCode: ?string, texts: array}>
Parameters
$rows : array<int, array<string, mixed>>

region x text rows (ordered)

Return values
array<int, array{id: int, parentId: ?int, type: ?string, officialCode: ?string, texts: array}>

identityProjectionAlias()

Alias of an `IDENTITY()` projection of an association identifier. Deliberately outside the `{alias}_{relation}` namespace JoinHelper uses for JOIN aliases.

private static identityProjectionAlias(string $fromTableAlias, string $idField) : string
Parameters
$fromTableAlias : string
$idField : string
Return values
string

insertTreeNodeUnderLock()

private insertTreeNodeUnderLock(string $table, array{id: string, left: string, right: string, parent: string, level: string} $columns, int $id, array<string, scalar|null> $scope) : Result
Parameters
$table : string
$columns : array{id: string, left: string, right: string, parent: string, level: string}
$id : int
$scope : array<string, scalar|null>
Return values
Result

localityCoordinateSql()

Coordinates of node `r`, otherwise the mean of the municipalities in the subtree (0 means not filled). The column name is a constant from the caller, not input.

private localityCoordinateSql(string $column) : string
Parameters
$column : string
Return values
string

moveSubtreeUnderLock()

private moveSubtreeUnderLock(string $table, array{id: string, left: string, right: string, parent: string, level: string} $columns, int $id, int|null $newParentId, array<string, scalar|null> $scope, int|null $afterSiblingId, TreeMoveExpectation|null $expectation) : Result
Parameters
$table : string
$columns : array{id: string, left: string, right: string, parent: string, level: string}
$id : int
$newParentId : int|null
$scope : array<string, scalar|null>
$afterSiblingId : int|null
$expectation : TreeMoveExpectation|null
Return values
Result

moveTreeNodeUnderLock()

private moveTreeNodeUnderLock(string $table, array{id: string, left: string, right: string, parent: string} $columns, int $id, int $direction, array<string, scalar|null> $scope) : Result
Parameters
$table : string
$columns : array{id: string, left: string, right: string, parent: string}
$id : int
$direction : int
$scope : array<string, scalar|null>
Return values
Result

quoteIdentifier()

private quoteIdentifier(string $identifier) : string
Parameters
$identifier : string
Return values
string

rawSqlParam()

Returns the value for a bare SQL parameter (no DBAL type).

private rawSqlParam(mixed $value) : mixed

The mysqli driver would send false as '' and cannot bind a DateTime at all; a database in strict mode then rejects the write ("Incorrect integer value"), while a non-strict one silently converts it to 0.

Parameters
$value : mixed

releaseTreeLock()

Releases one level of the tree lock. A failure is only logged: a failed release must not overwrite the operation result; the connection lock disappears with the connection anyway.

private releaseTreeLock(string $name) : void
Parameters
$name : string

reorderTreeChildrenUnderLock()

private reorderTreeChildrenUnderLock(string $table, array{id: string, left: string, right: string, parent: string} $columns, int|null $parentId, array<int, int> $orderedIds, array<string, scalar|null> $scope) : Result
Parameters
$table : string
$columns : array{id: string, left: string, right: string, parent: string}
$parentId : int|null
$orderedIds : array<int, int>
$scope : array<string, scalar|null>
Return values
Result

siblingWindow()

Returns the SQL window between the expected neighbors (`lft` of the previous and the next one), or `TREE_STALE` when they do not follow each other. `null` means no neighbor on that side (start/end of the parent).

private siblingWindow(string $leftCol, int|null $prevLft, int|null $nextLft, int $prevId, int $nextId) : array{0: string, 1: array}|Result
Parameters
$leftCol : string
$prevLft : int|null
$nextLft : int|null
$prevId : int
$nextId : int
Return values
array{0: string, 1: array}|Result

suggestContextJoins()

Joins the parent, grandparent (from the region level down) and country of the item with their texts. The country is matched through `country_code` (index `idx_country_type_*`), not through the nested-set bounds: that `JOIN` was "range checked for each record" and doubled the query time.

private suggestContextJoins() : string
Return values
string

toInt()

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

toString()

private static toString(mixed $value) : string
Parameters
$value : mixed
Return values
string
On this page

Search results