SortTrait uses trait:short, \oihana\controllers\traits\DefaultLangTrait, \oihana\traits\SortDefaultTrait
Turns the textual `?sort=` grammar into an AQL `SORT` expression, and powers distance ordering via the `?near=` anchor.
?sort= grammar
A comma-separated list of keys; a leading - flips a key to descending. Each
key is resolved through the model's AQL::SORTABLE whitelist (URL key →
AQL field path); a key outside the whitelist is silently dropped. The gate is
fail-closed: when a model declares no whitelist ($sortable === null),
nothing sorts — a client key never reaches doc.<key>. When no ?sort= is
given, the model's SORT_DEFAULT applies, and it too must name whitelisted
keys (it flows through the same gate). The synthetic distance / score
keys are the exception — they are driven by ?near= / a View search and are
resolved upstream of the whitelist, so they sort even without a SORTABLE.
?sort=name,-created // SORT doc.name ASC, doc.created DESC
Total order (SORT_TIEBREAK)
A pagination only means something under a total order: LIMIT/OFFSET say
« the first fifty », and first exists only when no two documents are left
level. SORT_DEFAULT answers for the case where nothing is asked for;
SORT_TIEBREAK answers for the case where something is — it is appended to
every resolved sort, last, so it speaks only on ties:
AQL::SORTABLE => [ Prop::ID , Prop::NAME ] ,
AQL::SORT_TIEBREAK => Prop::ID ,
// ?sort=name → SORT doc.name ASC, doc.id ASC
// ?sort=id → SORT doc.id ASC (already total, nothing appended)
It closes the order as a whole, so there is one per model, never one per
sortable key — and it must be declared in AQL::SORTABLE, since it travels the
same gates as any criterion. null (the default) keeps the historical
behaviour: a named sort orders exactly what it names, ties included.
Permission gate
A whitelisted key can still be permission-gated, so a field hidden from the
projection stays untriable (no sort oracle). The gate is resolved by
authorizeSortKey() — inherited from the projection at the resolved
field path (address.salary, gated at its exact sub-field via
isPathAuthorized(), like groupBy/bounds), or declared explicitly on the
$sortable entry:
// Inherited: `salary` is gated in $fields → its sort inherits the same subject.
AQL::SORTABLE => [ Prop::NAME , Prop::SALARY ]
// Explicit: a sortable-only field (absent from the projection) carries its own gate.
AQL::SORTABLE => [ Prop::NAME , 'rank' => [ Field::PATH => 'internal.rank' , Field::REQUIRES => 'staff:read' ] ]
A denied key drops its criterion; no subject (or no authorizer injected) sorts freely.
AQL::SORTABLE notations
The whitelist is normalised by normalizeSortable() into the canonical
urlKey => fieldPath map; three forms are accepted and may be mixed:
// Indexed shorthand — token equals field (the common case, no redundant map):
AQL::SORTABLE => [ Prop::_FROM , Prop::_TO , Prop::CREATED , Prop::MODIFIED ]
// Indexed alias — public token differs from the AQL field (?sort=name → givenName):
AQL::SORTABLE => [ [ Prop::NAME => Prop::GIVEN_NAME ] , Prop::CREATED ]
// Associative (legacy) — still supported, returned untouched:
AQL::SORTABLE => [ Prop::CREATED => Prop::CREATED , Prop::NAME => Prop::GIVEN_NAME ]
Multilingual ordering
An entry may aim at one locale of a translations object and fall back:
AQL::FIELDS => [ 'alternateName' => Filter::TRANSLATE , 'name' => [] ] ,
AQL::SORTABLE => [ 'label' => [ Field::PATH => 'alternateName' , Field::ELSE => 'name' ] ] ,
// ?sort=label&lang=en →
// NOT_NULL(doc.alternateName["en"], doc.alternateName["fr"], doc.name) ASC
The chain is the requested locale (Arango::LANG), then the fallback one, then
Field::ELSE; equal links are emitted once, and a chain left empty degrades to
the stored path. The fallback is resolved by resolveSortFallbackLang(),
the expression built by translatedSortExpression().
Distance ordering (?near=)
?near={ "key":"geo", "latitude":48.85, "longitude":2.35 } provides a
reference point and exposes the synthetic sort key distance
(Schema::DISTANCE). It is sort-only — it orders, it does not filter
(pair it with a geo ?filter= to bound a radius). ?sort= stays the single
ordering authority:
?near=…alone (no?sort=) defaults toSORT <distance> ASC.?near=…&sort=-distanceorders farthest first.?near=…&sort=distance,nameorders by distance then name (you pick the priority).?near=…&sort=namekeepsnameonly — distance is not auto-appended.?sort=distancewithout?near=is dropped (no anchor).
The key names the geo field, so it is a sort dimension and passes the same
fail-closed gate as any sort key: it must be declared in AQL::SORTABLE (which
resolves the field path and, via Field::REQUIRES, gates it — a geo field hidden
from the projection stays untriable). A missing, unwhitelisted or refused key
simply drops the distance sort.
The reference point is bound (@lat / @lng) and the predicate uses
DISTANCE(doc.<field>.latitude, doc.<field>.longitude, @lat, @lng), so it is
index-accelerated by a two-field GeoIndex. Coordinates are bound only
when a distance criterion is actually emitted, so the query never declares
an unused bind variable.
Tags
Table of Contents
Properties
- $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.
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.
- initializeSortable() : $this
- Initialize the sortable array definition.
- initializeSortTiebreak() : $this
- Initialize the criterion that closes the model's order.
- prepareSort() : string|null
- Prepare the AQL `SORT` expression from the `?sort=` grammar and, optionally, the `?near=` anchor.
- prepareNear() : string|null
- Build the `DISTANCE(...)` expression for a `?near=` anchor and bind its coordinates.
- 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.
- 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.
- 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`.
Properties
$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.
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.
initializeSortable()
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
$thisprepareSort()
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.
prepareNear()
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.
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.
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.
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.