MessageRepository
extends OrmRepository
in package
uses
TreeTrait
Message storage with raw DBAL queries.
The raw DBAL methods are intentional (AdvertHardDeleteRepositoryTrait):
they read and write inside an open delete/move/save transaction, and every Api ORM read calls
UnitOfWork::clear(), which would break the pending flush.
Table of Contents
Constants
- TABLE : string = 'cms_mod_discussion_messages'
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.
-
findDeleteDependents()
: array{messageIds: list
, commentThreadIds: list , ratingThreadIds: list , assetIds: list } - Finds data that depends on the messages, including the whole reply subtree (nested set `lft..rgt`), which the DB cascade does not reach.
- 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`.
- recomputeFirstParentIds() : int
- Recomputes `first_parent_id` of the whole subtree of `$id` after a tree move: a level-1 root gets `NULL`, other nodes the id of their level-1 ancestor (nested set). Returns the number of changed rows.
- 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.
- resolveThreadRootId() : int|null
- Returns the thread root (`first_parent_id`) for a message with parent `$parentId`: the nearest level-1 ancestor by the parent's nested set (the parent itself when it is a root). For a parent with an uncomputed tree it falls back to its `first_parent_id`, else to the parent itself.
- 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).
Constants
TABLE
private
string
TABLE
= 'cms_mod_discussion_messages'
Properties
$db
protected
EntityManagerInterface
$db
$entityManager
protected
EntityManagerInterface
$entityManager
$entityUpdater
protected
EntityUpdater
$entityUpdater
$managerProvider
protected
ManagerProvider
$managerProvider
$transactionManager
protected
TransactionManager
$transactionManager
$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
__construct()
public
__construct(ManagerProvider $managerProvider, EntityUpdater $entityUpdater, EntityManagerInterface $entityManager, TransactionManager $transactionManager) : mixed
Parameters
- $managerProvider : ManagerProvider
- $entityUpdater : EntityUpdater
- $entityManager : EntityManagerInterface
- $transactionManager : TransactionManager
beginTransaction()
public
beginTransaction() : void
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
stringcloseTreeGap()
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
Resultcommit()
public
commit() : void
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
lftorder - $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
intdegenerateTreeNodeResult()
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|nulldelete()
public
delete(BaseEntity $entity) : Result
Parameters
- $entity : BaseEntity
Return values
ResultdeleteRelations()
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
ResultfindDeleteDependents()
Finds data that depends on the messages, including the whole reply subtree (nested set `lft..rgt`), which the DB cascade does not reach.
public
findDeleteDependents(array<int, int> $messageIds) : array{messageIds: list, commentThreadIds: list, ratingThreadIds: list, assetIds: list}
The link goes from the message to the comment/rating thread (FK SET NULL, no FK at all for
ratings), so deleting a message orphans its thread; images (cms_system_images) are likewise
orphaned after the ..._message_image_rel cascade.
Parameters
- $messageIds : array<int, int>
Return values
array{messageIds: listget()
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|nullgetAll()
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[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|nullgetEntityClass()
Returns the entity class name.
public
getEntityClass() : class-string
Return values
class-stringgetTransactionLevel()
public
getTransactionLevel() : int
Return values
intgetTreeSubtree()
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
ResultinsertTreeNode()
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
rgtamong siblings (sameparent_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 (maxrgtof the whole scope tree), - before placing, open a gap of width 2 in the numbering (everything with
lft/rgt= the new
lftshifts by 2) to keep the nested set contiguous, level= parentlevel+ 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
ResultinTreeLock()
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
ResultisInTransaction()
public
isInTransaction() : bool
Return values
boolmoveSubtree()
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):
- extract the subtree (lft/rgt +=
BIG), - close the gap at the original position,
- open space at the new parent,
- 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
ResultmoveTreeNode()
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
ResultrecomputeFirstParentIds()
Recomputes `first_parent_id` of the whole subtree of `$id` after a tree move: a level-1 root gets `NULL`, other nodes the id of their level-1 ancestor (nested set). Returns the number of changed rows.
public
recomputeFirstParentIds(int $id) : int
Parameters
- $id : int
Return values
intreorderTreeChildren()
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
ResultresolveThreadRootId()
Returns the thread root (`first_parent_id`) for a message with parent `$parentId`: the nearest level-1 ancestor by the parent's nested set (the parent itself when it is a root). For a parent with an uncomputed tree it falls back to its `first_parent_id`, else to the parent itself.
public
resolveThreadRootId(int $parentId) : int|null
Returns null when the parent does not exist.
Parameters
- $parentId : int
Return values
int|nullrollback()
public
rollback() : void
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
ResultselectRelationIds()
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>|nullsetTransactionManager()
public
setTransactionManager(TransactionManager $transactionManager) : void
Parameters
- $transactionManager : TransactionManager
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
ResultupdateRelations()
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
ResultapplyFilters()
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
boolfetchCompleteEntities()
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
stringgetClassMetadata()
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
EntityManagerInterfacegetJoinedAliases()
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
stringgetPrimaryKeyField()
Returns the primary key field name.
protected
getPrimaryKeyField() : string
Return values
stringgetRelationField()
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|nullgetTableAlias()
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|nullhydrateByIdentifiers()
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
boolmergeWithDefaults()
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
stringbuildScopeCondition()
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: arraycheckMoveExpectation()
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|nullcloseTreeGapUnderLock()
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
ResultextractIdsFromScalarResult()
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|nullfetchNodeLevel()
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
intidentityProjectionAlias()
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
stringinsertTreeNodeUnderLock()
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
ResultmoveSubtreeUnderLock()
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
ResultmoveTreeNodeUnderLock()
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
ResultquoteIdentifier()
private
quoteIdentifier(string $identifier) : string
Parameters
- $identifier : string
Return values
stringrawSqlParam()
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
ResultsiblingWindow()
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