TreeTrait
Public API of nested-set tree operations: moving and deleting a subtree. Wraps them in a transaction and delegates to the service ({@see \App\Core\Traits\Api\Models\Services\TreeTrait}), which handles cache invalidation, and to the mapper/repository, which run the set-based SQL.
Table of Contents
Constants
- META_MOVE_SUBTREE_PARENT : mixed = 'moveSubtreeToParentId'
- Meta key by which an admin/front form says "this entity changed its parent"; the value is the new `parentId` (`null`/0 = root). See {@see applyMetaSubtreeMove()}.
Properties
- $service : Service<string|int, BaseEntity>
Methods
- deleteNode() : Result
- Deletes a node with its whole subtree and closes the gap in lft/rgt.
- insertTreeNode() : Result
- Computes lft/rgt/level for a newly inserted node (after `save()`, when it has `parent_id` in the database but zero lft/rgt). It is inserted as the last sibling (after the largest `rgt` among nodes with the same `parent_id`).
- moveDown() : Result
- Moves after the next sibling.
- moveSubtree() : Result
- Moves a whole subtree (root and descendants) under another parent.
- moveSubtreeTo() : Result
- Moves a whole subtree under a new parent at a specific position.
- moveUp() : Result
- Moves before the previous sibling.
- reorderChildren() : Result
- Reorders the direct children of a parent (with whole subtrees) into the given order, in one operation under the tree lock (the "sort children alphabetically" admin grid action; the caller decides the order). A mismatching set of children returns `TREE_STALE` and nothing is written; `data.changed` is the number of moved children.
- afterMoveSubtree() : Result
- Hook after a subtree move ({@see moveSubtree()}, {@see moveSubtreeTo()}, so also {@see applyMetaSubtreeMove()} from a form). It runs inside the transaction and the tree lock, so a failure returned from here also rolls back the move itself. The default does nothing; a manager whose derived data depends on the tree path (region slugs) overrides it.
- applyMetaSubtreeMove() : Result|null
- Recomputes the nested set after a parent change. Called from `afterSave()`, so inside the save transaction.
- treeConfigForEntity() : TreeConfig
- `TreeConfig` used when `save()` computes lft/rgt of a new entity automatically ({@see SaveTrait}).
- captureSubtreeAssetIds() : array<int, int>
- Returns the ids of images owned by the whole subtree; empty when the manager declares no image cleanup.
- moveTreeNode() : Result
- runTreeTransaction() : Result
- The body of {@see treeTransaction()}: a transaction only when none is running, a tag bump after success, an exception means log plus rollback of the own transaction.
- treeService() : TreeServiceInterface
- Loud guard against silent failure: tree operations run only when the underlying service implements the method (`TreeTrait` on service, mapper and repository). A missing one is a wiring bug, so a {@see \LogicException} is thrown (logged through `treeTransaction()` / `save()`) instead of a silent `INVALID_OPERATION` that did not show where the problem is.
- treeTransaction() : Result
- Wraps a tree operation in a transaction (when the service supports it): the operations rewrite lft/rgt on many rows and a partial write would break the tree.
Constants
META_MOVE_SUBTREE_PARENT
Meta key by which an admin/front form says "this entity changed its parent"; the value is the new `parentId` (`null`/0 = root). See {@see applyMetaSubtreeMove()}.
public
mixed
META_MOVE_SUBTREE_PARENT
= 'moveSubtreeToParentId'
Properties
$service
public
Service<string|int, BaseEntity>
$service
Methods
deleteNode()
Deletes a node with its whole subtree and closes the gap in lft/rgt.
public
deleteNode(int $id[, TreeConfig $config = new TreeConfig() ]) : Result
The DeleteTrait::delete() hooks do not reach here (the service deletes the subtree
through deleteAll()), so a manager declaring ownedAssetIds() gets the image cleanup of
the whole subtree here: ids are captured before the delete (the FK disappears after
DELETE) and released after a successful commit. A shared image stays
(AssetManager::releaseAssetRow()).
Parameters
- $id : int
- $config : TreeConfig = new TreeConfig()
Return values
ResultinsertTreeNode()
Computes lft/rgt/level for a newly inserted node (after `save()`, when it has `parent_id` in the database but zero lft/rgt). It is inserted as the last sibling (after the largest `rgt` among nodes with the same `parent_id`).
public
insertTreeNode(int $id[, TreeConfig $config = new TreeConfig() ]) : Result
Parameters
- $id : int
- $config : TreeConfig = new TreeConfig()
Return values
ResultmoveDown()
Moves after the next sibling.
public
moveDown(int $id[, TreeConfig $config = new TreeConfig() ]) : Result
Parameters
- $id : int
- $config : TreeConfig = new TreeConfig()
Return values
ResultmoveSubtree()
Moves a whole subtree (root and descendants) under another parent.
public
moveSubtree(int $id, int|null $newParentId[, TreeConfig $config = new TreeConfig() ]) : Result
Updates lft/rgt/level of the subtree and parent_id of its root.
Parameters
- $id : int
- $newParentId : int|null
-
null = move to the root level
- $config : TreeConfig = new TreeConfig()
Return values
ResultmoveSubtreeTo()
Moves a whole subtree under a new parent at a specific position.
public
moveSubtreeTo(int $id, int|null $newParentId, int $afterId[, TreeConfig $config = new TreeConfig() ][, TreeMoveExpectation|null $expectation = null ]) : Result
With $expectation (grid drag and drop) the old parent and the prev/next neighbours are
verified under the tree lock; a mismatch returns TREE_STALE and nothing is written. The
position is given by afterId and must equal expectation->prevId, otherwise
INVALID_ARGUMENT (the client would verify a different place than the one it moves to).
Parameters
- $id : int
- $newParentId : int|null
-
null = root level
- $afterId : int
-
-1 = as first child, 0 = as last child, >0 = after the sibling with this id
- $config : TreeConfig = new TreeConfig()
- $expectation : TreeMoveExpectation|null = null
Return values
ResultmoveUp()
Moves before the previous sibling.
public
moveUp(int $id[, TreeConfig $config = new TreeConfig() ]) : Result
Parameters
- $id : int
- $config : TreeConfig = new TreeConfig()
Return values
ResultreorderChildren()
Reorders the direct children of a parent (with whole subtrees) into the given order, in one operation under the tree lock (the "sort children alphabetically" admin grid action; the caller decides the order). A mismatching set of children returns `TREE_STALE` and nothing is written; `data.changed` is the number of moved children.
public
reorderChildren(int|null $parentId, array<int, int> $orderedIds[, TreeConfig $config = new TreeConfig() ]) : Result
Parameters
- $parentId : int|null
-
null = root level
- $orderedIds : array<int, int>
-
ids of all children of the parent in the new order
- $config : TreeConfig = new TreeConfig()
Return values
ResultafterMoveSubtree()
Hook after a subtree move ({@see moveSubtree()}, {@see moveSubtreeTo()}, so also {@see applyMetaSubtreeMove()} from a form). It runs inside the transaction and the tree lock, so a failure returned from here also rolls back the move itself. The default does nothing; a manager whose derived data depends on the tree path (region slugs) overrides it.
protected
afterMoveSubtree(int $id, Result $result) : Result
Reordering children (reorderChildren()) does not change the path and does not call it.
Parameters
- $id : int
- $result : Result
Return values
ResultapplyMetaSubtreeMove()
Recomputes the nested set after a parent change. Called from `afterSave()`, so inside the save transaction.
protected
applyMetaSubtreeMove(bool $isInsert, BaseEntity $entity, int|null $entityId, array<string, mixed> $meta) : Result|null
IMPORTANT: it belongs in afterSave() because a form that moved the subtree as a separate
operation after a successful save() left a new parent_id with unrecomputed lft/rgt
when the move failed: a broken tree that could not be repaired from the UI. From here a
failure is returned as a Result and the shared SaveTrait also rolls back the
parent_id save.
The scope of several trees in one table (menu items) is taken from the entity through treeConfigForEntity(); the caller passes nothing.
Returns null when nothing is moved (insert, missing key, entity without id) or when the
move succeeded; otherwise the failed Result.
$entityId is passed separately: BaseEntity declares no id getter (each entity has its
own), so only the concrete manager knows it.
Parameters
- $isInsert : bool
- $entity : BaseEntity
- $entityId : int|null
- $meta : array<string, mixed>
Return values
Result|nulltreeConfigForEntity()
`TreeConfig` used when `save()` computes lft/rgt of a new entity automatically ({@see SaveTrait}).
protected
treeConfigForEntity(BaseEntity $entity) : TreeConfig
The scope is read from the entity through ScopedTreeNodeInterface::getTreeScope():
several trees in one table (for example menu items told apart by menuId) are solved by
implementing the interface on the entity, not by overriding this method. The test targets
the interface, not a concrete class, so it also works for an entity touched through the
admin bridge (which persists the Admin variant, not the Api one; the Api SaveTrait
rewrites only \Front\ to \Api\ in entityClass). A tree with a single numbering per
table (pages) does not implement the interface and gets an empty scope.
Parameters
- $entity : BaseEntity
Return values
TreeConfigcaptureSubtreeAssetIds()
Returns the ids of images owned by the whole subtree; empty when the manager declares no image cleanup.
private
captureSubtreeAssetIds(int $id, TreeConfig $config) : array<int, int>
Parameters
- $id : int
- $config : TreeConfig
Return values
array<int, int>moveTreeNode()
private
moveTreeNode(int $id, int $direction, TreeConfig $config) : Result
Parameters
- $id : int
- $direction : int
- $config : TreeConfig
Return values
ResultrunTreeTransaction()
The body of {@see treeTransaction()}: a transaction only when none is running, a tag bump after success, an exception means log plus rollback of the own transaction.
private
runTreeTransaction(Closure $operation) : Result
Parameters
- $operation : Closure
Return values
ResulttreeService()
Loud guard against silent failure: tree operations run only when the underlying service implements the method (`TreeTrait` on service, mapper and repository). A missing one is a wiring bug, so a {@see \LogicException} is thrown (logged through `treeTransaction()` / `save()`) instead of a silent `INVALID_OPERATION` that did not show where the problem is.
private
treeService(string $method) : TreeServiceInterface
Parameters
- $method : string
Return values
TreeServiceInterfacetreeTransaction()
Wraps a tree operation in a transaction (when the service supports it): the operations rewrite lft/rgt on many rows and a partial write would break the tree.
private
treeTransaction(Closure $operation[, TreeConfig|null $lockConfig = null ]) : Result
IMPORTANT: opens its own transaction only when none is running. A nested call (typically
applyMetaSubtreeMove() from afterSave inside
SaveTrait::save()) runs in the caller's
transaction and only returns the result; the caller handles commit/rollback.
That way the caller keeps control over commit/rollback and returns the concrete Result
(TREE_NODE_CORRUPTED) instead of a generic save error.
IMPORTANT: tree lock ($lockConfig): when passed, it is taken before the transaction opens
and released only after commit/rollback, so no other operation ever reads bounds this one
has not committed yet, and waiting for the lock does not open a transaction (a timeout
returns TREE_LOCKED, nothing started). The repository takes the lock again under it
(re-entrant GET_LOCK). Without $lockConfig (older callers, for example PageManager)
only the repository locks.
Parameters
- $operation : Closure
- $lockConfig : TreeConfig|null = null