TraversalController extends Controller uses AuthorizationContextTrait, CapabilityContextTrait, ModelCallTrait, PermissionAuthorizerTrait, PrepareFilter
Navigates a **self-referential** edge (a graph whose two ends target the same vertex collection, e.g. a category tree or an org chart) and returns the traversed vertices, hydrated with the target collection's schema.
A single instance exposes the four navigation methods — designed to be wired,
one route each, by a TraversalRoute on a base
/{collection}/{id} path:
| Method | Direction | Transitive | Cardinality | Typical route |
|---|---|---|---|---|
| getParent | INBOUND | no | one/null | /{id}/parent |
| getChildren | OUTBOUND | no | list | /{id}/children |
| getAncestors | INBOUND | yes | list | /{id}/ancestors |
| getDescendants | OUTBOUND | yes | list | /{id}/descendants |
The edge is injected once through self::EDGE. The transitive methods
accept a ?depth= query parameter, clamped to self::DEFAULT_MAX_DEPTH
(defaulting to the full sub-tree). Vertices hydrate through the edge's target
model (Edges::get*Vertices()), so a projected field survives the traversal.
All four methods accept a ?filter= predicate (the same JSON DSL as the
Documents surface) restricting the traversed vertices, and ?prune= cutting
the whole branch under a non-matching vertex (rejected on the inbound methods).
Both are compiled by the edge model's gated engine — whitelist + authorizer —
never inlined raw.
It carries an authorization seat: the four traversals are wrapped by the
ModelCallTrait hooks, so a consumer can impose a server-side scope the
caller cannot widen. beforeModelCall() runs after ?filter= and
?prune= have been compiled in — last word to the scope — and the class poses
no scope of its own.
The payload differs from the other surfaces, and that is not a detail. At
that point AQL::FILTER no longer holds a JSON predicate but a list of
compiled AQL fragments targeting the vertex variable, alongside their
AQL::BINDS — because getVertices() reads that slot and nothing else
(Arango::CONDITIONS is never consulted on the traversal path). An override
therefore appends a fragment, not a predicate; compileVertexPredicate()
is protected so it can compile one through the same whitelist and gate as the
client filter.
Tags
Table of Contents
Constants
- DEFAULT_MAX_DEPTH : int = 16
- The hard cap on the traversal depth.
- DEPTH_PARAM : string = 'depth'
- The `?depth=` query-parameter name capping a transitive traversal.
- EDGE : string = 'edge'
- Initialization key for the self-referential edge model.
- GET_ANCESTORS : string = 'getAncestors'
- The name of the {@see getAncestors} method — for a route method binding.
- GET_CHILDREN : string = 'getChildren'
- The name of the {@see getChildren} method — for a route method binding.
- GET_DESCENDANTS : string = 'getDescendants'
- The name of the {@see getDescendants} method — for a route method binding.
- GET_PARENT : string = 'getParent'
- The name of the {@see getParent} method — for a route method binding.
- PRUNE_PARAM : string = 'prune'
- The `?prune=` query-parameter name — a JSON predicate that cuts the whole branch under a non-matching vertex. Rejected on inbound traversals.
Properties
Methods
- __construct() : mixed
- Creates a new TraversalController instance.
- getAncestors() : mixed
- Returns the **ancestors** lineage up to the root (INBOUND, transitive).
- getChildren() : mixed
- Returns the direct **children** vertices (OUTBOUND).
- getDescendants() : mixed
- Returns the full **descendants** sub-tree (OUTBOUND, transitive).
- getParent() : mixed
- Returns the single **parent** vertex (INBOUND, direct), or `null` for a root.
- compileVertexPredicate() : string|null
- Compiles one JSON predicate into an AQL fragment against the traversed `vertex` variable (matching `aqlTraversal`'s `FOR vertex …` and `getVertices`' `RETURN vertex`), accumulating its binds into `$binds`.
- initializeAuthorizationContext() : static
- Resolves the capability enforcer and the permission-subject resolver from the container (each guarded by an `instanceof`, null when absent) and wires them through `initializeCapabilities()` and `initializePermissionSubjectResolver()`.
- id() : string|null
- Resolves the `{id}` placeholder, or fails.
- many() : mixed
- Backs the list methods (children / ancestors / descendants).
- prepareTraversal() : bool
- Resolves the traversal entry point shared by {@see self::single()} and {@see self::many()} : the `{id}`, the decoded `?prune=` predicate, and the three refusals both surfaces have in common — a missing `{id}`, an unconfigured edge, and `?prune=` on an inbound traversal.
- prepareVertexFilter() : array<string|int, mixed>
- Folds the client `?filter=` and `?prune=` predicates (both a JSON predicate, the same DSL as the `Documents` surface) into the traversal `$modelInit` — as AQL fragments plus their binds — targeting the traversed `vertex`.
- readPrune() : array<string|int, mixed>|null
- Reads and decodes the `?prune=` query parameter (a JSON predicate).
- single() : mixed
- Backs the single-vertex methods (the direct parent).
Constants
DEFAULT_MAX_DEPTH
The hard cap on the traversal depth.
public
int
DEFAULT_MAX_DEPTH
= 16
DEPTH_PARAM
The `?depth=` query-parameter name capping a transitive traversal.
public
string
DEPTH_PARAM
= 'depth'
EDGE
Initialization key for the self-referential edge model.
public
string
EDGE
= 'edge'
GET_ANCESTORS
The name of the {@see getAncestors} method — for a route method binding.
public
string
GET_ANCESTORS
= 'getAncestors'
GET_CHILDREN
The name of the {@see getChildren} method — for a route method binding.
public
string
GET_CHILDREN
= 'getChildren'
GET_DESCENDANTS
The name of the {@see getDescendants} method — for a route method binding.
public
string
GET_DESCENDANTS
= 'getDescendants'
GET_PARENT
The name of the {@see getParent} method — for a route method binding.
public
string
GET_PARENT
= 'getParent'
PRUNE_PARAM
The `?prune=` query-parameter name — a JSON predicate that cuts the whole branch under a non-matching vertex. Rejected on inbound traversals.
public
string
PRUNE_PARAM
= 'prune'
Properties
$edges
The self-referential edges model.
protected
Edges|null
$edges
= null
Methods
__construct()
Creates a new TraversalController instance.
public
__construct(Container $container[, array<string|int, mixed> $init = [] ]) : mixed
Parameters
- $container : Container
-
The DI container reference.
- $init : array<string|int, mixed> = []
-
Supports self::EDGE : the self-referential
Edgesmodel (service id or instance).
Tags
getAncestors()
Returns the **ancestors** lineage up to the root (INBOUND, transitive).
public
getAncestors([ServerRequestInterface|null $request = null ][, ResponseInterface|null $response = null ][, array<string|int, mixed> $args = [] ][, array<string|int, mixed> $init = [] ]) : mixed
Parameters
- $request : ServerRequestInterface|null = null
- $response : ResponseInterface|null = null
- $args : array<string|int, mixed> = []
-
Route placeholders — expects Schema::ID.
- $init : array<string|int, mixed> = []
Tags
getChildren()
Returns the direct **children** vertices (OUTBOUND).
public
getChildren([ServerRequestInterface|null $request = null ][, ResponseInterface|null $response = null ][, array<string|int, mixed> $args = [] ][, array<string|int, mixed> $init = [] ]) : mixed
Parameters
- $request : ServerRequestInterface|null = null
- $response : ResponseInterface|null = null
- $args : array<string|int, mixed> = []
-
Route placeholders — expects Schema::ID.
- $init : array<string|int, mixed> = []
Tags
getDescendants()
Returns the full **descendants** sub-tree (OUTBOUND, transitive).
public
getDescendants([ServerRequestInterface|null $request = null ][, ResponseInterface|null $response = null ][, array<string|int, mixed> $args = [] ][, array<string|int, mixed> $init = [] ]) : mixed
Parameters
- $request : ServerRequestInterface|null = null
- $response : ResponseInterface|null = null
- $args : array<string|int, mixed> = []
-
Route placeholders — expects Schema::ID.
- $init : array<string|int, mixed> = []
Tags
getParent()
Returns the single **parent** vertex (INBOUND, direct), or `null` for a root.
public
getParent([ServerRequestInterface|null $request = null ][, ResponseInterface|null $response = null ][, array<string|int, mixed> $args = [] ][, array<string|int, mixed> $init = [] ]) : mixed
Parameters
- $request : ServerRequestInterface|null = null
- $response : ResponseInterface|null = null
- $args : array<string|int, mixed> = []
-
Route placeholders — expects Schema::ID.
- $init : array<string|int, mixed> = []
Tags
compileVertexPredicate()
Compiles one JSON predicate into an AQL fragment against the traversed `vertex` variable (matching `aqlTraversal`'s `FOR vertex …` and `getVertices`' `RETURN vertex`), accumulating its binds into `$binds`.
protected
compileVertexPredicate(array<string|int, mixed> $predicate, mixed $authorizer, array<string|int, mixed> &$binds) : string|null
Returns null when nothing is filterable — an undeclared attribute or an
empty AQL::FILTERS whitelist — so the caller drops the fragment.
Protected on purpose: an override of beforeModelCall() posing a
server-side scope can compile its own JSON predicate through the very same
whitelist and Field::REQUIRES gate as the client ?filter=, instead of
hand-writing AQL. Two things to know when calling it from a hook:
- the binds are merged into the array handed in, so pass (and give back)
$init[ AQL::BINDS ]rather than overwriting it ; - a
nullreturn means the attribute is not declared filterable — a wiring error, never "no scope". Treating it as the latter makes a mis-declared scope evaporate silently.
Parameters
- $predicate : array<string|int, mixed>
-
The decoded JSON predicate.
- $authorizer : mixed
-
The request-scoped authorizer (or null).
- $binds : array<string|int, mixed>
-
The accumulating bind variables (by reference).
Tags
Return values
string|null —The compiled vertex.<field> … fragment, or null.
initializeAuthorizationContext()
Resolves the capability enforcer and the permission-subject resolver from the container (each guarded by an `instanceof`, null when absent) and wires them through `initializeCapabilities()` and `initializePermissionSubjectResolver()`.
protected
initializeAuthorizationContext([array<string, mixed> $init = [] ]) : static
Parameters
- $init : array<string, mixed> = []
-
Same array passed to the controller constructor.
Tags
Return values
staticid()
Resolves the `{id}` placeholder, or fails.
private
id(array<string|int, mixed> $args) : string|null
Parameters
- $args : array<string|int, mixed>
Return values
string|nullmany()
Backs the list methods (children / ancestors / descendants).
private
many(ServerRequestInterface|null $request, ResponseInterface|null $response, array<string|int, mixed> $args, bool $inbound, bool $transitive[, array<string|int, mixed> $init = [] ]) : mixed
Parameters
- $request : ServerRequestInterface|null
- $response : ResponseInterface|null
- $args : array<string|int, mixed>
- $inbound : bool
- $transitive : bool
- $init : array<string|int, mixed> = []
Tags
prepareTraversal()
Resolves the traversal entry point shared by {@see self::single()} and {@see self::many()} : the `{id}`, the decoded `?prune=` predicate, and the three refusals both surfaces have in common — a missing `{id}`, an unconfigured edge, and `?prune=` on an inbound traversal.
private
prepareTraversal(ServerRequestInterface|null $request, ResponseInterface|null $response, array<string|int, mixed> $args, bool $inbound, string|null &$id, array<string|int, mixed>|null &$prune, mixed &$failure) : bool
The verdict travels through the return value, never through $failure :
StatusTrait::fail() yields null when no
$response was provided (a non-HTTP call), so a null $failure cannot tell
a refusal from a pass.
Parameters
- $request : ServerRequestInterface|null
-
The current PSR-7 request.
- $response : ResponseInterface|null
-
The PSR-7 response, or null out of HTTP.
- $args : array<string|int, mixed>
-
The route placeholders — expects Schema::ID.
- $inbound : bool
-
Whether the traversal walks INBOUND.
- $id : string|null
-
Receives the resolved
{id}when the entry passes. - $prune : array<string|int, mixed>|null
-
Receives the decoded
?prune=predicate, or null. - $failure : mixed
-
Receives the failure response when the entry is refused — itself null when
$responseis null.
Return values
bool —True when the traversal may proceed, false when it is refused.
prepareVertexFilter()
Folds the client `?filter=` and `?prune=` predicates (both a JSON predicate, the same DSL as the `Documents` surface) into the traversal `$modelInit` — as AQL fragments plus their binds — targeting the traversed `vertex`.
private
prepareVertexFilter(ServerRequestInterface|null $request, array<string|int, mixed> $init, array<string|int, mixed> $modelInit[, array<string|int, mixed>|null $prune = null ]) : array<string|int, mixed>
Neither predicate reaches the traversal raw : getVertices() inlines its
AQL::FILTER slot, and aqlTraversal() its AQL::PRUNE slot, verbatim
(server-only knobs), so the JSON is first compiled by the edge model's
gated engine (Edges::prepareFilter()), which applies the model's
AQL::FILTERS whitelist and, through the request authorizer, the
Field::REQUIRES gate. An undeclared or hidden attribute therefore cannot be
probed through the traversal (no filter oracle).
The two levers differ only on the descent:
?filter=hides a non-matching vertex but the traversal still descends through it (a matching grand-child survives a filtered parent) ;?prune=cuts the whole branch under a non-matching vertex — the boundary vertex is excluded too (its condition also joins theFILTER) and its sub-tree is never walked (PRUNE !( condition )).
They compose : every condition narrows the returned set (they AND in the
FILTER), and only the prune one also stops the descent.
The same request-scoped authorizer is threaded under Arango::AUTHORIZER
so the vertex projection (Edges::returnFields()) is gated too — but
only when one exists : with no authorization stack it stays absent and the
projection falls open (backward compatible).
Parameters
- $request : ServerRequestInterface|null
-
The current PSR-7 request.
- $init : array<string|int, mixed>
-
The caller init (an
Arango::AUTHORIZERhere wins). - $modelInit : array<string|int, mixed>
-
The traversal init being assembled.
- $prune : array<string|int, mixed>|null = null
-
The decoded
?prune=predicate, or null.
Tags
Return values
array<string|int, mixed> —The $modelInit enriched with AQL::FILTER / AQL::PRUNE /
AQL::BINDS / Arango::AUTHORIZER when applicable.
readPrune()
Reads and decodes the `?prune=` query parameter (a JSON predicate).
private
readPrune(ServerRequestInterface|null $request) : array<string|int, mixed>|null
Malformed or non-array JSON degrades to null (no prune), mirroring the
?filter= reader.
Parameters
- $request : ServerRequestInterface|null
-
The current PSR-7 request.
Return values
array<string|int, mixed>|null —The decoded predicate, or null.
single()
Backs the single-vertex methods (the direct parent).
private
single(ServerRequestInterface|null $request, ResponseInterface|null $response, array<string|int, mixed> $args, bool $inbound[, array<string|int, mixed> $init = [] ]) : mixed
Parameters
- $request : ServerRequestInterface|null
- $response : ResponseInterface|null
- $args : array<string|int, mixed>
- $inbound : bool
- $init : array<string|int, mixed> = []