Oihana PHP Arango

InheritedFieldSetResolver extends DocumentFieldSetResolver

Resolves — and caches — a set of field values that documents INHERIT from their ancestors.

DocumentFieldSetResolver answers "which documents match this filter?". This one answers "which documents match it, or descend from one that does?", for the trees whose identifiers encode the hierarchy: the parent is computed from the identifier itself, so the ancestry is walked without ever reading the graph.

The parentage rule belongs to the consumer, not to the lib: it is injected as a closure taking an identifier and returning its parent — or null at a root.

$resolver = new InheritedFieldSetResolver
(
    model    : $terms ,
    cache    : $memcached ,
    cacheKey : 'terms.disabled.inherited' ,
    filter   : [ 'status' => 'disabled' ] ,
    parent   : fn( int|string $id ) => strlen( (string) $id ) > 3
             ? substr( (string) $id , 0 , -2 )
             : null
) ;

Two reads at most, and the second one is conditional:

  1. the seed set — the documents matching the filter, exactly like the sibling;
  2. nothing more when that set is empty. This is the nominal case, and it must not cost a single extra query;
  3. otherwise the whole collection, each value walking up its ancestors until one of them belongs to the seed set.

The NATIVE type of each value is preserved, never normalised — the doctrine of the sibling, and it matters twice as much here: AQL does not coerce across types (5 NOT IN ["5"] is true), so a normalised set would filter nothing at all, silently. Membership is tested strictly, and the consumer's closure is expected to return the type its own documents carry.

Fail-open, but graduated: a failing read must never WIDEN the resolved set. A failing seed read yields an empty set, like the sibling; a failing expansion yields the seed set alone rather than nothing. Both are logged, distinctly, and neither propagates.

Tags
author

Marc Alcaraz

since
1.6.0

Table of Contents

Constants

DEFAULT_FIELD  : string = 'id'
The field collected when none is given — the foreign reference a consuming document usually stores, rather than the ArangoDB `_key`.
DEFAULT_TTL  : int = 3600
Default cache TTL in seconds (1 hour) — a safety net, not the primary refresh path (the write signals are).
MAX_DEPTH  : int = 32
How many ancestors a single walk may climb before it is abandoned.

Properties

$cache  : Memcached
$cacheKey  : string
$field  : string
$filter  : array<string|int, mixed>|null
$logger  : LoggerInterface|null
$model  : Documents
$parent  : Closure
$ttl  : int

Methods

__construct()  : mixed
Creates a new InheritedFieldSetResolver.
invalidate()  : void
Drops the cached set.
values()  : array<int, int|string>
Returns the set of values, de-duplicated and re-indexed.
collect()  : array<int, int|string>
Reads the documents matching the given filter and collects their field values.
expand()  : array<int, int|string>
Adds the documents inheriting from the seed set.
inherits()  : bool
Walks the ancestors of a value, looking for one that belongs to the seed set.
listInit()  : array<string, mixed>
Builds the model init of one read, narrowed to the collected field.
load()  : array<int, int|string>
Resolves the seed set, then the values inheriting from it.

Constants

DEFAULT_FIELD

The field collected when none is given — the foreign reference a consuming document usually stores, rather than the ArangoDB `_key`.

public string DEFAULT_FIELD = 'id'

DEFAULT_TTL

Default cache TTL in seconds (1 hour) — a safety net, not the primary refresh path (the write signals are).

public int DEFAULT_TTL = 3600

MAX_DEPTH

How many ancestors a single walk may climb before it is abandoned.

public int MAX_DEPTH = 32

A faulty closure can loop — returning its own input, or two identifiers pointing at each other. The depth cap and the per-path memory below make such a closure a logged non-event rather than a hung worker.

Properties

Methods

__construct()

Creates a new InheritedFieldSetResolver.

public __construct(Documents $model, Memcached $cache, string $cacheKey, array<string|int, mixed>|null $filter, Closure $parent[, string $field = self::DEFAULT_FIELD ][, int $ttl = self::DEFAULT_TTL ][, LoggerInterface|null $logger = null ]) : mixed
Parameters
$model : Documents

The collection to read.

$cache : Memcached

The shared Memcached connection.

$cacheKey : string

Cache key — REQUIRED and unique per resolver: two resolvers behind one key would serve each other's set.

$filter : array<string|int, mixed>|null

The filter selecting the SEED documents, in the model filter shape. null seeds with the whole collection.

$parent : Closure

The parentage rule: fn( int|string $id ) : int|string|null, returning null at a root. It carries no default, hence the mandatory $filter before it.

$field : string = self::DEFAULT_FIELD

The field whose values are collected, and the one the closure receives.

$ttl : int = self::DEFAULT_TTL

Cache TTL in seconds. 0 disables the cache (debugging only).

$logger : LoggerInterface|null = null

Optional logger.

values()

Returns the set of values, de-duplicated and re-indexed.

public values() : array<int, int|string>

An empty array means "nothing matched": the caller should then pose no predicate at all rather than an always-true NOT IN [].

Return values
array<int, int|string>

collect()

Reads the documents matching the given filter and collects their field values.

protected collect(array<string|int, mixed>|null $filter) : array<int, int|string>

Mechanics only — read, extract, de-duplicate. The fail-open policy lives in load(): a subclass reading twice must be able to tell WHICH read failed, which a catch buried here would hide.

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

The filter selecting the documents, in the model filter shape. null reads them all.

Tags
throws
Throwable

Whatever the underlying read throws.

Return values
array<int, int|string>

expand()

Adds the documents inheriting from the seed set.

protected expand(array<int, int|string> $seed) : array<int, int|string>
Parameters
$seed : array<int, int|string>

The non-empty seed set.

Return values
array<int, int|string>

inherits()

Walks the ancestors of a value, looking for one that belongs to the seed set.

protected inherits(int|string $value, array<int, int|string> $seed) : bool

The walk starts at the PARENT: a value already in the seed set has nothing to prove, and is filtered out before this is called.

Parameters
$value : int|string

The value to walk up from.

$seed : array<int, int|string>

The seed set.

Return values
bool

true as soon as an ancestor belongs to the seed set.

listInit()

Builds the model init of one read, narrowed to the collected field.

protected listInit(array<string|int, mixed>|null $filter) : array<string, mixed>

The expansion reads the WHOLE collection, where an un-projected read would return every document in full — and emit the sub-query of every declared join and edge along with it.

Narrowing takes two keys, and the pair is not redundant:

  • Arango::IN keeps the model's own field declaration and restricts it to the collected key. It must be IN rather than an empty Arango::QUERY_FIELDS: the latter replaces the declaration instead of filtering it, and would read a raw doc.<field> — the wrong attribute whenever the model declares that key against another one (Field::NAME), yielding an empty set with no error at all.
  • Arango::FIELDS catches the case where the key is NOT declared: the intersection is then empty, no declaration survives, and IN alone would fall back on the whole document — the opposite of the intent.

A dotted field is left un-projected: it would render as an unquoted a.b object key, which is not valid AQL. The wide read is slower, never wrong.

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

The filter selecting the documents. null reads them all.

Return values
array<string, mixed>

load()

Resolves the seed set, then the values inheriting from it.

protected load() : array<int, int|string>
Return values
array<int, int|string>
On this page

Search results