AQLQueryTrait uses trait:short, trait:short, trait:short, trait:short, trait:short, trait:short, trait:short, trait:short, trait:short
This trait defines all facet helpers in the Model class.
Table of Contents
Constants
- BOUNDS : string = 'bounds'
- The 'bounds' parameter constant.
- FACETS : string = 'facets'
- The 'facets' parameter constant.
- FILTERS : string = 'filters'
- The 'filters' parameter constant.
- SEARCHABLE : string = 'searchable'
- The 'searchable' parameter key.
Properties
- $activable : bool
- $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.
- $bounds : array<string|int, mixed>|null
- The bounds whitelist (`field => true | definition`), or `null` when nothing is boundable.
- $facets : array<string|int, mixed>|null
- The facet settings.
- $filters : array<string|int, mixed>|null
- Defines all valid filtering conditions for queries used in the list() and count() methods.
- $groupable : array<string, string>|null
- Optional whitelist/mapping of groupable dimensions: `urlKey => fieldPath`.
- $searchable : array<string|int, mixed>|null
- The searchable fields swept by the classic `?search=` `LIKE` (see {@see prepareSearch()}). A list of field names; an entry may instead be an array carrying its name under {@see Search::KEY} plus options such as {@see Search::REQUIRES} to gate the field by permission:
- $searchOperator : string
- The default operator combining the words of a `?search=` term **within a field** during the classic `LIKE` sweep ({@see prepareSearch()}): {@see \oihana\arango\db\enums\Logic::AND} (every word must be found in the field, order-independent) or {@see \oihana\arango\db\enums\Logic::OR} (the whole term matched as one substring — the default, backward-compatible).
- $searchSeparators : string
- The extra characters (beyond whitespace) that split a `?search=` term into words for the `AND` operator of the classic `LIKE` sweep ({@see prepareSearch()}).
- $sortable : array<string|int, mixed>|null
- The collection (map) of all the sortable fields.
- $sortTiebreak : string|null
- The criterion that closes this model's order, in the `?sort=` grammar.
- $view : array<string|int, mixed>|null
- The model-level ArangoSearch declaration (`AQL::VIEW` block, see {@see Search}).
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.
- getViewLinks() : array<string, ArangoSearchLink>
- Returns the desired per-collection link map of the declared View — the model's collection linked with {@see buildViewLink()} — or an empty map when the model has no collection.
- getViewName() : string|null
- Returns the name of the declared View ({@see Search::NAME} of the `AQL::VIEW` block), or `null` when the model declares none.
- hasViewSearch() : bool
- Indicates whether the View search is active: the model declares a named View (`AQL::VIEW` block) with at least one searched field, **and** the request carries a non-empty search term.
- initializeActivable() : static
- Initialize the activable flag to check if the documents are 'active' or not.
- initializeAggregatable() : static
- Initializes the {@see GroupTrait::$aggregatable} whitelist and its policy from the model options.
- initializeBounds() : static
- Initialize the 'bounds' property.
- initializeFacets() : static
- Initialize the 'facets' property.
- initializeFilters() : static
- Initialize the 'filters' property.
- initializeGroupable() : static
- Initializes the {@see GroupTrait::$groupable} whitelist from the model options.
- initializeSearchable() : static
- Initialize the 'searchable' property.
- initializeSearchOperator() : static
- Initialize the `LIKE` sweep operator ({@see $searchOperator}) from the `Search::OPERATOR` init key. An explicit value is normalized to {@see Logic::AND} / {@see Logic::OR}; an absent key keeps the default ({@see Logic::OR}, backward-compatible).
- initializeSearchSeparators() : static
- Initialize the `LIKE` sweep word separators ({@see $searchSeparators}) from the `Search::SEPARATORS` init key. Accepts a string of characters or a list of characters (joined to a string); an absent key keeps the default (the hyphen). Only meaningful with the `AND` operator. See {@see Search::SEPARATORS}.
- initializeSortable() : $this
- Initialize the sortable array definition.
- initializeSortTiebreak() : $this
- Initialize the criterion that closes the model's order.
- initializeView() : static
- Initialize the model-level ArangoSearch declaration (`AQL::VIEW` block) and lazily provision the View, mirroring the collection provisioning of `initializeCollection()`: when the model is lazy and the declared View does not exist, it is created from the declaration — the searched fields (dotted paths supported) are linked on the model's collection with the declared {@see Search::ANALYZER}. An existing View is never altered — inspect and resynchronize explicitly with {@see viewDiff()} / {@see viewSync()} (or the `views` action of the `arangodb` command).
- isGroupedQuery() : bool
- Tells whether a list query built from `$init` carries a `COLLECT`.
- prepareActive() : string|null
- Prepare the 'active' variable.
- prepareCollect() : array<string|int, mixed>
- Resolves the `COLLECT` spec for a list query.
- prepareFilter() : string|null
- Prepare the AQL query filtering with specific definitions.
- prepareGroupSort() : string|null
- Builds the `SORT` clause applied to a grouped result, from {@see Group::SORT}.
- prepareLimit() : string|null
- prepareSearch() : string|null
- Prepare the searchable AQL conditions — the classic `?search=` `LIKE` sweep, a parenthesized `OR` of case-insensitive `LIKE()` predicates over every (permitted) searchable field, used when no View search is active.
- prepareSort() : string|null
- Prepare the AQL `SORT` expression from the `?sort=` grammar and, optionally, the `?near=` anchor.
- prepareViewSearch() : string|null
- Prepare the relevance-ranked `SEARCH` expression of an active View search, or `null` when the View search is inactive (no `AQL::VIEW` declaration, no searched fields, or no search term) — the caller then falls back to the classic `LIKE` sweep of {@see prepareSearch()}.
- summedAggregates() : array<int, string>
- Names the aggregates of a {@see Arango::GROUP} spec that **add up** — the `sum` and the `avg` — and nothing else.
- viewDiff() : DiffReport
- Compares the model's View declaration with the server state and reports the differences, without touching anything.
- viewSync() : DiffReport
- Reconciles the model's View with its declaration: creates it when missing, repairs a drift with `updateProperties()` (the View stays available while the inverted index rebuilds in the background), and leaves {@see DiffStatus::IN_SYNC}, {@see DiffStatus::INVALID} or {@see DiffStatus::UNREACHABLE} reports untouched.
- alterFilterKey() : string
- Apply the key-side (left) `alt` transformation to a key expression.
- buildViewLink() : ArangoSearchLink
- Builds the View link of the model's collection from the `AQL::VIEW` declaration: every searched field (dotted paths become nested fields) is indexed with its resolved Analyzer — the per-field {@see Search::ANALYZER} when declared, otherwise the View-level one.
- buildViewSearchGroups() : array<string, array<string|int, string>>
- Builds the per-Analyzer expression groups of the View search under the historical `OR` grammar : each comma-separated term is bound whole and matched in one shot per field (`doc.<field> IN TOKENS(@search_N, …)`, whose `IN` semantics already `OR` the term's tokens), plus the optional phrase and fuzzy branches. This is the exact grammar emitted when no field opts into {@see Search::OPERATOR} `AND` — see {@see prepareViewSearch()}.
- buildViewSearchGroupsWithOperator() : array<string, array<string|int, string>>
- Builds the per-Analyzer expression groups when at least one field opts into {@see Search::OPERATOR} `AND`. Each comma-separated term is split into words (whitespace) ; a field then combines those words with its resolved operator :
- getSearchableSpecs() : array<string, array<string, mixed>>
- Normalizes the model's `AQL::SEARCHABLE` list into a per-field specification map `field => [ (Search::REQUIRES => …)? ]`, the single source consumed by {@see prepareSearch()} (the `LIKE` sweep) and by the `searchable` fallback of {@see getViewFieldSpecs()}.
- getViewFieldSpecs() : array<string, array<string, mixed>>
- Normalizes the searched fields of the `AQL::VIEW` declaration into a per-field specification map `field => [ Search::BOOST => float, … ]` — the single source of truth from which {@see getViewSearchFields()} derives the boost map and {@see prepareViewSearch()} resolves the per-field options.
- getViewSearchFields() : array<string, float>
- Normalizes the searched fields into a `field => boost` map — a boost-only façade over {@see getViewFieldSpecs()}, used by {@see buildViewLink()} and {@see viewDiff()} which only care about the field paths and their weights.
- matchFieldsAuthorized() : bool
- Decides whether every sub-field a `match` names may be read by this caller.
- ngramMatch() : string
- Builds the `NGRAM_MATCH(doc.<field>, @term [, threshold], "<analyzer>")` core of a {@see Search::NGRAM} branch (without the surrounding boost). The threshold is inlined only when the field declares one, else the server default applies. Shared by the whole-term and the per-word emissions.
- prepareFacets() : string|null
- Prepare the query with AQL facets definitions.
- prepareFilterBetween() : string|null
- Prepares an inclusive `between` (range) clause: `key >= @min && key <= @max`.
- prepareFilterComparator() : string
- Prepares the filter clause with a specific operator.
- prepareFilterKey() : string
- Prepares the filter clause with a specific key and document, with optional function transformations via 'alt' parameter.
- prepareFilterValue() : string
- Prepare the filter clause with a specific value to evaluates.
- prepareNear() : string|null
- Build the `DISTANCE(...)` expression for a `?near=` anchor and bind its coordinates.
- pushWholeTermField() : void
- Pushes the whole-term match of one field onto the Analyzer groups — the base `doc.<field> IN TOKENS(@term, …)` (boosted when the field weight is not `1`), the optional exact-phrase bonus and the optional fuzzy branch, plus the {@see Search::NGRAM} branch when declared. This is the `OR`-grammar emission, shared by {@see buildViewSearchGroups()} (every field) and by the `OR` fields of {@see buildViewSearchGroupsWithOperator()}.
- resolveFieldSearchSpec() : array{0: string[], 1: float, 2: int, 3: bool, 4: string}
- Resolves the per-field search facets shared by both group builders : the Analyzers indexing the field (a per-field {@see Search::ANALYZER} overrides the View-level one, a single Analyzer normalized to a one-element list), the boost, the fuzzy tolerance and phrase bonus (a per-field value overrides the View-level default), and the `doc.<field>` accessor (with the `[*]` array marker stripped — the flat path already matches any element of the indexed array, and the `SEARCH` grammar rejects the expansion form).
- resolveFilterComparator() : string
- Translates an operator code into its AQL comparator, refusing the ones this filter cannot honour.
- splitSearchTermWords() : array<string|int, string>
- Splits an `AND`-operator search term into its words, on whitespace plus the given extra separator characters ({@see Search::SEPARATORS}). Whitespace always splits; `$separators` adds literal characters (a string, or a list of characters joined to one), `null` falls back to the hyphen default, and an empty value splits on whitespace only. The extra characters are regex-escaped, so any punctuation is safe. Empty words are dropped.
- 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.
- authorizeRelationSortKey() : string|null
- Resolves a sortable entry that orders on a field of a **related** document, reached through a relation this model already projects.
- authorizeSortKey() : string|null
- Resolve a whitelisted sort entry to its AQL field expression, gated by permission.
- collectAggregate() : array<string|int, mixed>
- Builds the `AQL::AGGREGATE` map from {@see Group::AGG}.
- collectAssign() : array<string|int, mixed>
- collectMatchFields() : 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.
- isSortAuthorized() : bool
- Decide whether a sort/near field is granted for the request.
- isTranslatedSortEntry() : bool
- Decides whether a sortable entry orders on a **multilingual** field — one whose stored value is a translations object (`{ fr: "…", en: "…" }`) rather than the text to compare.
- 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.
- resolveSortEntry() : array{0: mixed, 1: mixed}
- Resolve a whitelisted sort entry to its `[ fieldPath, requires ]` pair.
- resolveSortFallbackLang() : string|null
- Resolve the **fallback** language of a multilingual sort entry — the locale used when the requested one is absent from a document, or when the call requests none.
- sortCriterion() : string|null
- Resolves one sort criterion through the two gates every key travels.
- sortTiebreakOrders() : array<int, string>
- The criteria that close the order, appended after everything the caller asked for.
- splitSortToken() : array{0: string, 1: string}
- Splits a sort token into the key it names and the direction it asks for.
- translatedSortExpression() : string
- Build the ordering expression of a multilingual entry: the requested locale, then the fallback one, then the field named by `Field::ELSE`.
Constants
BOUNDS
The 'bounds' parameter constant.
public
string
BOUNDS
= 'bounds'
FACETS
The 'facets' parameter constant.
public
string
FACETS
= 'facets'
FILTERS
The 'filters' parameter constant.
public
string
FILTERS
= 'filters'
SEARCHABLE
The 'searchable' parameter key.
public
string
SEARCHABLE
= 'searchable'
Properties
$activable
public
bool
$activable
= false
$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.
$bounds
The bounds whitelist (`field => true | definition`), or `null` when nothing is boundable.
public
array<string|int, mixed>|null
$bounds
= []
$facets
The facet settings.
public
array<string|int, mixed>|null
$facets
= []
$filters
Defines all valid filtering conditions for queries used in the list() and count() methods.
public
array<string|int, mixed>|null
$filters
= []
$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).
$searchable
The searchable fields swept by the classic `?search=` `LIKE` (see {@see prepareSearch()}). A list of field names; an entry may instead be an array carrying its name under {@see Search::KEY} plus options such as {@see Search::REQUIRES} to gate the field by permission:
public
array<string|int, mixed>|null
$searchable
= []
AQL::SEARCHABLE =>
[
'name' , // public
[ Search::KEY => 'salary' , Search::REQUIRES => 'hr:salary' ], // gated
]
$searchOperator
The default operator combining the words of a `?search=` term **within a field** during the classic `LIKE` sweep ({@see prepareSearch()}): {@see \oihana\arango\db\enums\Logic::AND} (every word must be found in the field, order-independent) or {@see \oihana\arango\db\enums\Logic::OR} (the whole term matched as one substring — the default, backward-compatible).
public
string
$searchOperator
= \oihana\arango\db\enums\Logic::OR
Set once per model through the Search::OPERATOR init key. The View search
carries its own operator inside the AQL::VIEW block (per field), see
Search::OPERATOR.
$searchSeparators
The extra characters (beyond whitespace) that split a `?search=` term into words for the `AND` operator of the classic `LIKE` sweep ({@see prepareSearch()}).
public
string
$searchSeparators
= \oihana\enums\Char::HYPHEN
A string of characters; set once per model through the Search::SEPARATORS
init key (a list of characters is normalized to a string at init). The
default is the hyphen (-) so « Jean-Marc » splits like « Jean Marc »; an
empty string keeps hyphenated codes whole. Only used in AND mode. See
Search::SEPARATORS.
$sortable
The collection (map) of all the sortable fields.
public
array<string|int, mixed>|null
$sortable
= null
$sortTiebreak
The criterion that closes this model's order, in the `?sort=` grammar.
public
string|null
$sortTiebreak
= null
null — the default — keeps the historical behaviour : a named sort orders
exactly what it names, ties included.
$view
The model-level ArangoSearch declaration (`AQL::VIEW` block, see {@see Search}).
public
array<string|int, mixed>|null
$view
= null
When present (with a Search::NAME), the ?search= parameter switches
from the simple LIKE sweep to an index-accelerated, relevance-ranked
SEARCH against the declared View.
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
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
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
nullis 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
Return values
string —The formatted bind variable representing the View.
getViewLinks()
Returns the desired per-collection link map of the declared View — the model's collection linked with {@see buildViewLink()} — or an empty map when the model has no collection.
public
getViewLinks() : array<string, ArangoSearchLink>
Tags
Return values
array<string, ArangoSearchLink>getViewName()
Returns the name of the declared View ({@see Search::NAME} of the `AQL::VIEW` block), or `null` when the model declares none.
public
getViewName() : string|null
Return values
string|nullhasViewSearch()
Indicates whether the View search is active: the model declares a named View (`AQL::VIEW` block) with at least one searched field, **and** the request carries a non-empty search term.
public
hasViewSearch([array<string|int, mixed>|string|null $search = [] ]) : bool
Parameters
- $search : array<string|int, mixed>|string|null = []
-
The
$initarray (readsArango::SEARCH) or the search term itself.
Tags
Return values
boolinitializeActivable()
Initialize the activable flag to check if the documents are 'active' or not.
public
initializeActivable([array<string|int, mixed> $init = [] ]) : static
Parameters
- $init : array<string|int, mixed> = []
Return values
staticinitializeAggregatable()
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
staticinitializeBounds()
Initialize the 'bounds' property.
public
initializeBounds([array<string|int, mixed> $init = [] ]) : static
Parameters
- $init : array<string|int, mixed> = []
Return values
staticinitializeFacets()
Initialize the 'facets' property.
public
initializeFacets([array<string|int, mixed> $init = [] ]) : static
Parameters
- $init : array<string|int, mixed> = []
Return values
staticinitializeFilters()
Initialize the 'filters' property.
public
initializeFilters([array<string|int, mixed> $init = [] ]) : static
Parameters
- $init : array<string|int, mixed> = []
Return values
staticinitializeGroupable()
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
staticinitializeSearchable()
Initialize the 'searchable' property.
public
initializeSearchable([array<string|int, mixed> $init = [] ]) : static
Parameters
- $init : array<string|int, mixed> = []
Return values
staticinitializeSearchOperator()
Initialize the `LIKE` sweep operator ({@see $searchOperator}) from the `Search::OPERATOR` init key. An explicit value is normalized to {@see Logic::AND} / {@see Logic::OR}; an absent key keeps the default ({@see Logic::OR}, backward-compatible).
public
initializeSearchOperator([array<string|int, mixed> $init = [] ]) : static
Parameters
- $init : array<string|int, mixed> = []
Return values
staticinitializeSearchSeparators()
Initialize the `LIKE` sweep word separators ({@see $searchSeparators}) from the `Search::SEPARATORS` init key. Accepts a string of characters or a list of characters (joined to a string); an absent key keeps the default (the hyphen). Only meaningful with the `AND` operator. See {@see Search::SEPARATORS}.
public
initializeSearchSeparators([array<string|int, mixed> $init = [] ]) : static
Parameters
- $init : array<string|int, mixed> = []
Return values
staticinitializeSortable()
Initialize the sortable array definition.
public
initializeSortable([array<string|int, mixed> $init = [] ]) : $this
The raw definition (from the AQL::SORTABLE init key, or the property default)
is normalised through normalizeSortable() into the canonical
urlKey => fieldPath map. Three interchangeable notations are accepted and may
be mixed: the legacy associative urlKey => fieldPath, the indexed shorthand
fieldName (token equals field), and the indexed alias [ urlKey => fieldPath ].
null is preserved and means fail-closed (no whitelist → nothing client
sorts). The normalisation is idempotent.
Parameters
- $init : array<string|int, mixed> = []
Return values
$thisinitializeSortTiebreak()
Initialize the criterion that closes the model's order.
public
initializeSortTiebreak([array<string|int, mixed> $init = [] ]) : $this
A string in the ?sort= grammar, so it may name several keys
('id,additionalType') and a direction ('-id'). null disables the
mechanism entirely — a model that declares nothing behaves exactly as before.
Parameters
- $init : array<string|int, mixed> = []
Return values
$thisinitializeView()
Initialize the model-level ArangoSearch declaration (`AQL::VIEW` block) and lazily provision the View, mirroring the collection provisioning of `initializeCollection()`: when the model is lazy and the declared View does not exist, it is created from the declaration — the searched fields (dotted paths supported) are linked on the model's collection with the declared {@see Search::ANALYZER}. An existing View is never altered — inspect and resynchronize explicitly with {@see viewDiff()} / {@see viewSync()} (or the `views` action of the `arangodb` command).
public
initializeView([array<string|int, mixed> $init = [] ]) : static
Parameters
- $init : array<string|int, mixed> = []
Tags
Return values
staticisGroupedQuery()
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
Return values
bool —True when the built query groups its rows.
prepareActive()
Prepare the 'active' variable.
public
prepareActive([array<string|int, mixed> $init = [] ][, array<string|int, mixed>|null &$binds = null ][, string $docRef = AQL::DOC ]) : string|null
Parameters
- $init : array<string|int, mixed> = []
- $binds : array<string|int, mixed>|null = null
- $docRef : string = AQL::DOC
Tags
Return values
string|nullprepareCollect()
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
Return values
array<string|int, mixed> —The raw COLLECT spec (AQL::ASSIGN, AQL::AGGREGATE, AQL::WITH_COUNT).
prepareFilter()
Prepare the AQL query filtering with specific definitions.
public
prepareFilter([array<string|int, mixed>|null $init = [] ][, array<string|int, mixed>|null &$binds = null ][, string $docRef = AQL::DOC ][, array<string|int, mixed> $auth = [] ]) : string|null
Parameters
- $init : array<string|int, mixed>|null = []
- $binds : array<string|int, mixed>|null = null
- $docRef : string = AQL::DOC
- $auth : array<string|int, mixed> = []
Tags
Return values
string|nullprepareGroupSort()
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.
prepareLimit()
public
prepareLimit([array<string|int, mixed> $init = [] ][, array<string|int, mixed>|null &$binds = null ]) : string|null
Parameters
- $init : array<string|int, mixed> = []
- $binds : array<string|int, mixed>|null = null
Tags
Return values
string|nullprepareSearch()
Prepare the searchable AQL conditions — the classic `?search=` `LIKE` sweep, a parenthesized `OR` of case-insensitive `LIKE()` predicates over every (permitted) searchable field, used when no View search is active.
public
prepareSearch([array<string|int, mixed>|string|null $search = [] ][, array<string|int, mixed>|null &$binds = null ][, array<string|int, mixed>|null $searchable = null ][, string $docRef = AQL::DOC ]) : string|null
The comma-separated terms and the fields are always OR-ed. How the words
of a single term combine within a field follows the model's
$searchOperator (Search::OPERATOR init key):
- Logic::OR (default) matches the whole term as one substring
(
LIKE(doc.name, "%marc fourcade%")), so it needs the words adjacent and in order — the historical, byte-for-byte grammar; - Logic::AND splits the term on whitespace and requires every word in
the same field (
( LIKE(doc.name, "%marc%") && LIKE(doc.name, "%fourcade%") )), order-independent.
Parameters
- $search : array<string|int, mixed>|string|null = []
- $binds : array<string|int, mixed>|null = null
- $searchable : array<string|int, mixed>|null = null
- $docRef : string = AQL::DOC
Tags
Return values
string|nullprepareSort()
Prepare the AQL `SORT` expression from the `?sort=` grammar and, optionally, the `?near=` anchor.
public
prepareSort([array<string|int, mixed> $init = [] ][, array<string|int, mixed>|null $sortable = null ][, string $docRef = AQL::DOC ][, array<string|int, mixed>|null &$binds = null ]) : string|null
Each comma-separated criterion in Arango::SORT is resolved against $sortable
(URL key → AQL field path); a leading - makes it descending. The synthetic
distance key (Schema::DISTANCE) is resolved from Arango::NEAR and only
honored when $binds is provided (so the reference point can be bound).
🔑 A criterion list that resolved to nothing is treated as no sort at all. The
gates below drop what they refuse — an unlisted key, a key whose field the caller
may not read — and a request whose every criterion was dropped used to leave the
query with no SORT, because $sortDefault is only read when Arango::SORT
is absent. The answer then came back in whatever order the store had at hand, and
a LIMIT/OFFSET walk over it could serve one document twice and another never.
Falling back on the model's own default restores a deterministic order — without
ever honoring the refused key, since the default travels through the very same
gates. A model declaring no default still answers unordered : there is nothing to
fall back on.
🔑 The model's tiebreaker closes whatever the caller asked for. A named sort
replaces the default outright — and with it the criterion the default carried to
break its ties. AQL::SORT_TIEBREAK is therefore appended to the resolved
criteria, last and only there, unless the order already names it. It is added
after the fallback below, so that a sort which resolved to nothing still
gets the default rather than the tiebreaker alone. A model declaring none keeps
the historical behaviour — see sortTiebreakOrders().
🔑 An empty ?sort= counts as nothing asked for. '' is not null, so it
used to reach the grammar, resolve to no criterion, and cost the model its default
order — while the score and the distance branches already read it as « no sort
given ». All three now agree.
Parameters
- $init : array<string|int, mixed> = []
-
Per-call parameters. Reads
Arango::SORT(grammar) andArango::NEAR(geo anchor). - $sortable : array<string|int, mixed>|null = null
-
URL-key → field-path whitelist. Defaults to
$this->sortable. - $docRef : string = AQL::DOC
-
The document variable the fields hang off (default
doc). - $binds : array<string|int, mixed>|null = null
-
Bind variables, populated by reference. Required to enable
distance/?near=sorting.
Tags
Return values
string|null —The SORT body (without the SORT keyword), or an empty string when nothing sorts.
prepareViewSearch()
Prepare the relevance-ranked `SEARCH` expression of an active View search, or `null` when the View search is inactive (no `AQL::VIEW` declaration, no searched fields, or no search term) — the caller then falls back to the classic `LIKE` sweep of {@see prepareSearch()}.
public
prepareViewSearch([array<string|int, mixed>|string|null $search = [] ][, array<string|int, mixed>|null &$binds = null ][, string $docRef = AQL::DOC ]) : string|null
The grammar keeps the ?search= contract (comma-separated terms, OR
everywhere, values bound — never inlined). Per term and per field:
- the base match
doc.<field> IN TOKENS(@search_N, "<analyzer>")(both sides analyzed), weighted byBOOST(…, <boost>)when the field boost differs from1; a field reaching into an array of objects has its[*]expansion marker stripped here (doc.contactPoints.email IN …, notdoc.contactPoints[*].email): theSEARCHgrammar rejects array expansion, and the flat path already matches any element of the indexed array — see buildViewLink(); - with a per-field or View-level Search::PHRASE, an exact-phrase
bonus
BOOST(PHRASE(doc.<field>, @search_N), <boost × 2>); a field may override the View-level flag (an explicitfalseopts that field out); - with a per-field or View-level Search::FUZZY
> 0, a typo-tolerantLEVENSHTEIN_MATCH(doc.<field>, @search_N, <fuzzy>); a field may override the View-level tolerance — an explicit0opts that field out while the rest stays fuzzy.
Field expressions are grouped by their resolved Analyzer (a field may
override the View-level Search::ANALYZER) and each group is wrapped
in its own ANALYZER(…, "<analyzer>"), the groups being OR-ed together.
With a single Analyzer the output is a single ANALYZER(…) wrap, byte for
byte the classic form.
Search::REQUIRES gates the search by the request authorizer
(Arango::AUTHORIZER, see isAuthorized()) at two levels: on the
AQL::VIEW block it gates the whole search, inside a field entry it gates
that field; the two combine with AND (fail-open without an authorizer).
If the View-level gate is denied, or permissions remove every field, the
expression is false — the search matches nothing and never falls back
to searching everything.
When the request carries an active language (Arango::LANG, the ?lang=
parameter), localized fields (those declaring Search::LANG) join
the SEARCH only when their locale matches; locale-agnostic fields always
do. An active language matching no field is ignored — the SEARCH is
never emptied (within the permitted set).
Parameters
- $search : array<string|int, mixed>|string|null = []
-
The
$initarray (readsArango::SEARCH) or the search term itself. - $binds : array<string|int, mixed>|null = null
-
Bind variables, populated by reference.
- $docRef : string = AQL::DOC
-
The document variable the fields hang off.
Tags
Return values
string|null —The SEARCH expression, or null when the View search is inactive.
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.
viewDiff()
Compares the model's View declaration with the server state and reports the differences, without touching anything.
public
viewDiff() : DiffReport
On top of the field/analyzer drift detected by ViewManagementTrait::viewDiff(), the model-level report validates the coherence of the declaration itself: a missing Search::NAME, no searched field, no collection, an analyzer or a collection unknown to the server all resolve to DiffStatus::INVALID — such a View is never created nor synchronized automatically.
Tags
Return values
DiffReportviewSync()
Reconciles the model's View with its declaration: creates it when missing, repairs a drift with `updateProperties()` (the View stays available while the inverted index rebuilds in the background), and leaves {@see DiffStatus::IN_SYNC}, {@see DiffStatus::INVALID} or {@see DiffStatus::UNREACHABLE} reports untouched.
public
viewSync() : DiffReport
Tags
Return values
DiffReport —The viewDiff() report, with $applied set when the View has been created or updated.
alterFilterKey()
Apply the key-side (left) `alt` transformation to a key expression.
protected
alterFilterKey(string $key[, array<string|int, mixed> $init = [] ][, array<string|int, mixed>|null &$binds = null ]) : string
Thin wrapper over static::alterExpression(): it resolves the alt
parameter into its key/value sides via static::resolveAltSides() and
applies the key-side chain. The three legacy alt forms (string, list of
functions, function-with-params) keep transforming the key only, unchanged.
Parameters
- $key : string
-
The key expression to transform.
- $init : array<string|int, mixed> = []
-
Filter initialization array containing the 'alt' parameter.
- $binds : array<string|int, mixed>|null = null
Tags
Return values
string —The transformed key expression.
buildViewLink()
Builds the View link of the model's collection from the `AQL::VIEW` declaration: every searched field (dotted paths become nested fields) is indexed with its resolved Analyzer — the per-field {@see Search::ANALYZER} when declared, otherwise the View-level one.
protected
buildViewLink() : ArangoSearchLink
A field may reach a sub-field of an array of objects with the [*]
expansion marker (e.g. contactPoints[*].email). The marker is stripped
here (stripArrayExpansion()) so the link declares the flat nested
path (contactPoints → email): ArangoSearch (Community) descends into
the array on its own — no Enterprise nested flag is needed for this
(non-correlated) search. The matching query strips the marker too — the
SEARCH grammar rejects array expansion, see prepareViewSearch().
The stripped path is validated (assertAttributeName()) to reject a
malformed declaration early.
A field whose resolved Analyzer equals the link-level default is emitted
as an empty node (no analyzers key) rather than spelling the default
out: the server normalizes a field whose analyzers equal the link default
to } (the redundant mention is dropped), so spelling it out would make
the declared form differ forever from the stored one and viewDiff()
would report a permanent false drift. The link carries no link-level
analyzers, so its default is the server default (identity) — computed
here rather than hard-coded so the elimination stays correct if a
link-level analyzer is introduced later.
Tags
Return values
ArangoSearchLinkbuildViewSearchGroups()
Builds the per-Analyzer expression groups of the View search under the historical `OR` grammar : each comma-separated term is bound whole and matched in one shot per field (`doc.<field> IN TOKENS(@search_N, …)`, whose `IN` semantics already `OR` the term's tokens), plus the optional phrase and fuzzy branches. This is the exact grammar emitted when no field opts into {@see Search::OPERATOR} `AND` — see {@see prepareViewSearch()}.
protected
buildViewSearchGroups(string $search, array<string|int, mixed>|null &$binds, string $docRef, array<string|int, mixed> $fields, string $modelAnalyzer, bool $globalPhrase, int $globalFuzzy) : array<string, array<string|int, string>>
Parameters
- $search : string
-
The raw
?search=term string (comma-separated). - $binds : array<string|int, mixed>|null
-
Bind variables, populated by reference.
- $docRef : string
-
The document variable the fields hang off.
- $fields : array<string|int, mixed>
-
The resolved, gated per-field specs.
- $modelAnalyzer : string
-
The View-level Analyzer (a per-field override is respected).
- $globalPhrase : bool
-
The View-level phrase-bonus default.
- $globalFuzzy : int
-
The View-level Levenshtein tolerance default.
Tags
Return values
array<string, array<string|int, string>> —Analyzer name => list of expressions, in first-seen order.
buildViewSearchGroupsWithOperator()
Builds the per-Analyzer expression groups when at least one field opts into {@see Search::OPERATOR} `AND`. Each comma-separated term is split into words (whitespace) ; a field then combines those words with its resolved operator :
protected
buildViewSearchGroupsWithOperator(string $search, array<string|int, mixed>|null &$binds, string $docRef, array<string|int, mixed> $fields, string $modelAnalyzer, bool $globalPhrase, int $globalFuzzy, string $globalOperator[, string|array<string|int, mixed>|null $separators = null ]) : array<string, array<string|int, string>>
OR(default) reproduces the whole-term match (pushWholeTermField()), so a mixed View keeps its loose fields loose ;ANDrequires every word in the same field (( doc.<field> IN TOKENS(@w0, …) && doc.<field> IN TOKENS(@w1, …) )), the fuzzy branch (when enabled) widening each word (( … IN TOKENS(@w, …) || LEVENSHTEIN_MATCH(…, @w, …) )), and the phrase bonus (when enabled) kept on the whole term as a ranking boostOR-ed on top — an adjacency match implies the words, so the matched set is unchanged.
The whole-term bind is materialized only when a field needs it (an OR
field, or an AND field carrying the phrase bonus), never left dangling.
Groups are OR-ed between fields and between terms by prepareViewSearch().
Parameters
- $search : string
-
The raw
?search=term string (comma-separated). - $binds : array<string|int, mixed>|null
-
Bind variables, populated by reference.
- $docRef : string
-
The document variable the fields hang off.
- $fields : array<string|int, mixed>
-
The resolved, gated per-field specs.
- $modelAnalyzer : string
-
The View-level Analyzer (a per-field override is respected).
- $globalPhrase : bool
-
The View-level phrase-bonus default.
- $globalFuzzy : int
-
The View-level Levenshtein tolerance default.
- $globalOperator : string
-
The View-level operator default (Logic::AND / Logic::OR).
- $separators : string|array<string|int, mixed>|null = null
-
The extra AND word separators (Search::SEPARATORS); null = the hyphen default.
Tags
Return values
array<string, array<string|int, string>> —Analyzer name => list of expressions, in first-seen order.
getSearchableSpecs()
Normalizes the model's `AQL::SEARCHABLE` list into a per-field specification map `field => [ (Search::REQUIRES => …)? ]`, the single source consumed by {@see prepareSearch()} (the `LIKE` sweep) and by the `searchable` fallback of {@see getViewFieldSpecs()}.
protected
getSearchableSpecs([array<string|int, mixed>|null $searchable = null ]) : array<string, array<string, mixed>>
Each entry of the list is either:
- a plain field name (string) → a public field, no options;
- an array carrying the field name under Search::KEY plus its
options (e.g. Search::REQUIRES) → keeps the list homogeneous
(no mixed numeric/string keys). The map form
field => [ … ]is also tolerated (the field falls back to the entry key).
Parameters
- $searchable : array<string|int, mixed>|null = null
-
An explicit list overriding the model's
searchableproperty.
Return values
array<string, array<string, mixed>>getViewFieldSpecs()
Normalizes the searched fields of the `AQL::VIEW` declaration into a per-field specification map `field => [ Search::BOOST => float, … ]` — the single source of truth from which {@see getViewSearchFields()} derives the boost map and {@see prepareViewSearch()} resolves the per-field options.
protected
getViewFieldSpecs() : array<string, array<string, mixed>>
Search::FIELDS entries accept a numeric boost shorthand or an
array carrying Search::BOOST, Search::FUZZY,
Search::ANALYZER, Search::LANG, Search::PHRASE and
Search::REQUIRES;
when the declaration has no fields, the model's searchable list is used with a
neutral boost. A per-field option is kept in the spec only when it is
explicitly declared — an absent key means "inherit the View-level
default", which prepareViewSearch() resolves so that an explicit
value (0 included) overrides the global tolerance.
Tags
Return values
array<string, array<string, mixed>>getViewSearchFields()
Normalizes the searched fields into a `field => boost` map — a boost-only façade over {@see getViewFieldSpecs()}, used by {@see buildViewLink()} and {@see viewDiff()} which only care about the field paths and their weights.
protected
getViewSearchFields() : array<string, float>
Tags
Return values
array<string, float>matchFieldsAuthorized()
Decides whether every sub-field a `match` names may be read by this caller.
protected
matchFieldsAuthorized(array<string|int, mixed> $match, string $relativeBase, array<string|int, mixed>|null $fields, array<string|int, mixed> $auth) : bool
🔑 Shared by both depths. A match gates each sub-field it references on the
Field::REQUIRES of that exact sub-field, and a refused one must neutralise the
whole predicate to false — never drop it, which would loosen a none match
into an existence oracle. The flat lookup gates against the model's own
projection; the hierarchical walk gates against the projection of the level it
has reached, with the path it has accumulated. Same rule, two inputs.
Parameters
- $match : array<string|int, mixed>
-
The match payload.
- $relativeBase : string
-
The array's path relative to
$fields(attachments,resolution.steps). - $fields : array<string|int, mixed>|null
-
The projection to gate against, or null when the model declares none (nothing to inherit).
- $auth : array<string|int, mixed>
-
The caller's permission context.
Return values
bool —False as soon as one named sub-field is refused.
ngramMatch()
Builds the `NGRAM_MATCH(doc.<field>, @term [, threshold], "<analyzer>")` core of a {@see Search::NGRAM} branch (without the surrounding boost). The threshold is inlined only when the field declares one, else the server default applies. Shared by the whole-term and the per-word emissions.
protected
ngramMatch(array<string|int, mixed> $spec, string $path, string $term) : string
Parameters
- $spec : array<string|int, mixed>
-
The field spec (must carry a Search::NGRAM entry).
- $path : string
-
The
doc.<field>accessor. - $term : string
-
The bound term (
@search_Nor@search_N_M).
Return values
string —The NGRAM_MATCH(…) expression.
prepareFacets()
Prepare the query with AQL facets definitions.
protected
prepareFacets(array<string|int, mixed>|null $init[, array<string|int, mixed>|null &$binds = null ][, string $docRef = AQL::DOC ][, string $logicalOperator = Logic::AND ]) : string|null
Parameters
- $init : array<string|int, mixed>|null
- $binds : array<string|int, mixed>|null = null
- $docRef : string = AQL::DOC
- $logicalOperator : string = Logic::AND
Return values
string|nullprepareFilterBetween()
Prepares an inclusive `between` (range) clause: `key >= @min && key <= @max`.
protected
prepareFilterBetween(array<string|int, mixed> $init, array<string|int, mixed>|null &$binds, string $docRef, callable $resolve, bool $defaultBounds) : string|null
The compared key is alt-aware (it flows through static::alterFilterKey()).
Each bound is resolved by $resolve, which differs per filter type — a raw
bind for numbers/strings, the date machinery (now / timezone) for dates.
Bound omission is type-driven:
$defaultBounds = false(number/string): an omitted bound drops its side, yielding a one-sided range (key >= @minorkey <= @max).$defaultBounds = true(date): an omitted bound still emits a clause; the resolver maps the null value to "now", so the range is always two-sided.
Parameters
- $init : array<string|int, mixed>
-
The filter init (reads
min/max). - $binds : array<string|int, mixed>|null
-
The bind variables, populated by reference.
- $docRef : string
-
The document reference.
- $resolve : callable
-
fn(mixed $value, ?array &$binds): string — resolves a bound to AQL.
- $defaultBounds : bool
-
Whether an omitted bound still emits a clause (dates) or is dropped.
Tags
Return values
string|null —The range clause, or null when no bound was given — no
constraint expressed is no clause, never an empty one.
prepareFilterComparator()
Prepares the filter clause with a specific operator.
protected
prepareFilterComparator([array<string|int, mixed> $init = [] ]) : string
Parameters
- $init : array<string|int, mixed> = []
Tags
Return values
stringprepareFilterKey()
Prepares the filter clause with a specific key and document, with optional function transformations via 'alt' parameter.
protected
prepareFilterKey([string|array<string|int, mixed>|null $init = [] ][, string $docRef = AQL::DOC ][, array<string|int, mixed>|null &$binds = null ]) : string
Supports function chaining:
- Single function: "alt":"lower"
- Multiple functions: "alt":["trim","lower"]
- Functions with params: "alt":[["trim",1],"lower"]
Parameters
- $init : string|array<string|int, mixed>|null = []
-
Filter initialization array
- $docRef : string = AQL::DOC
-
Document reference (default: AQL::DOC)
- $binds : array<string|int, mixed>|null = null
Tags
Return values
string —The transformed key expression
prepareFilterValue()
Prepare the filter clause with a specific value to evaluates.
protected
prepareFilterValue([array<string|int, mixed>|null $init = [] ][, array<string|int, mixed>|null &$binds = null ]) : string
Binds the raw value, then applies the value-side (right) alt chain when
one is set (object form alt:{ key:.. , val:.. } or val:true mirror):
- scalar value → the chain wraps the bind placeholder, e.g.
LOWER(@value). - array value (e.g.
op:in) → the chain is mapped over each element via an inline projection, e.g.@value[* RETURN LOWER(CURRENT)]. The single bind still holds the whole array, so existing binding behavior is preserved.
Parameters
- $init : array<string|int, mixed>|null = []
- $binds : array<string|int, mixed>|null = null
Tags
Return values
stringprepareNear()
Build the `DISTANCE(...)` expression for a `?near=` anchor and bind its coordinates.
protected
prepareNear(array<string|int, mixed> $near, array<string|int, mixed>|null &$binds[, string $docRef = AQL::DOC ][, array<string|int, mixed> $init = [] ]) : string|null
The key of the payload names the geo field to order by distance from, so it is a
sort dimension and travels through the same fail-closed gate as any sort key: it
must be declared in $this->sortable (URL key → geo field path) and it inherits (or
declares) a Field::REQUIRES permission — a geo field hidden from the projection
stays untriable (no distance oracle). Returns null when the key is missing, is not
whitelisted, is refused by permission, or the coordinates are incomplete.
Parameters
- $near : array<string|int, mixed>
-
The
?near=payload ({ key, latitude, longitude }), already array-checked by the caller. - $binds : array<string|int, mixed>|null
-
Bind variables, populated by reference.
- $docRef : string = AQL::DOC
-
The document variable the fields hang off.
- $init : array<string|int, mixed> = []
-
The request-level init. Reads
Arango::AUTHORIZER.
Tags
Return values
string|null —DISTANCE(doc.<field>.latitude, doc.<field>.longitude, @lat, @lng) or null.
pushWholeTermField()
Pushes the whole-term match of one field onto the Analyzer groups — the base `doc.<field> IN TOKENS(@term, …)` (boosted when the field weight is not `1`), the optional exact-phrase bonus and the optional fuzzy branch, plus the {@see Search::NGRAM} branch when declared. This is the `OR`-grammar emission, shared by {@see buildViewSearchGroups()} (every field) and by the `OR` fields of {@see buildViewSearchGroupsWithOperator()}.
protected
pushWholeTermField(array<string|int, mixed> &$groups, array<string|int, mixed> $spec, string $path, string $term, array<string|int, mixed> $analyzers, float $weight, bool $phrase, int $fuzzy) : void
Parameters
- $groups : array<string|int, mixed>
-
Analyzer name => list of expressions, mutated by reference.
- $spec : array<string|int, mixed>
-
The resolved field spec.
- $path : string
-
The
doc.<field>accessor. - $term : string
-
The bound whole term (
@search_N). - $analyzers : array<string|int, mixed>
-
The Analyzers indexing the field (
IN TOKENSside). - $weight : float
-
The field boost.
- $phrase : bool
-
Whether to add the exact-phrase bonus.
- $fuzzy : int
-
The Levenshtein tolerance (
0disables the branch).
resolveFieldSearchSpec()
Resolves the per-field search facets shared by both group builders : the Analyzers indexing the field (a per-field {@see Search::ANALYZER} overrides the View-level one, a single Analyzer normalized to a one-element list), the boost, the fuzzy tolerance and phrase bonus (a per-field value overrides the View-level default), and the `doc.<field>` accessor (with the `[*]` array marker stripped — the flat path already matches any element of the indexed array, and the `SEARCH` grammar rejects the expansion form).
protected
resolveFieldSearchSpec(array<string|int, mixed> $spec, int|string $field, string $modelAnalyzer, bool $globalPhrase, int $globalFuzzy, string $docRef) : array{0: string[], 1: float, 2: int, 3: bool, 4: string}
Parameters
- $spec : array<string|int, mixed>
-
The resolved field spec.
- $field : int|string
-
The field key (the dotted path,
[*]allowed). - $modelAnalyzer : string
-
The View-level Analyzer.
- $globalPhrase : bool
-
The View-level phrase-bonus default.
- $globalFuzzy : int
-
The View-level Levenshtein tolerance default.
- $docRef : string
-
The document variable the field hangs off.
Return values
array{0: string[], 1: float, 2: int, 3: bool, 4: string} —[ analyzers, weight, fuzzy, phrase, path ].
resolveFilterComparator()
Translates an operator code into its AQL comparator, refusing the ones this filter cannot honour.
protected
resolveFilterComparator(mixed $op) : string
🚨 Reaching this point means the operator was not handled upstream. The filter
types intercept what they can — sw / ew / contains / regex on a string,
between on a string, a number or a date, distance on a geo field — and everything
else falls through to the infix catalogue. So an operator arriving here that the
catalogue does not carry is one of two mistakes, and both used to compile to ==:
- a code that does not exist (
zzz,GT,>), plainly a typo ; - a code that exists but not for this field:
{"key":"price","op":"sw","val":12}asks for prices starting with 12 and used to answer prices equal to 12 — a handful of plausible rows, in200, answering a question nobody asked.
The second is the dangerous one: an empty page is noticed, a wrong page is not.
⚠ An absent operator still means equality. null is the documented default and
the one case where falling back to == is what the caller meant. An empty string is
treated the same way rather than refused — an unfilled <select> submits one, and
that is an absence expressed, not a typo.
Parameters
- $op : mixed
-
The operator code supplied by the caller, if any.
Tags
Return values
string —The AQL comparator.
splitSearchTermWords()
Splits an `AND`-operator search term into its words, on whitespace plus the given extra separator characters ({@see Search::SEPARATORS}). Whitespace always splits; `$separators` adds literal characters (a string, or a list of characters joined to one), `null` falls back to the hyphen default, and an empty value splits on whitespace only. The extra characters are regex-escaped, so any punctuation is safe. Empty words are dropped.
protected
splitSearchTermWords(string $term[, string|array<string|int, mixed>|null $separators = null ]) : array<string|int, string>
Parameters
- $term : string
-
The raw comma-term (may hold several words).
- $separators : string|array<string|int, mixed>|null = null
-
The extra separator characters (string / list / null=default hyphen).
Return values
array<string|int, string> —The non-empty words, in order.
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
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.
authorizeRelationSortKey()
Resolves a sortable entry that orders on a field of a **related** document, reached through a relation this model already projects.
private
authorizeRelationSortKey(array<string|int, mixed> $entry, array<string|int, mixed> $init) : string|null
The projection of a Filter::EDGE field emits a LET, and the compiled
query places every LET before the SORT — so ordering on the related
document is a matter of naming that variable, not of traversing again:
LET authorRef = ( FOR v IN OUTBOUND doc articles_authors RETURN … )
SORT FIRST( authorRef ).name ASC
Which is why the entry names the projected field (AQL::EDGE => 'author',
a key of $this->fields) rather than an edge collection: the sort reuses
the traversal the projection already performs. One traversal serves both.
Three declarations cannot be honoured, and each is refused rather than dropped — a dropped criterion looks like a client typo, while these are faults in the model that only its author can fix:
- the named field is not projected, so there is no
LETto name; - it is not a singular relation — ordering on a plural one asks which of the related documents decides, a question the declaration does not answer;
- it carries no declared
Field::UNIQUE, so its variable is the generated random name and cannot be designated.
Permission follows the projected field: an explicit Field::REQUIRES
on the sortable entry wins, otherwise the subject of the relation field is
reused. What you cannot read, you cannot order by — otherwise the order
betrays it.
Parameters
- $entry : array<string|int, mixed>
-
The sortable definition (
AQL::EDGE,Field::PATH,Field::REQUIRES). - $init : array<string|int, mixed>
-
The request-level init. Reads
Arango::AUTHORIZER.
Tags
Return values
string|null —The FIRST( <variable> ).<path> expression, or null when refused by permission.
authorizeSortKey()
Resolve a whitelisted sort entry to its AQL field expression, gated by permission.
private
authorizeSortKey(string $key, mixed $entry, array<string|int, mixed> $init, string $docRef) : string|null
The entry (the $sortable[$key] value) is either a plain field path — a string
or an array path ([ 'address', 'city' ]) — or an explicit definition (an
associative array carrying Field::PATH and/or Field::REQUIRES). The permission
subject is resolved in two steps, aligned on the projection's Field::REQUIRES:
- explicit — a
Field::REQUIRESdeclared on the entry itself takes priority; - inherited — otherwise the subject of the homonymous field declared in
$this->fieldsis reused, so « what you cannot read, you cannot sort on ».
When a subject is resolved and isAuthorized() denies it, the key is refused
(null) and the caller drops the criterion — a field hidden from the projection
stays untriable (no sort oracle). No subject, or no authorizer injected, sorts
freely (fail-open — exactly the field-level semantics).
Parameters
- $key : string
-
The public URL key (already resolved against the whitelist).
- $entry : mixed
-
The
$sortable[$key]value (path or explicit definition). - $init : array<string|int, mixed>
-
The request-level init. Reads
Arango::AUTHORIZER. - $docRef : string
-
The document variable the field hangs off.
Tags
Return values
string|null —The doc.<field> expression, or null when the sort is refused.
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
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>collectMatchFields()
private
collectMatchFields(array<string|int, mixed> $match) : array<string|int, mixed>
Parameters
- $match : array<string|int, mixed>
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
COLLECTit counts a multi-vertex document once per vertex, inflating every other aggregate of the sameCOLLECT; - no
Field::PATHsays 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
Return values
string|null —The dimension expression, or null when refused by permission.
isSortAuthorized()
Decide whether a sort/near field is granted for the request.
private
isSortAuthorized(string $path, mixed $requires, array<string|int, mixed> $init) : bool
Two paths, mirroring the projection's own gating:
- explicit (Façon A) — an entry that declared its own
Field::REQUIRESis run through isAuthorized(); - inherited (Façon B) — otherwise the
Field::REQUIRESis inherited from the projection at the resolved$pathvia isPathAuthorized(), which descendsField::FIELDS/AQL::SKIN_FIELDSand strips[*], so a dotted/aliased path (address.salary) is gated at its exact sub-field — never at the homonym of the URL key.
Both fail open: no explicit subject with a projection that carries no
Field::REQUIRES on the path (or no authorizer injected) sorts freely —
exactly the field-level semantics, symmetric with ?filter=.
Parameters
- $path : string
-
The resolved field path (
address.salary,location.point, …). - $requires : mixed
-
The explicit
Field::REQUIRESsubject(s) declared on the entry, ornull. - $init : array<string|int, mixed>
-
The request-level init. Reads
Arango::AUTHORIZER.
Return values
bool —true when the field may be sorted on, false when refused.
isTranslatedSortEntry()
Decides whether a sortable entry orders on a **multilingual** field — one whose stored value is a translations object (`{ fr: "…", en: "…" }`) rather than the text to compare.
private
isTranslatedSortEntry(mixed $entry, string $path) : bool
Two declarations say so, and they follow the two steps Field::REQUIRES already
follows — inherited first, explicit second:
- inherited — the resolved path names a field declared
Filter::TRANSLATEin$this->fields, the very declaration that makes the projection translate it; - explicit — the entry carries
Field::FILTER => Filter::TRANSLATEitself, for a field that is sortable but not projected (nothing to inherit from).
⚠ The inherited form reads a root field (a path of a single segment). A
translated field nested inside a structural one is not walked into: declare
Field::FILTER on the entry instead. A miss is not a hole — the entry then
behaves as the stored path it has always been.
Parameters
- $entry : mixed
-
The
$sortable[$key]value (path or explicit definition). - $path : string
-
The resolved (dotted) field path.
Return values
bool —true when the entry orders on a translations object.
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>resolveSortEntry()
Resolve a whitelisted sort entry to its `[ fieldPath, requires ]` pair.
private
resolveSortEntry(string $key, mixed $entry) : array{0: mixed, 1: mixed}
The entry (the $sortable[$key] value) is either a plain field path — a string
or an array path ([ 'address', 'city' ]) — or an explicit definition (an
associative array carrying Field::PATH and/or Field::REQUIRES). Only the
explicit Field::REQUIRES (Façon A) is returned here; the inherited
permission (Façon B) is decided by isSortAuthorized() against the
resolved $path — never against the URL key — so a dotted/aliased path
(salary → address.salary) is gated at its exact (sub-)field, symmetric
with groupBy/bounds and free of the "wrong homonym" pitfall.
Shared by the textual ?sort= grammar and the ?near= distance anchor, so both
resolve a geo/scalar field the same way.
Parameters
- $key : string
-
The public URL key (already resolved against the whitelist).
- $entry : mixed
-
The
$sortable[$key]value (path or explicit definition).
Return values
array{0: mixed, 1: mixed} —The [ fieldPath, requires ] pair (requires is the
explicit subject, or null when the entry declares none).
resolveSortFallbackLang()
Resolve the **fallback** language of a multilingual sort entry — the locale used when the requested one is absent from a document, or when the call requests none.
private
resolveSortFallbackLang(mixed $entry, array<string|int, mixed> $init) : string|null
Three declaration sites, from the most local to the most general; the first that answers wins:
- the sortable entry (
Field::DEFAULT_LANG), - the model (
$this->defaultLang, see DefaultLangTrait), - the host, pushed per call (
$init[ Arango::DEFAULT_LANG ]).
⚠ The model outranks the host on purpose. What the host pushes is a default,
and a default must never override an explicit declaration — otherwise a model would
change behaviour depending on which site loads it, without a line of it moving.
Arango::LANG (the requested language) is the opposite case: an instruction, and
it wins over all three.
Parameters
- $entry : mixed
-
The
$sortable[$key]value. - $init : array<string|int, mixed>
-
The request-level init. Reads
Arango::DEFAULT_LANG.
Tags
Return values
string|null —The lowercased fallback tag, or null when none is declared.
sortCriterion()
Resolves one sort criterion through the two gates every key travels.
private
sortCriterion(string $key, string $order, array<string|int, mixed>|null $sortable, array<string|int, mixed> $init, string $docRef) : string|null
Whitelist gate (fail-closed) : a key is honoured only when the model
declares it in $sortable. No whitelist (null) means nothing sorts — the
key never reaches doc.<key>.
Permission gate : a field hidden from the projection stays untriable, so the order cannot become an oracle on what the projection withholds. A refused key drops its criterion.
Shared by the client's own criteria and by the tiebreaker, so that neither can reach a field the other could not.
Parameters
- $key : string
-
The URL key, already stripped of its direction.
- $order : string
-
ASCorDESC. - $sortable : array<string|int, mixed>|null
-
The whitelist in force for this call.
- $init : array<string|int, mixed>
-
The request-level init. Reads
Arango::AUTHORIZER. - $docRef : string
-
The document variable the fields hang off.
Tags
Return values
string|null —The <field> <order> criterion, or null when either gate refuses it.
sortTiebreakOrders()
The criteria that close the order, appended after everything the caller asked for.
private
sortTiebreakOrders(array<string, bool> $named, array<string|int, mixed>|null $sortable, array<string|int, mixed> $init, string $docRef) : array<int, string>
🚨 A pagination only means something under a total order. LIMIT and
OFFSET say « the first fifty », then « the next fifty », and first exists
only when no two documents are left level. A sort on a non-unique key orders
the groups and leaves their inside free : the store is at liberty there, two
pages may serve one document twice and another never, and it happens in 200
with nothing in the log. The model's SORT_DEFAULT closes that hole when
nothing is asked for ; this closes it when something is.
🔑 It closes the order as a whole, so there is one per model — never one
per sortable key. And it is appended to every sort the model serves,
including the synthetic distance and score, where ties are the rule rather
than the exception : two addresses equally far from a point, two documents
holding a term equally often.
🔑 Except when the order is already total, which is a property of the
criteria list, not of any one key : an order that already names the
tiebreaker cannot be refined by naming it twice. A key unique on one
collection is not unique on the next — id closes a collection of one type
and leaves an overlapping one open — which is why the model declares what
closes its own order and no universal rule can.
The tiebreaker travels the same two gates as any criterion
(sortCriterion()), so a model naming a key its whitelist does not carry
closes nothing at all — in silence, which reads as settled. Declare the
tiebreaker in AQL::SORTABLE.
Parameters
- $named : array<string, bool>
-
The keys the order already carries.
- $sortable : array<string|int, mixed>|null
-
The whitelist in force for this call.
- $init : array<string|int, mixed>
-
The request-level init.
- $docRef : string
-
The document variable the fields hang off.
Tags
Return values
array<int, string> —The criteria to append, empty when there is nothing to close.
splitSortToken()
Splits a sort token into the key it names and the direction it asks for.
private
splitSortToken(string $token) : array{0: string, 1: string}
A leading - flips the criterion to descending and is stripped ; anything
else ascends. Shared so that the client's grammar and the model's own
declarations are read the same way.
Parameters
- $token : string
-
A non-empty sort token (
'name','-created').
Return values
array{0: string, 1: string} —The [ key , order ] pair.
translatedSortExpression()
Build the ordering expression of a multilingual entry: the requested locale, then the fallback one, then the field named by `Field::ELSE`.
private
translatedSortExpression(mixed $entry, string $path, array<string|int, mixed> $init, string $docRef) : string
SORT NOT_NULL(doc.alternateName["en"], doc.alternateName["fr"], doc.name) ASC
The locale is a bracket accessor rather than a dotted one, uniformly: a tag
carrying a dash reads as a subtraction in dot notation (doc.alternateName.pt-BR),
and one shape for every tag beats a shape that depends on the tag. The tag is
written verbatim — an attribute name can never be bound — hence the guards.
Links are dropped rather than duplicated: a requested locale equal to the fallback
yields two terms, not three. Field::ELSE is optional; without it a document that
has no translation at all orders on null, which is where it ordered before.
⚠ Field::ELSE names another field, so it passes the same permission gate as any
sort key — an unreadable one is dropped from the chain instead of leaking its
values through the order.
⚠ When nothing answers — no requested locale, no fallback, no Field::ELSE — the
expression is the stored path itself, exactly what an ordinary entry would emit.
An incomplete declaration degrades to today's behaviour; it never drops the
criterion in silence.
Parameters
- $entry : mixed
-
The
$sortable[$key]value. - $path : string
-
The resolved (dotted) path of the translations object.
- $init : array<string|int, mixed>
-
The request-level init. Reads
Arango::LANGandArango::DEFAULT_LANG. - $docRef : string
-
The document variable the fields hang off.
Tags
Return values
string —The ordering expression.