Oihana PHP Arango

GroupTrait uses trait:short

Translates the high-level {@see Arango::GROUP} spec ({@see Group}) into the raw `COLLECT` spec consumed by {@see \oihana\arango\db\operations\aqlCollect()} and {@see \oihana\arango\db\operations\aqlCollectReturn()} in {@see \oihana\arango\models\traits\queries\ListQueryTrait::buildListQuery()}.

It is the COLLECT counterpart of FacetTrait, reusing the same engines:

  • FacetAggregator for the aggregate functions (sum→SUM, …),
  • the alt engine (alterExpression()) for grouping-key transforms,
  • the key() helper to prefix fields with the document reference.

A raw Arango::COLLECT spec is passed through untouched when no Arango::GROUP is supplied, so power users keep full control.

One gate per half, and they do not default alike

by is fail-closed through GroupTrait::$groupable: without a declared whitelist, nothing is groupable. agg is fail-open through GroupTrait::$aggregatable: without one, every projected path stays aggregatable — declaring the whitelist is what closes it, and GroupTrait::$aggregatablePolicy says whether an undeclared aggregate is then dropped or refused outright. Both halves are permission-gated the same way, on the resolved path, so a field hidden from reading is neither a dimension nor an aggregate.

Tags
see
GroupTrait::prepareCollect()

The entry point.

Table of Contents

Properties

$aggregatable  : array<string, string|array<int, string>>|null
Optional whitelist/mapping of aggregatable fields: `urlKey => fieldPath`.
$aggregatablePolicy  : string|null
What happens to an aggregate absent from {@see GroupTrait::$aggregatable}: one of the {@see AggregatablePolicy} codes.
$groupable  : array<string, string>|null
Optional whitelist/mapping of groupable dimensions: `urlKey => fieldPath`.

Methods

bind()  : string
Bind a value to an AQL query variable.
bindCollection()  : string
Bind a collection name to an AQL query variable.
binder()  : callable(mixed): string
Returns a binder over `$binds` — the callable {@see Arango::BINDER} carries down to the `alt` engine, so a parameter that arrived with a request becomes a bound value instead of text written into the query.
bindView()  : string
Bind the model's declared View name (`AQL::VIEW` block, {@see Search::NAME}) to an AQL query variable — collection bind parameters (`@@view`) are valid for View names as well.
initializeAggregatable()  : static
Initializes the {@see GroupTrait::$aggregatable} whitelist and its policy from the model options.
initializeGroupable()  : static
Initializes the {@see GroupTrait::$groupable} whitelist from the model options.
isGroupedQuery()  : bool
Tells whether a list query built from `$init` carries a `COLLECT`.
prepareCollect()  : array<string|int, mixed>
Resolves the `COLLECT` spec for a list query.
prepareGroupSort()  : string|null
Builds the `SORT` clause applied to a grouped result, from {@see Group::SORT}.
summedAggregates()  : array<int, string>
Names the aggregates of a {@see Arango::GROUP} spec that **add up** — the `sum` and the `avg` — and nothing else.
aggregatableEntry()  : string|AggregateExpression|null
Resolves an aggregate field token against {@see GroupTrait::$aggregatable}, applying {@see GroupTrait::$aggregatablePolicy} when the token is undeclared.
aggregateExpression()  : string|null
Resolves a declared {@see AggregateExpression} into the operand the aggregate function wraps, or `null` when it must be withdrawn.
collectAggregate()  : array<string|int, mixed>
Builds the `AQL::AGGREGATE` map from {@see Group::AGG}.
collectAssign()  : array<string|int, mixed>
groupRelationDimension()  : string|null
Resolves a grouping dimension that reads a value on the document at the other end of a relation, or null when the permission refuses it.
normalizeAggregate()  : array{0: ?string, 1: ?string}
Normalizes an aggregate definition into a `[ code, field ]` pair.
normalizeGroupFields()  : array<string, string>
Normalizes {@see Group::BY} into a `[ varName => field ]` map.

Properties

$aggregatable

Optional whitelist/mapping of aggregatable fields: `urlKey => fieldPath`.

public array<string, string|array<int, string>>|null $aggregatable = null

It is the agg counterpart of GroupTrait::$groupable, with one deliberate difference: it keys on the field token, not on the output name. In [ 'total' => 'sum:speed' ] the name total is chosen freely by the client — whitelisting it would mean nothing — while speed is the token this map resolves (to speed.value, say).

The gate is fail-open: null (no whitelist) means every projected path stays aggregatable, exactly as before this option existed. Declaring it closes the gate, and GroupTrait::$aggregatablePolicy says how loudly.

A whitelisted field is further permission-gated (Field::REQUIRES inherited from the projection), so a field hidden from reading cannot be aggregated on (MAX/MIN/AVG/SUM leak a bound of its values).

$aggregatablePolicy

What happens to an aggregate absent from {@see GroupTrait::$aggregatable}: one of the {@see AggregatablePolicy} codes.

public string|null $aggregatablePolicy = null

null resolves to AggregatablePolicy::DROP when a whitelist is declared, and to AggregatablePolicy::OPEN when none is — so a model that never heard of the option emits the query it always emitted.

$groupable

Optional whitelist/mapping of groupable dimensions: `urlKey => fieldPath`.

public array<string, string>|null $groupable = null

When set, only whitelisted Group::BY keys are allowed and each resolves to its real field path (decoupling the public group key from the internal attribute, like SortTrait::$sortable). The gate is fail-closed: null (no whitelist) means nothing is groupable — a client key never reaches doc.<key>. A whitelisted dimension is further permission-gated (Field::REQUIRES inherited from the projection), so a field hidden from reading cannot be grouped on (no group-by oracle).

Methods

bind()

Bind a value to an AQL query variable.

public bind(mixed $value[, array<string|int, mixed> &$binds = [] ][, string|null $to = null ]) : string
Parameters
$value : mixed

The value to bind to the query.

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

Reference to the array of existing bind variables.

$to : string|null = null

Optional name of the bind variable. If null, a unique name is generated.

Tags
throws
BindException

If the provided bind variable name is invalid.

Return values
string —

The formatted bind variable (including the "@" prefix as needed) for use in the query.

bindCollection()

Bind a collection name to an AQL query variable.

public bindCollection([array<string|int, mixed> &$binds = [] ][, array<string|int, mixed> $init = [] ]) : string

Prepares a bind variable for a collection name. Uses the collection defined in $init or falls back to $this->collection if none is provided.

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

Reference to the array of existing bind variables. If null, a new array is used.

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

Optional initialization array with keys:

  • Arango::COLLECTION => the collection name to bind
  • Arango::NAME => optional bind variable name
Tags
throws
BindException

If the bind variable name is invalid.

Return values
string —

The formatted bind variable representing the collection.

binder()

Returns a binder over `$binds` — the callable {@see Arango::BINDER} carries down to the `alt` engine, so a parameter that arrived with a request becomes a bound value instead of text written into the query.

public binder([array<string|int, mixed>|null &$binds = null ]) : callable(mixed): string

⚠ Deliberately a function () use ( &$binds ) and not an arrow function: fn() captures by value, so the bind would land in a copy and the query would declare a parameter nothing ever fills. That mistake costs a 400 from the server and is invisible to any assertion made on the emitted AQL — which is why the closure is built here, once, rather than at each reading point.

Parameters
$binds : array<string|int, mixed>|null = null

Reference to the array of existing bind variables; a null is initialised in place.

Return values
callable(mixed): string —

A callable registering a value and returning its @name.

bindView()

Bind the model's declared View name (`AQL::VIEW` block, {@see Search::NAME}) to an AQL query variable — collection bind parameters (`@@view`) are valid for View names as well.

public bindView([array<string|int, mixed> &$binds = [] ]) : string
Parameters
$binds : array<string|int, mixed> = []

Reference to the array of existing bind variables.

Tags
throws
BindException

If the bind variable name is invalid.

Return values
string —

The formatted bind variable representing the View.

initializeAggregatable()

Initializes the {@see GroupTrait::$aggregatable} whitelist and its policy from the model options.

public initializeAggregatable([array<string|int, mixed> $init = [] ]) : static

The whitelist is normalised through normalizeSortable(), so the three sortable notations are accepted and may be mixed: the associative urlKey => fieldPath, the indexed shorthand fieldName (token equals field), and the indexed alias [ urlKey => fieldPath ].

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

The model options (Arango::AGGREGATABLE, Arango::AGGREGATABLE_POLICY).

Return values
static

initializeGroupable()

Initializes the {@see GroupTrait::$groupable} whitelist from the model options.

public initializeGroupable([array<string|int, mixed> $init = [] ]) : static
Parameters
$init : array<string|int, mixed> = []

The model options (Arango::GROUPABLE).

Return values
static

isGroupedQuery()

Tells whether a list query built from `$init` carries a `COLLECT`.

public isGroupedQuery([array<string|int, mixed> $init = [] ]) : bool

🔑 A grouped row is not a document. After a COLLECT the projection is made of the declared variables — the dimensions, the aggregates, the count — and nothing of the collection's own shape survives. Hydrating such a row in the model's schema keeps only the names that class happens to declare, so the list and stream entry points read this to decide whether their result is taken raw.

🚨 The test is the emitted COLLECT, not the requested one. A group spec whose every dimension is dropped — undeclared, or closed by the permission gate — and which carries no aggregate emits no COLLECT at all: the query still returns documents, and they must still be hydrated. The emptiness test below mirrors, key for key, the one of aqlCollect().

The spec is resolved a second time rather than carried over from ListQueryTrait::buildListQuery(), which already knows the answer: the reading stays self-contained, and no shared signature has to grow a return channel for it. Nothing is bound along the way — BindTrait::binder() builds a throwaway map when it is handed none.

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

The list query options.

Tags
throws
UnsupportedOperationException
ValidationException
Return values
bool —

True when the built query groups its rows.

prepareCollect()

Resolves the `COLLECT` spec for a list query.

public prepareCollect([array<string|int, mixed> $init = [] ][, string $docRef = AQL::DOC ][, array<string|int, mixed>|null &$binds = null ]) : array<string|int, mixed>

Translates a friendly Arango::GROUP spec (Group::BY, Group::AGG, Group::COUNT, Group::ALT) into the raw aqlCollect() keys. Falls back to the raw Arango::COLLECT spec (or an empty array) when no group is requested.

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

The list query options.

$docRef : string = AQL::DOC

The document reference grouping fields are read from.

$binds : array<string|int, mixed>|null = null
Tags
throws
UnsupportedOperationException
ValidationException
Return values
array<string|int, mixed> —

The raw COLLECT spec (AQL::ASSIGN, AQL::AGGREGATE, AQL::WITH_COUNT).

prepareGroupSort()

Builds the `SORT` clause applied to a grouped result, from {@see Group::SORT}.

public prepareGroupSort([array<string|int, mixed> $init = [] ][, array<string|int, mixed>|null $availableVars = null ]) : string|null

The sort operates on group/aggregate variable names (never on doc, which is out of scope after COLLECT): a CSV with a leading - for descending, e.g. '-count' → count DESC, 'category,-total' → category ASC, total DESC.

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

The list query options.

$availableVars : array<string|int, mixed>|null = null
Return values
string|null —

The inner sort expression, or null when none.

summedAggregates()

Names the aggregates of a {@see Arango::GROUP} spec that **add up** — the `sum` and the `avg` — and nothing else.

public summedAggregates([array<string|int, mixed> $init = [] ]) : array<int, string>

🚨 Those are the only ones that compute a figure, and a computed float carries noise. SUM and AVERAGE add binary floats, and the store hands the result back as it is : 1380913.4299999992 where every sheet holds cents. A min and a max return a value some document holds, a count an integer, a dimension a stored value — none of them is touched, since none was computed.

🔑 Read from the request, not from the compiled spec. The friendly spec names each aggregate with its code ('sum:amount') ; a raw Arango::COLLECT spec is code written by hand, whose author decides what it serves — it names nothing here. The whitelist and the permission gate are not replayed : an aggregate they refused is absent from the row, and a name absent from a row costs nothing to shedAggregateNoise().

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

The list query options.

Return values
array<int, string> —

The variable names of the summed and averaged aggregates.

aggregatableEntry()

Resolves an aggregate field token against {@see GroupTrait::$aggregatable}, applying {@see GroupTrait::$aggregatablePolicy} when the token is undeclared.

private aggregatableEntry(string $field, string $name) : string|AggregateExpression|null

A declared token always resolves to its mapped path, whatever the policy — so the whitelist doubles as a pure publicKey => fieldPath alias map. An undeclared one is answered by the policy: passed through (AggregatablePolicy::OPEN), dropped (AggregatablePolicy::DROP), or refused (AggregatablePolicy::STRICT). An unrecognised policy code drops, so a typo closes the gate rather than opening it.

🚨 This gate answers for the whitelist only, never for the permission gate that follows it. A whitelisted field refused by Field::REQUIRES is dropped in silence even under STRICT: an error naming a protected field would tell the client that field exists — the very oracle the permission gate closes.

A declared entry may also be an AggregateExpression rather than a path, which is handed back as it is: what it reads and what it compiles is decided further down, by GroupTrait::aggregateExpression(). Only a declared entry can be one — OPEN answers an undeclared token with the client's own token, which is a path by construction.

Parameters
$field : string

The field token written by the client (sum:speed → speed).

$name : string

The aggregate output name, quoted in the strict error.

Tags
throws
ValidationException

Under AggregatablePolicy::STRICT, naming the refused token.

Return values
string|AggregateExpression|null —

The resolved field path, the declared expression, or null when the aggregate is dropped.

aggregateExpression()

Resolves a declared {@see AggregateExpression} into the operand the aggregate function wraps, or `null` when it must be withdrawn.

private aggregateExpression(AggregateExpression $expression, string $docRef, array<string|int, mixed> $init) : string|null

🚨 Every path the expression reads is gated, and one refusal is enough. A path-based aggregate has a single path to check; an expression has several — that is what it is for. Checking none of them, or only the first, would make a derived expression the way around Field::REQUIRES: a field closed to the projection would come back out as a sum, in silence, without a single existing essay turning red.

⚠ An empty paths() withdraws the aggregate. An expression that declares no path declares that it reads nothing. Read as "nothing to gate", it would be precisely the hole above; read as a refusal, a mis-declaration costs the aggregate and shows. The refusal is silent, like every permission refusal here — naming a protected field would tell the client it exists.

The attribute-name guard does not apply: an expression is not an attribute name, and its paths are never interpolated — they only feed the gate. What replaces that guard is origin, not trust: an expression is always a declaration of the consumer's own code, reachable only through a public key already on the whitelist, and any value from the request goes through Arango::BINDER.

Parameters
$expression : AggregateExpression

The declared expression.

$docRef : string

The document reference to read from.

$init : array<string|int, mixed>

The query init, carrying the binder and the authorizer.

Return values
string|null —

The per-document expression, or null when the aggregate is dropped.

collectAggregate()

Builds the `AQL::AGGREGATE` map from {@see Group::AGG}.

private collectAggregate(array<string|int, mixed> $group, string $docRef[, array<string|int, mixed> $init = [] ][, array<string|int, mixed>|null &$binds = null ]) : array<string|int, mixed>

An entry of the whitelist may be an AggregateExpression instead of a path, in which case the engine wraps what the expression compiles rather than doc.<path> — one function, one computed operand.

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

The group spec.

$docRef : string

The document reference.

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

The query init.

$binds : array<string|int, mixed>|null = null

The bind map, by reference: an expression binds its values through it.

Tags
throws
ValidationException
Return values
array<string|int, mixed> —

[ outName => 'FN(doc.field)' ].

collectAssign()

private collectAssign(array<string|int, mixed> $group, string $docRef[, array<string|int, mixed> $init = [] ][, array<string|int, mixed>|null &$binds = null ]) : array<string|int, mixed>
Parameters
$group : array<string|int, mixed>
$docRef : string
$init : array<string|int, mixed> = []
$binds : array<string|int, mixed>|null = null
Return values
array<string|int, mixed>

groupRelationDimension()

Resolves a grouping dimension that reads a value on the document at the other end of a relation, or null when the permission refuses it.

private groupRelationDimension(string $var, array<string|int, mixed> $entry, array<string|int, mixed> $init) : string|null

A grouped query never projects — doc is consumed by the COLLECT, and returnFields() is not called — so there is no LET to name, unlike the relational sort. The dimension carries its own traversal instead, written inline in the COLLECT by buildEdgeGroupExpression().

Three declarations cannot be honoured, and each is refused rather than dropped: a dropped dimension reads as a client typo, while these are faults in the model that only its author can fix.

  • the named relation is not declared in AQL::EDGES, so there is no traversal to compile;
  • it is not a singular relation. Grouping on a plural one has no sound answer: kept as an array the dimension groups by the combination, and unwound before the COLLECT it counts a multi-vertex document once per vertex, inflating every other aggregate of the same COLLECT;
  • no Field::PATH says which field of the related document labels the group.

Permission follows the relation: an explicit Field::REQUIRES on the dimension wins, otherwise the subject declared on the relation field is inherited. Grouping by a field hidden from reading would return its distinct values in clear.

Parameters
$var : string

The dimension key (the URL key).

$entry : array<string|int, mixed>

The groupable definition (AQL::EDGE, Field::PATH, Field::REQUIRES).

$init : array<string|int, mixed>

The request-level init. Reads Arango::AUTHORIZER.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
ReflectionException
UnsupportedOperationException
ValidationException

When the declaration cannot be honoured.

Return values
string|null —

The dimension expression, or null when refused by permission.

normalizeAggregate()

Normalizes an aggregate definition into a `[ code, field ]` pair.

private normalizeAggregate(mixed $definition) : array{0: ?string, 1: ?string}

Accepts 'sum:amount' (string) or ['sum','amount'] (list).

Parameters
$definition : mixed
Return values
array{0: ?string, 1: ?string}

normalizeGroupFields()

Normalizes {@see Group::BY} into a `[ varName => field ]` map.

private normalizeGroupFields(mixed $by) : array<string, string>
  • CSV string 'category,status' → [ 'category' => 'category', 'status' => 'status' ].
  • list ['category','status'] → same.
  • assoc ['year' => 'created'] → kept as-is.

Dotted fields yield underscore variable names (address.city → address_city).

Parameters
$by : mixed
Return values
array<string, string>
On this page

Search results