SortTrait uses trait:short, \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
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 ]
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.
Methods
- bind() : string
- Bind a value to an AQL query variable.
- bindCollection() : string
- Bind a collection name to an AQL query variable.
- 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.
- 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.
- 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.
- resolveSortEntry() : array{0: mixed, 1: mixed}
- Resolve a whitelisted sort entry to its `[ fieldPath, requires ]` pair.
Properties
$sortable
The collection (map) of all the sortable fields.
public
array<string|int, mixed>|null
$sortable
= null
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.
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
$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).
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.
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.
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.
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).