Oihana PHP Arango

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 to SORT <distance> ASC.
  • ?near=…&sort=-distance orders farthest first.
  • ?near=…&sort=distance,name orders by distance then name (you pick the priority).
  • ?near=…&sort=name keeps name only — distance is not auto-appended.
  • ?sort=distance without ?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
author

Marc Alcaraz

since
1.0.0

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
throws
BindException

If the provided bind variable name is invalid.

Return values
string

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

bindCollection()

Bind a collection name to an AQL query variable.

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

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

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

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

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

Optional initialization array with keys:

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

If the bind variable name is invalid.

Return values
string

The formatted bind variable representing the collection.

bindView()

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

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

Reference to the array of existing bind variables.

Tags
throws
BindException

If the bind variable name is invalid.

Return values
string

The formatted bind variable representing the View.

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
$this

prepareSort()

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) and Arango::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
throws
BindException

When a bound coordinate cannot be registered.

example

Plain field sort

$model->prepareSort( [ Arango::SORT => 'name,-created' ] ) ;
// "doc.name ASC, doc.created DESC"

Distance sort (nearest first) via ?near=

$binds = [] ;
$model->prepareSort
(
    [ Arango::NEAR => [ FilterParam::KEY => 'geo' , 'latitude' => 48.85 , 'longitude' => 2.35 ] ] ,
    binds : $binds
) ;
// "DISTANCE(doc.geo.latitude, doc.geo.longitude, @lat, @lng) ASC"

Distance then name

$model->prepareSort
(
    [ Arango::SORT => 'distance,name' , Arango::NEAR => [ ... ] ] ,
    binds : $binds
) ;
// "DISTANCE(...) ASC, doc.name ASC"
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
throws
BindException

When a bound coordinate cannot be registered.

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::REQUIRES declared on the entry itself takes priority;
  • inherited — otherwise the subject of the homonymous field declared in $this->fields is 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::REQUIRES is run through isAuthorized();
  • inherited (Façon B) — otherwise the Field::REQUIRES is inherited from the projection at the resolved $path via isPathAuthorized(), which descends Field::FIELDS / AQL::SKIN_FIELDS and 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::REQUIRES subject(s) declared on the entry, or null.

$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 (salaryaddress.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).

On this page

Search results