helpers
Table of Contents
Namespaces
Functions
- assertDocuments() : void
- Ensures that a given value is an instance of {@see Documents}.
- authorizeRelationFields() : array<string, mixed>|null
- Drops the relation markers whose edge/join definition is denied by the request-scoped authorizer (definition-level gating, `AQL::REQUIRES` on the definition itself).
- authorizeTargetFields() : array<string|int, mixed>|null
- Re-applies the **target model**'s `Field::REQUIRES` gates to a relation's projection (T6).
- buildPolymorphicRelationVariable() : string
- Assembles the AQL 'LET' of a *polymorphic* relation — a join or an edge whose target collection is chosen at query time from a discriminator field — shared by {@see \oihana\arango\models\helpers\joins\buildPolymorphicJoinVariable()} and {@see \oihana\arango\models\helpers\edges\buildPolymorphicEdgeVariable()}.
- buildTree() : array<string|int, mixed>
- Reconstructs a nested `children[]` tree from a **flat** list of nodes.
- buildTreeAlter() : callable
- Builds an {@see \oihana\models\enums\Alter::MAP} callback that reshapes a flat hierarchy projection into a nested `children[]` tree via {@see buildTree()}.
- buildVariables() : void
- Builds all fields variables definitions (edges, joins, etc.).
- extractFromIDs() : array<string|int, mixed>
- Extract unique "_from" vertex IDs from a list of edge documents.
- extractNestedRelations() : array{edges: array, joins: array}
- Extract nested edges and joins from a relation configuration or resolved model.
- extractToIDs() : array<string|int, mixed>
- Extract unique "_to" vertex IDs from a list of edge documents.
- extractVertexIDs() : array<string|int, mixed>
- Extract unique vertex IDs from a list of edge documents.
- getDocuments() : Documents|null
- Resolves a {@see Documents} instance from various types of input definitions.
- isAttributeAuthorized() : bool
- Decides whether a query attribute (a filter / facet / group key) is allowed for the current request, by inheriting the permission of the homonymous projection field.
- isAuthorized() : bool
- Decides whether a field projection is allowed for the current request.
- isPathAuthorized() : bool
- Decides whether a *dotted* query attribute (a filter / facet / group path) is allowed for the current request, by inheriting the projection permission of the **exact sub-field** it targets — not only its root.
- isPolymorphic() : bool
- Tells whether a relation definition — a **join** or an **edge** — is *polymorphic*, i.e. its target collection is chosen at query time from a discriminator field of the parent document / start vertex.
- normalizeSortable() : array<string, string|array<int, string>>|null
- Normalises a `sortable` whitelist into the canonical `urlKey => fieldPath` associative map consumed by {@see \oihana\arango\models\traits\aql\SortTrait::prepareSort()}.
- parseFilterSegment() : FilterPath|null
- Parse a single path segment from hierarchical configuration.
- prepareRelationDefinition() : array<string|int, mixed>|null
- Resolves, permission-gates and stamps a relation definition (edge or join) before it is handed to a relation builder — the common preamble shared by the `Filter::EDGE(S)` and `Filter::JOIN(S)` branches of {@see buildVariables()}.
- sortRelationVariable() : string
- Generates the internal AQL 'SORT' clause for a relation sub-query (edge or join).
- vertexID() : string|null
- Resolves a fully-qualified vertex ID for an ArangoDB edge.
Functions
assertDocuments()
Ensures that a given value is an instance of {@see Documents}.
assertDocuments(mixed $value) : void
This runtime assertion validates type safety for Documents.
If the provided value is not an instance of Documents, an
UnexpectedValueException is thrown with a descriptive message.
This is especially useful when handling dynamically typed data or container-resolved dependencies, where you want to enforce strict model integrity.
Parameters
- $value : mixed
-
The value to assert as an Documents instance.
Tags
authorizeRelationFields()
Drops the relation markers whose edge/join definition is denied by the request-scoped authorizer (definition-level gating, `AQL::REQUIRES` on the definition itself).
authorizeRelationFields(array<string, mixed>|null $fields, array<string, mixed>|null $edges, array<string, mixed>|null $joins[, array<string|int, mixed> $init = [] ]) : array<string, mixed>|null
A relation is emitted by two parallel walks that must stay symmetric :
the LET sub-query (buildVariables) and the projected key (aqlFields).
When a definition declares AQL::REQUIRES and the authorizer denies it,
the LET is not emitted — so the matching marker must also disappear from
the projected fields, otherwise the RETURN would reference an unbound
variable. This helper performs that field-side purge : it is applied at
every point where a prepared fields array meets its edges/joins registries,
right before buildVariables() / aqlFields() run on them.
Resolution mirrors buildVariables() : the definition is looked up in the registry under the field key, a string value being a one-hop alias to another registry entry. A marker without a resolvable array definition is left untouched (buildVariables skips it anyway — nothing to desynchronize).
The check itself is delegated to isAuthorized() with the whole
definition (its AQL::REQUIRES key equals Field::REQUIRES), so the
semantics are exactly the field-level ones : no marker → allowed, no
authorizer → allowed (fail open), a list of subjects → logical OR. The
definition-level gate COMPOSES with the field-level Field::REQUIRES
(evaluated downstream) : either level can drop the relation.
The function is pure and idempotent : applying it twice — e.g. once on the
LET walk and once on the projection walk of a wrapped reference — yields
the same fields, which is precisely what keeps both walks symmetric.
Parameters
- $fields : array<string, mixed>|null
-
The prepared query fields (normalized definitions).
- $edges : array<string, mixed>|null
-
The edges registry paired with the fields.
- $joins : array<string, mixed>|null
-
The joins registry paired with the fields.
- $init : array<string|int, mixed> = []
-
The request-level init array (reads
Arango::AUTHORIZER).
Tags
Return values
array<string, mixed>|null —The fields with denied relation markers removed.
authorizeTargetFields()
Re-applies the **target model**'s `Field::REQUIRES` gates to a relation's projection (T6).
authorizeTargetFields(array<string|int, mixed>|null $fields, Documents|null $documents[, array<string|int, mixed> $init = [] ]) : array<string|int, mixed>|null
When an edge/join definition declares its own projection
(AQL::FIELDS / AQL::SKIN_FIELDS), that ad-hoc field list replaces the
target model's $fields — so the Field::REQUIRES markers carried by the
target model are not applied, and a field hidden from reading could be
re-projected in clear through the relation. This helper closes that hole by
dropping every projected field whose source attribute is refused by the
target model's own projection (isPathAuthorized() against
$documents->fields), mirroring the read-side gate applied everywhere else.
The source attribute — Field::NAME when the field aliases a document
attribute, otherwise the output key — is what gets gated, never the output
label, so an alias cannot dodge (or borrow) the wrong permission.
Relation-scoped OR alternative — Field::SELF_REQUIRES. A field refused by
the target model is nonetheless kept when it declares Field::SELF_REQUIRES
and the authorizer grants at least one of its subjects — the relation being a
legitimately broader context (reading a sub-field of a resource the caller owns).
This override lives only here: first-level model reads never pass through this
helper, so it never widens a model's direct reads. The subjects are normalized to
a non-empty list of strings before the check, so an empty / malformed value cannot
borrow isAuthorized()'s "no subject → true" fail-open and re-open T6.
Fail-open and idempotent: with no target model, no $documents->fields, a
field carrying no Field::REQUIRES on the target, or no authorizer injected,
the field is kept — and re-running it on a projection that already came from
$documents->fields changes nothing.
Parameters
- $fields : array<string|int, mixed>|null
-
The relation's resolved projection (mutated copy returned).
- $documents : Documents|null
-
The resolved target model (reads its
$fields). - $init : array<string|int, mixed> = []
-
The request-level init. Reads
Arango::AUTHORIZER.
Tags
Return values
array<string|int, mixed>|null —The projection with the target-refused fields removed.
buildPolymorphicRelationVariable()
Assembles the AQL 'LET' of a *polymorphic* relation — a join or an edge whose target collection is chosen at query time from a discriminator field — shared by {@see \oihana\arango\models\helpers\joins\buildPolymorphicJoinVariable()} and {@see \oihana\arango\models\helpers\edges\buildPolymorphicEdgeVariable()}.
buildPolymorphicRelationVariable(string|null $name, array<string|int, mixed> $definition, string $ref, array<string|int, mixed> $init, callable $buildBranch) : string
AQL forbids a computed collection in FOR … IN …, so the relation is compiled
as an APPEND of guarded static branches: one sub-query per Arango::MAP
entry, each guarded by an equality on the discriminator so only the matching
branch yields rows. The $buildBranch callback is the only relation-specific
part — it turns a branch definition + its guard into a parenthesized
sub-query string (a join wraps buildJoinSubquery(), an edge delegates to the
already-parenthesized buildEdgeSubquery()).
Security (fail-closed):
- Per-branch gate — a branch denied by
isAuthorized()(Field::REQUIRES/AQL::REQUIRES) is dropped from theAPPEND; its collection is never queried, so neither a value nor an existence bit of the hidden type leaks. - Fallback — an optional
Arango::FALLBACKbranch catches discriminator values matching none of the DECLARED types, guarded byNOT IN [ … ]over all map keys (gated or not), so a document of a denied type routes to nothing, never to the fallback (no oracle). - When every branch is dropped the
LETholds[], so the projection resolves tonull/[]rather than a broken statement.
Parameters
- $name : string|null
-
The relation field name (also the default
LETvariable name). - $definition : array<string|int, mixed>
-
The polymorphic definition. Keys:
Arango::DISCRIMINATOR(string) Parent / start-vertex field deciding the branch (required).Arango::MAP(array) Non-emptytype => relation-definitiontable (required).Arango::UNIQUE(string|null) OptionalLETvariable name, overrides$name.Arango::FALLBACK(array|null) Definition for unmatched discriminator values (null= none).
- $ref : string
-
The AQL variable name carrying the discriminator (
docReffor a join, the start vertex for an edge). - $init : array<string|int, mixed>
-
The request-level init array (reads
Arango::AUTHORIZER). - $buildBranch : callable
-
fn(array $branch, string $guard): string— builds one parenthesized, guarded branch sub-query. Called only on array branches that pass the gate.
Tags
Return values
string —The complete AQL 'LET' statement.
buildTree()
Reconstructs a nested `children[]` tree from a **flat** list of nodes.
buildTree(array<string|int, mixed> $flat[, string $parentSource = AQL::_PARENT ][, string|null $rootKey = null ][, string $childrenKey = AQL::CHILDREN ][, string $keyField = Schema::_KEY ]) : array<string|int, mixed>
The flat list is the shape produced by a depth-ranged edge projection
(see buildEdgeVariable() with AQL::MAX_DEPTH): every element is an
associative row that knows its parent — either through the AQL::WITH_PATH
injected _parent key (default), or through a parent field the document
already stores (pass its name as $parentSource, e.g. 'broader').
Each returned node gains a $childrenKey (default children) holding its
direct children, recursively.
Roots
- When
$rootKeyis given, the roots are the nodes whose parent equals it (the start vertex of the traversal — its own key). This is what buildTreeAlter() passes (the document's_key). - When
$rootKeyisnull, a node is a root when its parent key is absent from the list (a depth-1 node points at the — unlisted — start vertex).
Robustness
The reconstruction is O(n) and cycle-safe: a node already seen on the
current branch is never descended into again (pathological self-referential
data cannot cause infinite recursion). A node whose parent is missing simply
becomes a root. Each node is expected to have a single parent — with
AQL::WITH_PATH this is guaranteed by the traversal's global vertex
uniqueness. Rows that are not associative arrays are ignored.
Parameters
- $flat : array<string|int, mixed>
-
The flat list of nodes (associative arrays).
- $parentSource : string = AQL::_PARENT
-
The key holding each node's parent key (default
AQL::_PARENT=_parent). - $rootKey : string|null = null
-
The start-vertex key;
nullinfers the roots. - $childrenKey : string = AQL::CHILDREN
-
The key under which children are nested (default
children). - $keyField : string = Schema::_KEY
-
The identity key of a node (default
Schema::_KEY=_key).
Tags
Return values
array<string|int, mixed> —The list of root nodes, each with a nested $childrenKey.
buildTreeAlter()
Builds an {@see \oihana\models\enums\Alter::MAP} callback that reshapes a flat hierarchy projection into a nested `children[]` tree via {@see buildTree()}.
buildTreeAlter([string $parentSource = AQL::_PARENT ][, string $childrenKey = AQL::CHILDREN ][, string $keyField = Schema::_KEY ]) : callable
Declare it on the field that carries the depth-ranged projection:
use oihana\arango\models\enums\Alter ;
use function oihana\arango\models\helpers\buildTreeAlter ;
AQL::FIELDS =>
[
Prop::DESCENDANTS =>
[
Field::FILTER => Filter::EDGES ,
Field::ALTERS => [ [ Alter::MAP , buildTreeAlter() ] ] ,
],
],
AQL::EDGES =>
[
Prop::DESCENDANTS =>
[
AQL::MODEL => 'concept_links' ,
AQL::DIRECTION => Traversal::OUTBOUND ,
AQL::MAX_DEPTH => 5 ,
AQL::WITH_PATH => true , // provides the `_parent` used to rebuild the tree
],
],
At map time the callback:
- leaves the value untouched when it is not an array (an absent/scalar relation),
- reads the root from the enclosing document's
$keyField(the traversal start vertex — its own_key), - returns the nested tree built by buildTree().
The parent link source defaults to AQL::_PARENT (_parent, injected by
AQL::WITH_PATH); pass a stored field name instead (e.g. 'broader') when the
document already carries its parent.
Parameters
- $parentSource : string = AQL::_PARENT
-
The key holding each node's parent key (default
AQL::_PARENT). - $childrenKey : string = AQL::CHILDREN
-
The key under which children are nested (default
children). - $keyField : string = Schema::_KEY
-
The identity key of a node and of the enclosing document (default
Schema::_KEY).
Tags
Return values
callable —A callback matching the Alter::MAP signature
( array|object $document, ?Container $container, string $key, mixed $value, array $params, array $context ): mixed.
buildVariables()
Builds all fields variables definitions (edges, joins, etc.).
buildVariables([array<string|int, mixed> &$variables = [] ][, array<string|int, mixed> $fields = [] ][, array<string|int, mixed>|null $edges = [] ][, array<string|int, mixed>|null $joins = [] ][, ContainerInterface|null $container = null ][, string $docRef = AQL::DOC ][, array<string|int, mixed> $init = [] ]) : void
Permission gating happens at TWO composable levels — either one can drop the relation (logical AND across levels, logical OR inside a declared subjects list):
- Field-level — the FIELDS entry (the relation marker) declares
Field::REQUIRES: gates this projection of the relation in this parent. Combined with the symmetric drop inaqlFields(), the field disappears from both the AQL projection and the response — no key in the JSON, no orphan reference in the AQL. - Definition-level — the edge/join definition itself declares
AQL::REQUIRES: gates the relation wherever the definition is used. The symmetric field-side drop is handled upstream by authorizeRelationFields() at every point where a prepared fields array meets its registries.
The check is uniform: edges, joins, and edge-counts all share the
same gating contract. A Filter::EDGES_COUNT companion is gated
exactly the same way as the others — it is simply not gated by
default because no Field::REQUIRES is declared on it. To gate
the count, declare Field::REQUIRES on the count entry itself
(or AQL::REQUIRES on the shared definition to gate every usage).
Parameters
- $variables : array<string|int, mixed> = []
- $fields : array<string|int, mixed> = []
- $edges : array<string|int, mixed>|null = []
- $joins : array<string|int, mixed>|null = []
- $container : ContainerInterface|null = null
- $docRef : string = AQL::DOC
- $init : array<string|int, mixed> = []
Tags
extractFromIDs()
Extract unique "_from" vertex IDs from a list of edge documents.
extractFromIDs(array<string|int, mixed> $edges) : array<string|int, mixed>
This helper function is a shortcut for extractVertexIds() with $side fixed to Schema::_FROM.
It returns a unique list of vertex IDs from the "_from" field in an array of edge documents
(objects or associative arrays).
Parameters
- $edges : array<string|int, mixed>
-
Array of edge objects or associative arrays containing
_fromand_to.
Tags
Return values
array<string|int, mixed> —Unique list of "_from" vertex IDs.
extractNestedRelations()
Extract nested edges and joins from a relation configuration or resolved model.
extractNestedRelations(array<string|int, mixed> $config[, object|null $targetModel = null ][, bool|null $isEdge = null ][, ContainerInterface|null $container = null ]) : array{edges: array, joins: array}
This function can work in two modes:
- Resolution mode: Resolves target model from config via container, then extracts relations
- Direct mode: Extracts relations from an already-resolved target model
In both modes, the function merges the extracted relations with any explicit AQL::EDGES and AQL::JOINS defined in the relation configuration.
Resolution mode (for parseFilterSegment):
$relations = extractNestedRelations(
config: $edgeConfig,
isEdge: true,
container: $container
);
Direct mode (for buildEdgeTraversal/buildJoinTraversal):
$relations = extractNestedRelations(
config: $edgeConfig,
targetModel: $alreadyResolvedModel
);
Parameters
- $config : array<string|int, mixed>
-
The relation configuration (edge or join config).
- $targetModel : object|null = null
-
Optional: already-resolved target model.
- $isEdge : bool|null = null
-
Required if $targetModel is null: true for edges, false for joins.
- $container : ContainerInterface|null = null
-
Required if $targetModel is null: DI container.
Tags
Return values
array{edges: array, joins: array} —Associative array with 'edges' and 'joins' keys.
extractToIDs()
Extract unique "_to" vertex IDs from a list of edge documents.
extractToIDs(array<string|int, mixed> $edges) : array<string|int, mixed>
This helper function is a shortcut for extractVertexIds() with $side fixed to Schema::_TO.
It returns a unique list of vertex IDs from the "_to" field in an array of edge documents
(objects or associative arrays).
Parameters
- $edges : array<string|int, mixed>
-
Array of edge objects or associative arrays containing
_fromand_to.
Tags
Return values
array<string|int, mixed> —Unique list of "_to" vertex IDs.
extractVertexIDs()
Extract unique vertex IDs from a list of edge documents.
extractVertexIDs(array<string|int, mixed> $edges, string $side) : array<string|int, mixed>
This helper function is used to extract either the "_from" or "_to" vertex IDs from an array of edge documents (objects or arrays) and return only the unique IDs.
Parameters
- $edges : array<string|int, mixed>
-
Array of edge objects or associative arrays containing
_fromand_to. - $side : string
-
The side to extract, either
Schema::_FROMorSchema::_TO.
Tags
Return values
array<string|int, mixed> —Unique list of vertex IDs.
getDocuments()
Resolves a {@see Documents} instance from various types of input definitions.
getDocuments([array<string|int, mixed>|string|Documents|null $definition = null ][, ContainerInterface|null $container = null ][, string $key = Arango::DOCUMENTS ][, Documents|null $default = null ]) : Documents|null
This helper function returns a Documents object from: a direct instance, an array definition, a service name within a PSR-11 container, or falls back to a provided default value.
Behavior:
- If
$definitionis a Documents instance, it is returned as-is. - If
$definitionis an array, the function looks for the$key(default: Arango::DOCUMENTS). - If
$definitionis a non-empty string and$containercontains a service with that name, the corresponding service is fetched. - If none of the above conditions are met, the
$defaultvalue is returned.
Parameters
- $definition : array<string|int, mixed>|string|Documents|null = null
-
Input definition that may represent an
Documentsinstance, an associative array containing one, or a container service name. - $container : ContainerInterface|null = null
-
Optional PSR-11 container used to resolve string service names.
- $key : string = Arango::DOCUMENTS
-
Array key to look for when
$definitionis an array - $default : Documents|null = null
-
Default
Documentsinstance to return if resolution fails. (defaults to Arango::DOCUMENTS).
Tags
Return values
Documents|null —Returns the resolved Documents instance or the default value if not found.
isAttributeAuthorized()
Decides whether a query attribute (a filter / facet / group key) is allowed for the current request, by inheriting the permission of the homonymous projection field.
isAttributeAuthorized(string $key, array<string|int, mixed>|null $fields[, array<string|int, mixed> $init = [] ]) : bool
The whitelists that gate what can be queried (AQL::FILTERS, Arango::FACETS,
Arango::GROUPABLE) are disjoint from the projection permission (Field::REQUIRES
on $fields). Without this bridge, a field declared queryable but hidden from
reading could still be filtered / faceted / grouped on, and the presence, the
distribution or the order of the results would leak the hidden value (an oracle).
This helper closes that gap the same way SortTrait
does for sorting: it looks the attribute up in the projection map $fields and, when
that field carries a Field::REQUIRES subject, defers the decision to the shared
isAuthorized() gate (backend-agnostic closure injected through
$init[Arango::AUTHORIZER]). Devise: « what you cannot read, you cannot query on ».
Resolution rules (aligned on the field-level semantics):
$fieldsis not an array, or the attribute is absent / not an array definition →true(nothing to inherit, no gating).- The field definition carries no
Field::REQUIRES→true(no gating). - Otherwise → the result of isAuthorized() (OR over the subjects list, fail-open when no authorizer is injected).
Parameters
- $key : string
-
The public attribute key (already whitelisted by the surface).
- $fields : array<string|int, mixed>|null
-
The model projection map (
$this->fields), keyed by field name. - $init : array<string|int, mixed> = []
-
The request-level init array. Reads
Arango::AUTHORIZER.
Tags
Return values
bool —true when the attribute may be queried, false when the inherited subject is refused.
isAuthorized()
Decides whether a field projection is allowed for the current request.
isAuthorized(array<string|int, mixed> $definition[, array<string|int, mixed> $init = [] ]) : bool
Reads the optional Field::REQUIRES permission subject(s) declared on the
field definition, then defers the actual decision to a backend-agnostic
Closure(string $subject): bool injected through $init[Arango::AUTHORIZER].
The framework remains agnostic of the underlying authorization layer (Casbin, OPA, custom, ...) — the consumer is responsible for binding the callable to a real enforcer and a request-scoped user identifier.
Resolution rules:
- No
Field::REQUIRESdeclared on the definition →true(no gating). Field::REQUIRESresolves to an empty list →true(no gating).- No
Arango::AUTHORIZERinjected, or value is not callable →true(authorization layer disabled, fail open). - One or more subjects declared →
trueif at least one subject is granted by the callable (logical OR).
Parameters
- $definition : array<string|int, mixed>
-
Field definition. Reads
Field::REQUIRES. - $init : array<string|int, mixed> = []
-
The request-level init array. Reads
Arango::AUTHORIZER.
Tags
Return values
bool —true when the projection is allowed, false when every
declared subject was refused.
isPathAuthorized()
Decides whether a *dotted* query attribute (a filter / facet / group path) is allowed for the current request, by inheriting the projection permission of the **exact sub-field** it targets — not only its root.
isPathAuthorized(string $path, array<string|int, mixed>|null $fields[, array<string|int, mixed> $init = [] ]) : bool
This is the depth-aware counterpart of isAttributeAuthorized(): instead
of gating explode('.', $path)[0] (the root only), it walks the whole path
segment by segment, descending through the sub-fields of the structural fields
(Field::FIELDS, or the per-skin buckets AQL::SKIN_FIELDS) and reading the
optional Field::REQUIRES at every level. A single locked level anywhere on
the path denies the whole path — « what you cannot read, you cannot query on ».
The decision at each level is deferred to the shared isAuthorized() gate
(backend-agnostic closure injected through $init[Arango::AUTHORIZER]), so the
semantics (OR over the subjects list, fail-open when no authorizer is injected)
match the field-level gate exactly.
Resolution rules (aligned on isAttributeAuthorized(), extended in depth) :
$fieldsis not an array →true(nothing to inherit, no gating).- A segment is absent from the current field map(s), or only declared as a
scalar / bool definition →
true(noField::REQUIRESto inherit here). - A segment carries a
Field::REQUIRESrefused by the authorizer →false. - Otherwise the walk descends into the sub-fields and continues; a path with no
deeper declared sub-fields resolves to
true.
Sub-fields resolution (fail-closed union). A structural field can declare
its sub-fields under Field::FIELDS and / or under per-skin buckets
AQL::SKIN_FIELDS. Because a Field::REQUIRES is a permission (independent of
the projection skin), the descent gathers the sub-field from every source
and treats it as locked as soon as it is locked in any one of them — a
sub-field hidden in a single skin bucket stays gated on every path.
A single-segment path behaves exactly like isAttributeAuthorized() — it
is its degenerate case. Each segment is stripped of the [*] array-expansion
marker, so the same helper serves the hierarchical filter leaves.
Parameters
- $path : string
-
The dotted public attribute path (e.g.
address.city,employee[*].salary). - $fields : array<string|int, mixed>|null
-
The model projection map (
$this->fields), keyed by field name. - $init : array<string|int, mixed> = []
-
The request-level init array. Reads
Arango::AUTHORIZER.
Tags
Return values
bool —true when every level of the path is authorized, false as soon as one level is refused.
isPolymorphic()
Tells whether a relation definition — a **join** or an **edge** — is *polymorphic*, i.e. its target collection is chosen at query time from a discriminator field of the parent document / start vertex.
isPolymorphic(mixed $definition) : bool
A polymorphic relation replaces the single AQL::MODEL by:
Arango::DISCRIMINATOR— the parent field path deciding the branch (e.g.selector.areaScopefor a join,kindfor an edge) ;Arango::MAP— a non-emptytype => relation-definitiontable, one branch per discriminator value (each branch is itself a regular relation definition) ;Arango::FALLBACK(optional) — the branch used when the discriminator value matches noArango::MAPkey.
Parameters
- $definition : mixed
-
The resolved relation definition (usually an array).
Tags
Return values
bool —true when the definition carries a non-empty Arango::MAP
and a non-empty string Arango::DISCRIMINATOR, false otherwise.
normalizeSortable()
Normalises a `sortable` whitelist into the canonical `urlKey => fieldPath` associative map consumed by {@see \oihana\arango\models\traits\aql\SortTrait::prepareSort()}.
normalizeSortable(array<int|string, mixed>|null $sortable) : array<string, string|array<int, string>>|null
Three notations are accepted and may be mixed within the same array:
- Associative (legacy) —
urlKey => fieldPath. The public?sort=token (the array key) resolves to the AQL field (the value). The value may be a string or an array path ([ 'address', 'city' ]→address.city). This is the historical form and is returned untouched. - Indexed shorthand — a plain string
fieldName. The token equals the field (urlKey === fieldPath), so the redundantfield => fieldmap is avoided. - Indexed alias — a single-pair array
[ urlKey => fieldPath ], for the rare case where the public token differs from the AQL field (?sort=name→givenNameis written[ 'name' => 'givenName' ]). The pair direction is always[ urlKey => fieldPath ], identical to the associative form.
The pair direction [ urlKey => fieldPath ] is preserved across every notation, so
the legacy associative map and the new indexed forms speak the same language.
Robustness rules:
null(open mode, no whitelist) is returned as-is.- In an indexed alias, an entry keyed by a non-string (a pure list such as
[ 'address', 'city' ]with no token) is dropped — an alias must carry a token. - An indexed value that is neither a string nor an array contributes nothing.
The function is idempotent: re-running it on an already-canonical map returns the same map, so it is safe to apply at construction even when the input is already legacy.
Parameters
- $sortable : array<int|string, mixed>|null
-
The raw
sortabledefinition, in any of the three notations.
Tags
Return values
array<string, string|array<int, string>>|null —The canonical urlKey => fieldPath map, or null when $sortable is null.
parseFilterSegment()
Parse a single path segment from hierarchical configuration.
parseFilterSegment(string $segment, array<string|int, mixed> $filters[, array<string|int, mixed> $edges = [] ][, array<string|int, mixed> $joins = [] ][, array<string|int, mixed> $parentPath = [] ][, ContainerInterface|null $container = null ]) : FilterPath|null
This function analyzes a filter path segment and determines its type (simple field, document, array expansion, edge, or join). It validates the segment against the configuration and, for edges and joins, resolves nested relations from target models.
Key Features:
- Validates array notation consistency (e.g.,
employee[*]must be of type EDGES/JOINS/ARRAY_EXPANSION) - Resolves nested edges/joins from target models for multi-level traversals
- Supports both explicit relation references (via AQL::RELATION) and implicit (segment key)
- Accumulates full path for better error reporting
Nested Relations Resolution: For edges, the function:
- Gets the edge model from the container
- Determines target model based on traversal direction (INBOUND → from, OUTBOUND → to)
- Extracts edges/joins from target model
- Merges with explicit nested relations from edge configuration
For joins, the function:
- Gets the join target model from the container
- Extracts edges/joins from target model
Parameters
- $segment : string
-
Current path segment (e.g., "employee[*]", "address", "workLocation")
- $filters : array<string|int, mixed>
-
Current level AQL::FILTERS configuration
- $edges : array<string|int, mixed> = []
-
Available edges configuration at current level
- $joins : array<string|int, mixed> = []
-
Available joins configuration at current level
- $parentPath : array<string|int, mixed> = []
-
Accumulated path from parent segments for error reporting
- $container : ContainerInterface|null = null
-
DI container for resolving target models and their relations
Tags
Return values
FilterPath|null —Parsed segment information with nested relations, or null if segment is not allowed
prepareRelationDefinition()
Resolves, permission-gates and stamps a relation definition (edge or join) before it is handed to a relation builder — the common preamble shared by the `Filter::EDGE(S)` and `Filter::JOIN(S)` branches of {@see buildVariables()}.
prepareRelationDefinition(array<string|int, mixed>|null $registry, int|string $key, array<string|int, mixed> $field, mixed $unique, array<string|int, mixed> $init) : array<string|int, mixed>|null
Steps:
- Look the definition up in
$registry($edgesor$joins) by$key; a string value is a shortcut reference to another entry, dereferenced once. - Return
nullwhen nothing resolves (the caller skips the relation). - Two composable gates (logical AND):
Field::REQUIRESon the FIELDS entry ($field) andAQL::REQUIRESon the definition itself. Either one denied returnsnull, dropping the relation from both theLETwalk and — mirrored upstream by authorizeRelationFields() — the projection. - Stamp
Field::UNIQUEonto the definition (the caller-provided AQL variable name override).
A Filter::EDGES_COUNT is gated the same way: its count LET is dropped only
when the entry (or the shared definition) declares a requirement — otherwise the
cardinality stays visible, as knowing it is rarely a leak.
Parameters
- $registry : array<string|int, mixed>|null
-
The relation registry —
$edgesor$joins. - $key : int|string
-
The field key naming the relation.
- $field : array<string|int, mixed>
-
The FIELDS entry (read for
Field::REQUIRES/ gating). - $unique : mixed
-
The AQL variable name override stamped as
Field::UNIQUE. - $init : array<string|int, mixed>
-
The request-level init array (reads
Arango::AUTHORIZER).
Tags
Return values
array<string|int, mixed>|null —The prepared definition, or null when it does not resolve or is denied.
sortRelationVariable()
Generates the internal AQL 'SORT' clause for a relation sub-query (edge or join).
sortRelationVariable(array<string|int, mixed>|string|null $definition, string $sortRef, string $defaultRef[, string $defaultProperty = Schema::CREATED ]) : string
This helper interprets a flexible sort definition and constructs the appropriate AQL 'SORT' expression, shared by sortEdgeVariable() and sortJoinVariable(). The only thing those two wrappers customize is which variable reference carries the sort property and which one carries the default fallback property:
- An explicit sort property (string definition, or
AQL::SORTin an array) is sorted on$sortRef(the vertex for an edge, the join document for a join). - The fallback (no
AQL::SORTprovided) sorts by$defaultPropertyon$defaultRefin DESC order (the edge for an edge relation, the same join document for a join).
Parameters
- $definition : array<string|int, mixed>|string|null
-
The sort configuration (a
$definitionarray, a legacy string, or null). - $sortRef : string
-
The AQL variable reference carrying an explicit sort property.
- $defaultRef : string
-
The AQL variable reference carrying the fallback default property.
- $defaultProperty : string = Schema::CREATED
-
The fallback property to sort by (default: 'created').
Tags
Return values
string —The generated AQL 'SORT' clause.
vertexID()
Resolves a fully-qualified vertex ID for an ArangoDB edge.
vertexID(string|null $vertexKey[, Documents|string|null $collection = null ]) : string|null
Converts a vertex key into a complete ArangoDB vertex ID by optionally prefixing it with a collection name.
This is useful when preparing
_from or _to fields in edge queries, ensuring the correct
collection/key format.
Behavior
- If
$vertexKeyisnull, the function returnsnull. - If
$collectionis aDocumentsinstance with acollectionproperty, the returned ID will be prefixed with this collection. - If
$collectionis a string, it will be used directly as the collection prefix. - If
$collectionisnull, the function returns the raw$vertexKey.
Parameters
- $vertexKey : string|null
-
The vertex key (document _key) or null.
- $collection : Documents|string|null = null
-
Optional collection prefix, either a string or a Documents instance.
Tags
Return values
string|null —Returns the prefixed vertex ID if a collection is provided, otherwise the original key, or null if $vertexKey is null.