CoolCMS API

CategoryRepository extends OrmRepository
in package
uses TreeTrait

Abstract ORM repository with dynamic-includes support.

Every concrete repository extends this class.

Table of Contents

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.
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.
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()}).
identityProjectionAlias()  : string
Alias of an `IDENTITY()` projection of an association identifier. Deliberately outside the `{alias}_{relation}` namespace JoinHelper uses for JOIN aliases.
insertTreeNodeUnderLock()  : Result
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).

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

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

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

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

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
On this page

Search results