Oihana PHP Arango

edges

Table of Contents

Functions

assertEdges()  : void
Ensures that a given value is an instance of {@see Edges}.
buildEdgeCountVariable()  : string|null
Generates a string of multiple AQL 'LET' statements for calculate the number of edges of a specific document.
buildEdgeGroupExpression()  : string
Compiles the grouping dimension of a **relation**: the value a document is grouped by when that value lives on the document at the other end of an edge.
buildEdgeSubquery()  : string
Builds the inner AQL edge traversal sub-query — everything an edge `LET` wraps, already enclosed in parentheses but WITHOUT the leading `LET name =`.
buildEdgeVariable()  : string
Builds a single AQL 'LET' subquery string for a specific edge relation.
buildEdgesVariables()  : string
Generates a string of multiple AQL 'LET' statements for all defined edge variables.
buildPolymorphicEdgeVariable()  : string
Builds a single AQL 'LET' subquery for a *polymorphic* edge — an edge whose traversed collection is chosen at query time from a discriminator field of the start vertex (the parent document).
edgeTraversalOptions()  : array<string|int, mixed>
The traversal options every edge relation is walked with — breadth-first, and each vertex visited **once globally**.
getEdges()  : Edges|null
Retrieves an {@see Edges} instance from various types of input definitions.
resolveEdgeContext()  : Edges, 1: string, 2: string}
Resolves the common edge-traversal context shared by the edge variable builders.
resolveEdgeDepthRange()  : array{0: int|null, 1: int|null}
Resolves the traversal depth range declared by an edge definition — the `[ $minDepth , $maxDepth ]` pair handed to {@see \oihana\arango\db\operations\aqlTraversal()}.
resolveEdgeDirection()  : string
Resolves the traversal direction of an edge relation: which way its edges are followed from the document that declares them.
resolveEdges()  : void
Resolves and initializes internal edge model definitions from a dependency container.
resolveEdgeTarget()  : Documents|null
Resolves the vertex model a traversal reaches — the model whose fields are projected, whose `Field::REQUIRES` gate the projection, and whose own relations the walk continues through.
resolveEdgeVertexScope()  : array{0: string|null, 1: string|null}
Compiles the **row scope** an edge definition declares over the vertices it traverses — the `[ $filter , $prune ]` pair, both already AQL predicate strings targeting `$vertexRef`, or `null` when nothing is declared.
sortEdgeVariable()  : string
Generates the internal AQL 'SORT' clause for an edge variable subquery.

Functions

assertEdges()

Ensures that a given value is an instance of {@see Edges}.

assertEdges(mixed $value) : void

This helper function acts as a runtime assertion to validate type safety. If the provided value is not an instance of Edges, 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 Edges instance.

Tags
throws
UnexpectedValueException

If the provided value is not an instance of Edges.

example
use oihana\arango\models\helpers\assertEdges;
use oihana\arango\models\Edges;

$edges = new Edges();

// ✅ Valid: no exception thrown
assertEdges( $edges );

// ❌ Invalid: throws UnexpectedValueException
assertEdges( 'not an edges instance' );
// → UnexpectedValueException: The value property must be an instance of Edges.
author

Marc Alcaraz (eKameleon)

version
1.0.0

buildEdgeCountVariable()

Generates a string of multiple AQL 'LET' statements for calculate the number of edges of a specific document.

buildEdgeCountVariable(string|null $name[, array<string|int, mixed> $definition = [] ][, string $startVertex = AQL::DOC ][, ContainerInterface|null $container = null ]) : string|null

The count must answer the same question as the list. A count and a Filter::EDGES projection normally share one definition — the registry's string shortcut ('descendantsCount' => 'descendants') is the idiomatic way to say so — therefore every part of that declaration shaping which vertices are walked has to be read here exactly as buildEdgeSubquery() reads it. Three did not use to be, each producing a number the rows contradicted:

Declaration List Count, before
AQL::MAX_DEPTH => 5 over a ─┬─ b ── c / └─ e ── f [ b , c , e , f ] → 4 2 (direct children only)
depth 1 with a duplicated a → c edge [ b , c ] → 2 3 (counted twice)
1..5 over the diamond a → b → d / a → c → d [ b , c , d ] → 3 6

All three are now read through the shared helpers — resolveEdgeDepthRange(), resolveEdgeVertexScope() and edgeTraversalOptions() — so the count and the list cannot drift again. AQL::WHERE filters the counted loop and AQL::PRUNE stops it, both compiled against the inner vertex.

Nothing is emitted for a key that is not declared, so a definition without depth, scope or prune produces the historical single-level count — plus the traversal options, which is the one intentional change to the emitted AQL of an existing definition (and the one that makes a duplicated edge stop being counted twice).

Parameters
$name : string|null
$definition : array<string|int, mixed> = []
$startVertex : string = AQL::DOC
$container : ContainerInterface|null = null
Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
ReflectionException
BindException
ConstantException
UnexpectedValueException

If AQL::MIN_DEPTH is declared without AQL::MAX_DEPTH, or AQL::PRUNE => true has no AQL::WHERE to negate.

UnsupportedOperationException

If an AQL::WHERE / AQL::PRUNE condition descriptor is malformed.

ValidationException

If an AQL::WHERE / AQL::PRUNE condition attribute name is unsafe.

Return values
string|null

buildEdgeGroupExpression()

Compiles the grouping dimension of a **relation**: the value a document is grouped by when that value lives on the document at the other end of an edge.

buildEdgeGroupExpression(string $key, array<string|int, mixed> $definition, array<string|int, mixed>|string $path[, string $startVertex = AQL::DOC ][, ContainerInterface|null $container = null ]) : string

The expression is a sub-query written inline in the COLLECT, and it has to be, because a grouped query never projects. The LET a projected relation emits — the one a relational sort names — is produced by returnFields(), which a grouped query does not call: doc is consumed by the COLLECT, so there is nothing to project and no variable to reach for. The dimension therefore carries its own traversal:

COLLECT author = FIRST( FOR author_v IN OUTBOUND doc articles_authors RETURN author_v.name )
AGGREGATE total = SUM( doc.amount )

FIRST() is what makes the dimension a scalar, and it is why only a singular relation may be grouped on. The two alternatives were measured, and neither is acceptable:

  • keeping the sub-query as an array groups by the combination — three buckets (["Alice"], ["Alice","Zoe"], ["Zoe"]) for two authors;
  • unwinding the relation with a FOR before the COLLECT duplicates the document once per related vertex, which inflates every other aggregate of the same COLLECT: over three documents worth 10, 20 and 30, a SUM answers 70 where the truth is 60, because the two-author document is counted twice.

The declaration is read through the same shared doors as the list — the depth range, the row scope (AQL::WHERE / AQL::PRUNE) and the traversal options — so a grouped dimension describes exactly the relation the list projects. Read differently, it would put a label next to rows that contradict it.

Parameters
$key : string

The dimension key; names the traversal variable (<key>_v).

$definition : array<string|int, mixed>

The edge definition (AQL::MODEL, AQL::DIRECTION, …).

$path : array<string|int, mixed>|string

The field of the related document carrying the value.

$startVertex : string = AQL::DOC

The document the traversal departs from.

$container : ContainerInterface|null = null

Resolves an AQL::MODEL given by container id.

Tags
throws
BindException
ConstantException
ContainerExceptionInterface
NotFoundExceptionInterface
ReflectionException
UnsupportedOperationException
ValidationException
since
1.7.0
author

Marc Alcaraz

Return values
string —

The FIRST( FOR … RETURN … ) expression.

buildEdgeSubquery()

Builds the inner AQL edge traversal sub-query — everything an edge `LET` wraps, already enclosed in parentheses but WITHOUT the leading `LET name =`.

buildEdgeSubquery(string|null $name[, array<string|int, mixed> $definition = [] ][, string $startVertex = AQL::DOC ][, ContainerInterface|null $container = null ][, array<string|int, mixed> $init = [] ][, array<string|int, mixed> $extraConditions = [] ]) : string

The returned string is the parenthesized traversal:

( FOR vertex, edge IN OUTBOUND doc edge_collection [<nested LETs>] [FILTER …] [SORT …] RETURN … )

buildEdgeVariable() prefixes it with LET name = for a regular edge, while buildPolymorphicEdgeVariable() wraps several such sub-queries into a single APPEND( ( … ) , ( … ) ) array so the traversed edge collection can vary with a discriminator field of the start vertex.

Extracting this body from buildEdgeVariable() lets a polymorphic edge reuse the whole traversal machinery (direction, depth, path metadata, skinning, nested edges / joins, definition-level gating) per branch. The only addition over the historical logic is $extraConditions: a list of ready-made AQL predicates (typically the discriminator guard on the start vertex) emitted as a FILTER right after the traversal. When empty, no FILTER is emitted, so the output is byte-for-byte identical to the legacy edge sub-query.

A definition may also declare AQL::WHERE — a condition in the Field::WHEN grammar, compiled against the traversed vertex and appended to $extraConditions. It restricts WHICH vertices the relation projects, wherever the definition is used, so a consumer masking part of a collection is not contradicted by the relation of a served document:

( FOR vertex, edge IN OUTBOUND doc coll FILTER vertex.id NOT IN @hiddenTerms … )

Its value may hold an aqlBindRef(), so the retained set is decided at query time; the contract is the one Field::WHERE already carries on a Filter::MAP (a bind bound to [] retains nothing, an absent bind fails the query — never "no filter"). It composes with the polymorphic guard rather than replacing it, and is orthogonal to AQL::REQUIRES / Field::REQUIRES, which decide whether the relation is projected at all.

AQL::WHERE filters the traversal's OUTPUT; on a ranged relation (AQL::MAX_DEPTH) the walk still descends through a masked vertex, so its descendants keep being projected. AQL::PRUNE stops the walk itself — true reuses the AQL::WHERE predicate negated ("hide it and its descent"), or a condition of its own when stopping is not hiding. The two are emitted together because PRUNE stops after visiting: the vertex it stops on is still returned unless the FILTER removes it.

Parameters
$name : string|null

The logical name of the relation (used to skin the projection).

$definition : array<string|int, mixed> = []

Configuration array for the traversal — same keys as buildEdgeVariable() (AQL::MODEL, AQL::DIRECTION, AQL::EDGES, AQL::JOINS, AQL::SKIN, AQL::MAX_DEPTH, AQL::WITH_PATH, AQL::WHERE, AQL::PRUNE, Arango::PROPERTY, …).

$startVertex : string = AQL::DOC

The AQL variable name of the starting vertex (default 'doc').

$container : ContainerInterface|null = null

The DI container used to resolve models.

$init : array<string|int, mixed> = []

Optional associative array used for variable initialization.

$extraConditions : array<string|int, mixed> = []

Ready-made AQL predicate strings emitted as a FILTER after the traversal (e.g. the discriminator guard of a polymorphic edge). Empty → no FILTER emitted (byte-identical output).

Tags
throws
Exception

If the traversal direction is invalid.

ContainerExceptionInterface

If the Edges model cannot be resolved from the container.

NotFoundExceptionInterface

If the Edges model cannot be resolved from the container.

ReflectionException
UnexpectedValueException

If $name is empty, the model is invalid, the collection is not set, or AQL::DIRECTION => Traversal::ANY is declared on a relation whose two ends are different collections (no single model to project), or AQL::PRUNE => true has no AQL::WHERE condition to negate.

UnsupportedOperationException

If an AQL::WHERE / AQL::PRUNE condition descriptor is malformed.

ValidationException

If an AQL::WHERE / AQL::PRUNE condition attribute name is unsafe.

Return values
string —

The parenthesized traversal sub-query (no leading LET name =).

buildEdgeVariable()

Builds a single AQL 'LET' subquery string for a specific edge relation.

buildEdgeVariable(string|null $name[, array<string|int, mixed> $definition = [] ][, string $startVertex = AQL::DOC ][, ContainerInterface|null $container = null ][, array<string|int, mixed> $init = [] ]) : string

This method generates a complete traversal subquery, enclosed in parentheses, which is assigned to a 'LET' variable. It handles direction, filtering, sorting, and shaping of the results.

The traversal body itself is produced by buildEdgeSubquery(); this wrapper only resolves the LET variable name and prefixes the body.

Example output: LET myFriends = ( FOR v, e IN OUTBOUND 'users/123' friends_edge ... RETURN v.name )

Parameters
$name : string|null

The logical name for this variable (e.g., 'friends', 'comments'). This is used as the AQL 'LET' variable name.

$definition : array<string|int, mixed> = []

Configuration array for the traversal. Expected keys:

  • AQL::MODEL: (string) The class name of the Edges model.
  • AQL::DIRECTION: (string|null) Traversal direction (OUTBOUND, INBOUND).
  • AQL::UNIQUE: (string|null) Optional AQL variable name, overrides $name.
  • AQL::EDGES: (array) Further edge definitions for nested queries.
  • AQL::JOINS: (array) Join definitions for the target model.
  • AQL::SKIN: (string|null) A 'skin' name to select specific fields.
  • AQL::SORT: (string|array|null) Sort definition (see getSortEdgeVariableExpression).
  • Arango::SOURCE: (string|null) Optional absolute path, read from the start vertex, holding the traversal start-vertex _id — the traversal then departs from doc.<source> instead of doc. The value MUST be a full document _id (e.g. providers/123), not a bare _key.
$startVertex : string = AQL::DOC

The AQL variable name of the starting vertex (default 'doc').

$container : ContainerInterface|null = null

The DI Container reference.

$init : array<string|int, mixed> = []

Optional associative array definitions.

Tags
throws
Exception

If Traversal direction is invalid.

ContainerExceptionInterface

If the Edges model cannot be resolved from the container.

NotFoundExceptionInterface

If the Edges model cannot be resolved from the container.

ReflectionException
UnexpectedValueException

If $name is empty, the model is invalid, or the collection is not set.

Return values
string —

The complete AQL 'LET' statement.

buildEdgesVariables()

Generates a string of multiple AQL 'LET' statements for all defined edge variables.

buildEdgesVariables([array<string|int, mixed> &$variables = [] ][, array<string|int, mixed> $definitions = [] ][, string $startVertex = AQL::DOC ][, ContainerInterface|null $container = null ][, array<string|int, mixed> $init = [] ]) : string

This is a convenience method that iterates over a list of definitions and calls getEdgeVariable() for each one, concatenating the results.

Definition-level gating: a definition declaring AQL::REQUIRES that the request-scoped authorizer denies is skipped — its LET is not emitted.

Parameters
$variables : array<string|int, mixed> = []

The variables list reference to fill.

$definitions : array<string|int, mixed> = []

An associative array of edge definitions [ $name => $definition ].

$startVertex : string = AQL::DOC

The default AQL document reference (start vertex) for all traversals.

$container : ContainerInterface|null = null

The DI Container reference.

$init : array<string|int, mixed> = []

Optional associative array definition.

Tags
throws
ContainerExceptionInterface

| NotFoundExceptionInterface

ReflectionException
Exception
Return values
string —

A string containing all generated AQL 'LET' statements, or an empty string if none.

buildPolymorphicEdgeVariable()

Builds a single AQL 'LET' subquery for a *polymorphic* edge — an edge whose traversed collection is chosen at query time from a discriminator field of the start vertex (the parent document).

buildPolymorphicEdgeVariable(string|null $name[, array<string|int, mixed> $definition = [] ][, string $startVertex = AQL::DOC ][, ContainerInterface|null $container = null ][, array<string|int, mixed> $init = [] ]) : string

The whole branch machinery (guarding, per-branch permission gating, the FALLBACK branch, the APPEND combine) lives in the shared buildPolymorphicRelationVariable(); this function only supplies the per-branch builder, which delegates to buildEdgeSubquery() (already a parenthesized traversal). Each branch is a full edge definition, so it may declare its own AQL::DIRECTION, AQL::MAX_DEPTH, etc.

The resulting LET always holds an array — exactly like a regular edge — so the projection layer (aqlFieldObject()) unwraps it with FIRST() for a Filter::EDGE or keeps the whole array for a Filter::EDGES.

Example output (two branches):

LET rel = APPEND(
  ( FOR vertex, edge IN OUTBOUND doc warehouse_edges
      FILTER doc.kind == "warehouse"
      RETURN { _key: vertex._key, name: vertex.name } ) ,
  ( FOR vertex, edge IN OUTBOUND doc company_edges
      FILTER doc.kind == "company"
      RETURN { _key: vertex._key, name: vertex.name } )
)
Parameters
$name : string|null

The edge field name — also the default LET variable name.

$definition : array<string|int, mixed> = []

The polymorphic edge definition. Keys:

  • Arango::DISCRIMINATOR (string) Start-vertex field path deciding the branch (required).
  • Arango::MAP (array) Non-empty type => edge-definition table (required).
  • Arango::UNIQUE (string|null) Optional LET variable name, overrides $name.
  • Arango::SOURCE (string|null) Optional absolute path holding the traversal start-vertex _id; the traversal then departs from doc.<source> while the discriminator STAYS resolved on the parent document (doc.<discriminator>).
  • Arango::FALLBACK (array|null) Edge definition for unmatched discriminator values (null = none).
$startVertex : string = AQL::DOC

The AQL variable name of the start vertex.

$container : ContainerInterface|null = null

Optional DI container used to resolve branch models.

$init : array<string|int, mixed> = []

Optional associative array used for variable initialization.

Tags
throws
Exception

If a branch traversal cannot be built.

ContainerExceptionInterface

If a branch model cannot be resolved from the container.

NotFoundExceptionInterface

If a branch model cannot be found in the container.

ReflectionException
UnexpectedValueException

If $name is empty, or the definition lacks a non-empty Arango::MAP / Arango::DISCRIMINATOR.

since
1.0.0
author

Marc Alcaraz

Return values
string —

The complete AQL 'LET' statement.

edgeTraversalOptions()

The traversal options every edge relation is walked with — breadth-first, and each vertex visited **once globally**.

edgeTraversalOptions() : array<string|int, mixed>

uniqueVertices: global is not cosmetic, it decides how many rows come back. Two shapes make a vertex reachable more than once: a diamond (a → b → d plus a → c → d) and a plainly duplicated edge (a → c created twice). ArangoDB's default (uniqueVertices: none) yields such a vertex once per path.

The list always passed these options; the count passed none — so the two disagreed on the same data, measured live on the shapes above:

Declaration List Count, before
depth 1, one duplicated edge [ b , c ] → 2 3
1..5 over the diamond [ b , c , d ] → 3 6

Both builders now read the options here, so a count can no longer over-count rows the list de-duplicated.

Tags
since
1.0.0
author

Marc Alcaraz

Return values
array<string|int, mixed> —

The AQL::OPTIONS payload of an edge traversal.

getEdges()

Retrieves an {@see Edges} instance from various types of input definitions.

getEdges([mixed $definition = null ][, ContainerInterface|null $container = null ][, string $key = Arango::EDGES ][, Edges|null $default = null ]) : Edges|null

This helper function resolves an Edges 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 $definition is an Edges instance, it is returned as-is.
  • If $definition is an array, the function looks for the Arango::EDGES key.
  • If $definition is a non-empty string and $container contains a service with that name, the corresponding service is fetched.
  • If none of the above conditions are met, the $default value is returned.
  • Any other value — a number, a boolean, an object of another class — answers the $default value : the function accepts anything, so that a caller can hand it an untyped configuration entry as is.
Parameters
$definition : mixed = null

Input definition that may represent an Edges instance, 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::EDGES

Array key to look for when $definition is an array

$default : Edges|null = null

Default Edges instance to return if resolution fails.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
example
use oihana\arango\models\helpers\getEdges;
use oihana\arango\models\Edges;
use oihana\arango\enums\Arango;
use Psr\Container\ContainerInterface;

$edges = new Edges(['_from' => 'users/1', '_to' => 'posts/5']);

// Example 1: Direct instance
$result = getEdges($edges);
// → returns the same $edges instance

// Example 2: From array definition
$result = getEdges([Arango::EDGES => $edges]);
// → returns the $edges instance from the array

// Example 3: From container service name
$container->method('has')->willReturn(true);
$container->method('get')->willReturn($edges);
$result = getEdges('my.edges.service', $container);
// → returns the $edges instance resolved from the container

// Example 4: With default fallback
$default = new Edges(['_from' => 'fallback/A', '_to' => 'fallback/B']);
$result = getEdges(null, null, $default);
// → returns $default
author

Marc Alcaraz (eKameleon)

version
1.0.0
Return values
Edges|null —

Returns the resolved Edges instance or the default value if not found.

resolveEdgeContext()

Resolves the common edge-traversal context shared by the edge variable builders.

resolveEdgeContext([array<string|int, mixed> $definition = [] ][, ContainerInterface|null $container = null ]) : Edges, 1: string, 2: string}

This helper centralizes the preamble duplicated by buildEdgeVariable() and buildEdgeCountVariable(): it resolves the Edges model from the definition (via getEdges()), validates it, reads its non-empty collection name and normalizes the traversal direction.

Parameters
$definition : array<string|int, mixed> = []

The relation definition. Expected keys:

  • AQL::MODEL : the Edges model (instance, array or container id).
  • AQL::DIRECTION : the traversal direction (defaults to Traversal::OUTBOUND). Resolved through resolveEdgeDirection(), so an unknown keyword is refused rather than folded back on the default.
$container : ContainerInterface|null = null

Optional DI container used to resolve the model.

Tags
throws
ConstantException

If the declared traversal direction is not a Traversal keyword.

ContainerExceptionInterface
NotFoundExceptionInterface
UnexpectedValueException

If the model is not an Edges instance or its collection is empty.

Return values
Edges, 1: string, 2: string} —

A triplet [ $model , $edgeCollection , $direction ].

resolveEdgeDepthRange()

Resolves the traversal depth range declared by an edge definition — the `[ $minDepth , $maxDepth ]` pair handed to {@see \oihana\arango\db\operations\aqlTraversal()}.

resolveEdgeDepthRange(array<string|int, mixed> $definition) : array{0: int|null, 1: int|null}

A self-referential relation (a thesaurus, a category tree, an org chart) can project several levels in a single traversal through AQL::MAX_DEPTH. The rules:

  • Neither declared → [ null , null ], the traversal stays at depth 1 and the emitted AQL is byte-for-byte the un-ranged one.
  • AQL::MAX_DEPTH alone defaults the lower bound to 1 — the natural 1..N.
  • AQL::MIN_DEPTH alone is refused. ArangoDB requires a bounded range, and an unbounded traversal over a self-referential edge risks a runaway cycle.

This lives in its own helper because the list and the count must read the declaration the same way. They did not: buildEdgeCountVariable() ignored the range entirely, so a definition declaring AQL::MAX_DEPTH => 5 produced a list of the whole descent beside a count of the direct children — measured live as 4 rows under a count saying 2. A shared door makes that divergence impossible to reintroduce, including the refusal rule.

Parameters
$definition : array<string|int, mixed>

The edge definition (reads AQL::MIN_DEPTH / AQL::MAX_DEPTH).

Tags
throws
UnexpectedValueException

If AQL::MIN_DEPTH is declared without AQL::MAX_DEPTH.

since
1.0.0
author

Marc Alcaraz

Return values
array{0: int|null, 1: int|null} —

The [ $minDepth , $maxDepth ] pair.

resolveEdgeDirection()

Resolves the traversal direction of an edge relation: which way its edges are followed from the document that declares them.

resolveEdgeDirection(array<string|int, mixed> $definition) : string

The direction is not a detail of the AQL — it decides whether the traversal finds anything at all. A relation whose edges leave the document (doc is the _from) reaches its vertices OUTBOUND; one whose edges point at it reaches them INBOUND. Follow the wrong way and the traversal is perfectly valid and matches nothing, so the relation projects an empty array in 200 without a word.

Two rules, and each is deliberate:

  • the default is Traversal::OUTBOUND, which every edge surface of the library already applies — the projection, the count, the grouped dimension, the hierarchical filters, the tree helpers and the write path;
  • an unknown value is refused, never quietly replaced. Traversal::get() was used here before, and it falls back on its default when it does not recognise a value: a mistyped 'OUTBOUD' compiled to OUTBOUND without a word, so a typo on an inbound relation produced exactly the empty projection the option exists to prevent. Traversal::validate() answers instead.

A malformed declaration is a server-side mistake, so it is loud. A key coming from the request is a different matter and keeps being dropped in silence — the frontier the library draws everywhere else.

Parameters
$definition : array<string|int, mixed>

The relation definition. Reads AQL::DIRECTION.

Tags
throws
ConstantException

When the declared direction is not a Traversal keyword.

example
use function oihana\arango\models\helpers\edges\resolveEdgeDirection;

resolveEdgeDirection( [] ) ;                                        // 'OUTBOUND' (the default)
resolveEdgeDirection( [ AQL::DIRECTION => Traversal::INBOUND ] ) ;  // 'INBOUND'
resolveEdgeDirection( [ AQL::DIRECTION => 'OUTBOUD' ] ) ;           // throws
since
1.7.0
author

Marc Alcaraz

Return values
string —

The validated direction keyword.

resolveEdges()

Resolves and initializes internal edge model definitions from a dependency container.

resolveEdges([array<string|int, mixed>|null &$edges = [] ][, Container|null $container = null ]) : void

This helper ensures that all edge-related dependencies defined in the model configuration (under AQL::EDGES) are properly instantiated in the DI container.

It supports multiple edge definition formats:

  • Associative arrays mapping a property to its edge configuration.
  • Indexed arrays containing simple string references to container entries.
  • The special key AQL::RESOLVE (or __resolve__) for one-shot dependency resolution, used to pre-load edge models in memory without explicit configuration.

Typical usage:

resolveEdges( $model[AQL::EDGES] ?? [], $container );

Behavior:

  • Each string identifier found in indexed or AQL::RESOLVE arrays is resolved through the container.
  • Associative definitions are analyzed to resolve their AQL::MODEL entry if it refers to a container ID.
  • Existing Edges instances are left untouched.
Parameters
$edges : array<string|int, mixed>|null = []

The array of edge definitions to resolve (may be associative or indexed).

$container : Container|null = null

The DI container used to resolve Edges references.

Tags
throws
DependencyException

If a dependency cannot be loaded by the DI container.

NotFoundException

If a referenced container entry is not found.

ContainerExceptionInterface

If the container encounters a general error while resolving.

NotFoundExceptionInterface

If a referenced entry is missing in a PSR-11 container.

resolveEdgeTarget()

Resolves the vertex model a traversal reaches — the model whose fields are projected, whose `Field::REQUIRES` gate the projection, and whose own relations the walk continues through.

resolveEdgeTarget(Edges $model, string $direction) : Documents|null

An edge has two ends, and the direction says which one the walk lands on: INBOUND reaches the _from model, OUTBOUND the _to model. That much was already written, three times, as a binary ternary — and a binary ternary has no room for the third keyword:

$documents = $direction == Traversal::INBOUND ? $model->from : $model->to ; // ANY silently lands on `to`

Traversal::ANY reaches both ends, so on a heterogeneous relation no single model describes what comes back: the vertices of the far side were projected with the near side's fields and, worse, gated by the near side's permissions — a field masked on one collection served through the other. It is therefore refused rather than resolved to half of it.

ANY stays perfectly valid where it is meaningful — a self-referential relation, both ends on the same collection (user_follows, a thesaurus), which is the case it exists for. It then keeps resolving to the _to end, the one the ternary picked, so an unambiguous declaration compiles byte for byte as before. Both ends being unwired is left alone too: the model declares no vertex model at all, and the callers that tolerate that keep their behaviour.

Parameters
$model : Edges

The edge model carrying the two vertex ends.

$direction : string

A validated Traversal keyword, as returned by resolveEdgeDirection().

Tags
throws
UnexpectedValueException

When ANY cannot designate a single reached model.

example
use function oihana\arango\models\helpers\edges\resolveEdgeTarget;

resolveEdgeTarget( $articlesAuthors , Traversal::OUTBOUND ) ; // the `authors` model
resolveEdgeTarget( $articlesAuthors , Traversal::INBOUND  ) ; // the `articles` model
resolveEdgeTarget( $userFollows     , Traversal::ANY      ) ; // the `users` model (both ends)
resolveEdgeTarget( $articlesAuthors , Traversal::ANY      ) ; // throws — two different ends
since
1.7.0
author

Marc Alcaraz

Return values
Documents|null —

The reached vertex model, or null when that end is not wired.

resolveEdgeVertexScope()

Compiles the **row scope** an edge definition declares over the vertices it traverses — the `[ $filter , $prune ]` pair, both already AQL predicate strings targeting `$vertexRef`, or `null` when nothing is declared.

resolveEdgeVertexScope(array<string|int, mixed> $definition, string $vertexRef) : array{0: string|null, 1: string|null}

Two keys, answering two different questions:

  • AQL::WHERE — WHICH vertices the relation yields. Compiled from the Field::WHEN grammar, so a value may hold an aqlBindRef() and the retained set is decided at query time (a bind bound to [] retains nothing, an absent bind fails the query — never "no filter"). Emitted as a FILTER after the traversal.
  • AQL::PRUNE — whether the walk STOPS there. A FILTER only filters the traversal's output: on a ranged relation the walk still descends through a masked vertex, so its descendants keep being projected. true reuses the AQL::WHERE predicate negated ("hide it and its descent"); a condition of its own covers the case where stopping is not hiding. A condition is compiled rather than read as a boolean — anything written there is truthy, so it would otherwise be silently swapped for the negated AQL::WHERE. false means OFF, like an absent key, so a AQL::PRUNE => $flag toggle works both ways.

The two are emitted together and neither replaces the other: PRUNE stops the walk after visiting, so the vertex it stops on is still returned unless the FILTER removes it.

This lives in its own helper because the list and the count must read the declaration the same way — a count that scoped differently from the list would announce a number the rows contradict, which is the whole class of bug these keys exist to close.

Parameters
$definition : array<string|int, mixed>

The edge definition (reads AQL::WHERE / AQL::PRUNE).

$vertexRef : string

The AQL variable of the traversed vertex both predicates target.

Tags
throws
UnexpectedValueException

If AQL::PRUNE => true has no AQL::WHERE to negate.

UnsupportedOperationException

If a condition descriptor is malformed.

ValidationException

If a condition attribute name is unsafe.

since
1.0.0
author

Marc Alcaraz

Return values
array{0: string|null, 1: string|null} —

The [ $filter , $prune ] predicates.

sortEdgeVariable()

Generates the internal AQL 'SORT' clause for an edge variable subquery.

sortEdgeVariable(array<string|int, mixed>|string|null $definition[, string $vertexRef = AQL::VERTEX ][, string $edgeRef = AQL::EDGE ][, string $defaultProperty = Schema::CREATED ]) : string

This helper method interprets a flexible sort definition and constructs the appropriate AQL 'SORT' expression.

  • If $definition is an array: Looks for AQL::SORT (property) and AQL::ORDER (ASC/DESC) and sorts by the vertex property.
  • If $definition is a string (legacy): Sorts by that string as the property on the vertex in ASC order.
  • If $definition is null (or AQL::SORT is not set in the array): Sorts by the $defaultProperty (e.g., 'created') on the edge in DESC order.
Parameters
$definition : array<string|int, mixed>|string|null

The sort configuration. Typically the $definition array from getEdgeVariable.

$vertexRef : string = AQL::VERTEX

The internal AQL vertex variable reference.

$edgeRef : string = AQL::EDGE

The internal AQL edge variable reference.

$defaultProperty : string = Schema::CREATED

The fallback property to sort by (default: 'created').

Tags
example

Assume the following constant values for the examples:

  • AQL::SORT = 'sort'
  • AQL::ORDER = 'order'
  • Order::DESC = 'DESC'
  • Order::ASC = 'ASC'
  • Schema::CREATED = 'created'
  • AQL::EDGE_PREFIX = 'e_'
  • AQL::VERTEX_PREFIX = 'v_'

Case 1: Default sort (null definition)

Sorts by 'created' on the edge (e_) in DESC order.

echo sortEdgeVariable( null , 'friends_rel');
// Output: "SORT e_friends_rel.created DESC"

Case 2: Legacy string sort (string definition)

Sorts by 'name' on the vertex (v_) in ASC order.

echo sortEdgeVariable( 'name' , 'friends_rel');
// Output: "SORT v_friends_rel.name ASC"

Case 3: Array definition (DESC)

Sorts by 'age' on the vertex (v_) in DESC order.

$definition =
[
    AQL::SORT  => 'age',
    AQL::ORDER => Order::DESC
];
echo sortEdgeVariable( $definition, 'friends_rel' );
// Output: "SORT v_friends_rel.age DESC"

Case 4: Array definition (ASC)

Sorts by 'lastName' on the vertex (v_) in ASC order.

$definition = [ AQL::SORT => 'lastName' ]; // AQL::ORDER defaults to ASC
echo sortEdgeVariable( $definition , 'friends_rel');
// Output: "SORT v_friends_rel.lastName ASC"

Case 5: Array definition missing 'sort' key ---

Falls back to default sort (Case 1).

$def5 = [ AQL::ORDER => Order::DESC ];
echo sortEdgeVariable($def5, 'friends_rel');
// Output: "SORT e_friends_rel.created DESC"
* ```
Return values
string —

The generated AQL 'SORT' clause (e.g., "SORT v_myVar_collectionName.name ASC").

On this page

Search results