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:
- the seed set — the documents matching the filter, exactly like the sibling;
- nothing more when that set is empty. This is the nominal case, and it must not cost a single extra query;
- 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
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
$cache
protected
Memcached
$cache
$cacheKey
protected
string
$cacheKey
$field
protected
string
$field
= self::DEFAULT_FIELD
$filter
protected
array<string|int, mixed>|null
$filter
= null
$logger
protected
LoggerInterface|null
$logger
= null
$model
protected
Documents
$model
$parent
protected
Closure
$parent
$ttl
protected
int
$ttl
= self::DEFAULT_TTL
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.
nullseeds with the whole collection. - $parent : Closure
-
The parentage rule:
fn( int|string $id ) : int|string|null, returningnullat a root. It carries no default, hence the mandatory$filterbefore 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.
0disables the cache (debugging only). - $logger : LoggerInterface|null = null
-
Optional logger.
invalidate()
Drops the cached set.
public
invalidate() : void
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.
nullreads them all.
Tags
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::INkeeps the model's own field declaration and restricts it to the collected key. It must beINrather than an emptyArango::QUERY_FIELDS: the latter replaces the declaration instead of filtering it, and would read a rawdoc.<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::FIELDScatches the case where the key is NOT declared: the intersection is then empty, no declaration survives, andINalone 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.
nullreads them all.
Return values
array<string, mixed>load()
Resolves the seed set, then the values inheriting from it.
protected
load() : array<int, int|string>