Oihana PHP Arango

FederatedSearch uses ContainerTrait

The federated multi-collection search engine.

One search bar over several collections at once (customers, products, sellers, places, …), returning a single list ranked by relevance. The hard part is not finding the matches — the search-alias view substrate already searches every collection in one go — but rebuilding heterogeneous results: a customer, a product and a place have different shapes (fields, joins, skins, permissions). The engine therefore works in two stages, like a librarian who first hands you a ranked list of call numbers, then fetches each book at its own shelf:

  1. Findfind() runs one scored SEARCH over the search-alias view and returns, for every match, only its source collection, its _key and its relevance score (BM25), globally ranked and paginated (the LIMIT is applied once, on the whole ranking).
  2. Rebuildrebuild() groups the page by collection and re-hydrates each group in one list() call per collection (not per result) through the model that owns it, reusing that model's own projection pipeline (fields, joins, skin, permissions); the documents are then merged back in score order, each wrapped as { collection, score, document }.

Pagination is done once, in the cheap find stage, so rebuild only ever touches one page of documents. The total number of matches (before the LIMIT) is exposed by foundRows(), the federated counterpart of the model foundRows(), so a UI can render "X results, page Y".

This is the read-only orchestrator. It is not a Documents subclass — it owns no single collection — but a standalone, container-aware service: the container resolves the per-collection models at rebuild time.

The per-source permission gate and the HTTP triplet land in the later lots.

Tags
author

Marc Alcaraz (ekameleon)

since
1.3.0

Table of Contents

Constants

DEFAULT_LIMIT  : int = 25
The default page size of the federated SEARCH when none is supplied.
DOCUMENT  : string = 'document'
The rebuilt-document key carried by each {@see search()} result row.
SCORE  : string = 'score'
The relevance-score key carried by each result row (and the AQL `LET` score variable name).
COLLECTIONS_OPTION  : string = 'collections'
The `SEARCH … OPTIONS { collections: [...] }` key restricting the search to a subset of the view's source collections.
DISCRIMINATOR_ALIAS  : string = 'discriminator'
The RETURN alias under which the discriminator value is read back by the lightweight type lookup of a composite (polymorphic) collection.

Properties

$arangodb  : ArangoDB|null
The {@see ArangoDB} façade used to run the federated SEARCH, or null when none is configured.
$models  : array<string, string|array<string, mixed>>
The collection → model registry — the directory telling the engine which model rebuilds the documents of which collection. A value is either a model-service-id string (direct), or, for a polymorphic collection, a normalised composite spec `[ DISCRIMINATOR => field, MAP => [type => id], FALLBACK => id|null ]` routing by a discriminator field (see {@see FederatedSearchParam::MODELS}).
$requires  : array<string, string|array<string|int, mixed>>
The collection → required permission registry. A collection absent from this map is public. Each value is either a **collection-level** requirement — a subject string or an OR-list, evaluated by {@see isAuthorized()} — or a normalised **structured** cascade gate for a polymorphic collection, `[ COLLECTION => subjects|null, MAP => [ type => subjects ], FALLBACK => bool|subjects|null ]`, gating the collection first (level 1) then each type (level 2). See {@see FederatedSearchParam::REQUIRES}.
$searchable  : array<string, mixed>
The federated search specification (`fields` + `analyzer`) applied uniformly across the aggregated collections.
$skin  : string|null
The default skin (projection variant) used to rebuild the matched documents, overridable per request by `Arango::SKIN`.
$view  : string|null
The name of the `search-alias` view to query, or null when none is set.
$found  : int
The total number of matches of the last {@see find()} — before the LIMIT, exposed by {@see foundRows()}.

Methods

__construct()  : mixed
Creates a new FederatedSearch engine.
find()  : array<int, array<string, mixed>>
Stage 1 — *find*: runs one scored SEARCH over the `search-alias` view and returns the matches as a flat list ranked by relevance, each row holding only its provenance and score: `{ collection, key, score }`.
foundRows()  : int
Returns the total number of matches of the last {@see find()} / {@see search()} before the LIMIT — the federated counterpart of the model `foundRows()`, for "X results, page Y" pagination.
getViewName()  : string|null
Returns the name of the `search-alias` view the engine queries.
rebuild()  : array<int, array<string, mixed>>
Stage 2 — *rebuild*: re-hydrates a *find* result page into full documents, ranked by relevance. The matches are grouped by collection and each group is rebuilt **in one `list()` call per collection** (a `_key IN […]` filter) by the model that owns it — resolved through the collection → model registry — applying the resolved skin (request `Arango::SKIN` → the engine default → {@see Skin::DEFAULT}). The documents are then merged back in the find order, each wrapped as `{ collection, score, document }`.
search()  : array<int, array<string, mixed>>
Runs a federated search end to end: the *find* stage ranks and paginates the matches across every collection, the *rebuild* stage re-hydrates the page through each owning model. Returns a flat list ranked by relevance, each row `{ collection, score, document }`; {@see foundRows()} carries the total for pagination.
initializeDatabase()  : static
Resolves the {@see ArangoDB} façade used to run the search: an instance passed verbatim, or a container id resolved through the container. Any other value leaves the engine without a database.
initializeModels()  : static
Normalises the collection → model registry. A **direct** entry keeps its non-empty model-service-id string (`collection => 'model.x'`). A **composite** entry (a polymorphic collection routed by a discriminator field) is normalised to `[ DISCRIMINATOR => field, MAP => [type => id], FALLBACK => id|null ]`.
initializeRequires()  : static
Normalises the collection → required-permission registry. A **collection-level** entry — a subject string or an OR-list of subjects — is kept verbatim (unchanged). A **structured** entry (an associative array, the cascade gate of a polymorphic collection) is normalised to `[ COLLECTION => subjects|null, MAP => [ type => subjects ], FALLBACK => bool|subjects|null ]` by {@see normaliseCompositeRequire()}; a structured entry that gates nothing is dropped (the registry is config-trusted).
initializeSearchable()  : static
Reads the federated search spec, ignoring a non-array declaration.
initializeSkin()  : static
Reads the engine default skin, keeping {@see Skin::DEFAULT} when none is declared (a non-string value is ignored).
initializeView()  : static
Reads the `search-alias` view name, keeping only a non-empty string.
allowedCollections()  : array<int, string>
Returns the registered collections the request is authorized to search — the **level-1** (collection) gate. A collection passes when its declared collection-level requirement is granted by the request authorizer (`Arango::AUTHORIZER`), via {@see isAuthorized()}. The requirement is the value itself for a collection-level entry, or the {@see FederatedSearchParam::COLLECTION} sub-key of a structured (cascade) entry — read by {@see collectionLevelSubjects()}.
analyzerName()  : string
Returns the analyzer the federated search applies, defaulting to {@see AnalyzerType::IDENTITY} when the spec declares none.
bucketKeysByModel()  : array<string, array<int, string>>
Buckets a composite collection's keys by the model resolved from each key's discriminator value — read in one lightweight lookup ({@see readDiscriminators()}). A key whose type maps to no model (and no fallback) is dropped.
buildSearchExpression()  : string|null
Builds the SEARCH expression matching the bound term against every declared field: `doc.<field> IN TOKENS(@search, "<analyzer>")`, OR-combined. The term is bound (developer-trusted field names are inlined by {@see key()}). Returns null when no usable field is declared.
buildTypeGate()  : string|null
Builds the **level-2** (per type) gate : the discriminator predicate ANDed onto the search for every authorized polymorphic collection that declares a structured requirement, or null when none applies (no structured/composite collection, or nothing to restrict). Each per-collection clause exploits **field absence** to scope itself — a non-polymorphic collection does not index the discriminator, so it is never touched — instead of the (removed) `IS_SAME_COLLECTION`. Two shapes, all kept **before the LIMIT** so the total stays exact, the type value matched under the `identity` analyzer : <ul> <li><b>permissive</b> (unlisted types visible) — hide only the denied types: `!ANALYZER(doc.<disc> IN @denied, "identity")` (omitted when nothing is denied);</li> <li><b>strict</b> (unlisted types hidden) — keep only the allowed types, plus any document with no discriminator (other collections, via field absence): `( ANALYZER(doc.<disc> IN @allowed, "identity") || !EXISTS(doc.<disc>) )` — collapsing to `!EXISTS(doc.<disc>)` when no type is allowed.</li> </ul>
collectionLevelSubjects()  : string|array<int, string>|null
Returns the level-1 (collection) permission subject(s) declared for a collection: the requirement value itself for a collection-level entry (a subject string or an OR-list), or the {@see FederatedSearchParam::COLLECTION} sub-key for a structured (cascade) entry — null when the collection itself is public (absent, or a structured entry with no collection subject).
discriminatorField()  : string|null
Returns the discriminator field of a collection — read from its composite {@see FederatedSearchParam::MODELS} entry (the single source of truth, never redeclared in {@see FederatedSearchParam::REQUIRES}) — or null when the collection is not composite (a direct, non-polymorphic model).
documentKey()  : string|null
Returns the `_key` of a rebuilt document, reading it from an array or an object shape (a model hydrates to either). Null when absent.
isTypeVisible()  : bool
Decides whether a document of the given discriminator value is visible to the request under a collection's structured (level-2) requirement — the rebuild counterpart of {@see buildTypeGate()}, kept in lock-step through the shared {@see partitionTypes()}. A scalar or an array of types is accepted (a multi-typed document is visible when **any** of its types is); a document with no discriminator value is always visible (it belongs to a non-polymorphic shape, matched by field absence in the SEARCH gate).
normaliseCompositeModel()  : array<string, mixed>|null
Normalises a composite (polymorphic) model spec, or null when it can never resolve (no mapping and no fallback). The discriminator field defaults to {@see FederatedSearchParam::DEFAULT_DISCRIMINATOR}; the `type => model-id` mapping keeps only non-empty string pairs (declaration order = priority).
normaliseCompositeRequire()  : array<string, mixed>|null
Normalises a **structured** require entry — the cascade gate of a polymorphic collection — or null when it gates nothing (no collection subject, no type map, no fallback : equivalent to a public collection).
normaliseSubjects()  : string|array<int, string>|null
Normalises a permission subject declaration to a non-empty subject string, a cleaned OR-list of non-empty subject strings, or null (nothing usable).
partitionTypes()  : array{0: array, 1: array, 2: bool}
Partitions a structured requirement's {@see FederatedSearchParam::MAP} into the type values the request **is** and **is not** authorized to see, and decides whether the **unlisted** types are visible (the {@see FederatedSearchParam::FALLBACK} is the literal `true`, or its subjects are granted). The single source of truth shared by the SEARCH gate ({@see buildTypeGate()}) and the rebuild gate ({@see isTypeVisible()}), so the two never diverge.
readDiscriminators()  : array<string, mixed>
Reads the discriminator value of each matched key in one lightweight lookup (`FOR d IN @@collection FILTER d._key IN @keys RETURN { _key, discriminator }`), keyed by `_key`. Returns an empty map when no database is configured, so the resolution falls back per the registry spec. The field name is config-trusted but still guarded by {@see assertAttributeName()} before interpolation.
resolveModelId()  : string|null
Resolves the model-service-id for a hit from its registry spec and its discriminator value. A direct (string) spec returns it verbatim. A composite spec walks its `type => model-id` map in declaration order (priority) — accepting a scalar type or an array of types — and falls back to its fallback model-id, or null (the hit is dropped).
resolveModelInstance()  : Documents|null
Resolves a model-service-id to its {@see Documents} instance through the container. Null when the service is missing or is not a {@see Documents}.
returnExpression()  : string
Builds the RETURN expression of the find query: the document provenance (`{ collection, key }` from `PARSE_IDENTIFIER(doc._id)`) merged with its relevance score.
structuredRequire()  : array<string, mixed>|null
Returns a collection's **structured** (cascade) requirement — the normalised `[ COLLECTION, MAP, FALLBACK ]` array — or null when the collection has no requirement, or only a collection-level one (a subject string or an OR-list).

Constants

DEFAULT_LIMIT

The default page size of the federated SEARCH when none is supplied.

public int DEFAULT_LIMIT = 25

DOCUMENT

The rebuilt-document key carried by each {@see search()} result row.

public string DOCUMENT = 'document'

SCORE

The relevance-score key carried by each result row (and the AQL `LET` score variable name).

public string SCORE = 'score'

COLLECTIONS_OPTION

The `SEARCH … OPTIONS { collections: [...] }` key restricting the search to a subset of the view's source collections.

private string COLLECTIONS_OPTION = 'collections'

DISCRIMINATOR_ALIAS

The RETURN alias under which the discriminator value is read back by the lightweight type lookup of a composite (polymorphic) collection.

private string DISCRIMINATOR_ALIAS = 'discriminator'

Properties

$arangodb

The {@see ArangoDB} façade used to run the federated SEARCH, or null when none is configured.

public ArangoDB|null $arangodb = null

$models

The collection → model registry — the directory telling the engine which model rebuilds the documents of which collection. A value is either a model-service-id string (direct), or, for a polymorphic collection, a normalised composite spec `[ DISCRIMINATOR => field, MAP => [type => id], FALLBACK => id|null ]` routing by a discriminator field (see {@see FederatedSearchParam::MODELS}).

public array<string, string|array<string, mixed>> $models = []

$requires

The collection → required permission registry. A collection absent from this map is public. Each value is either a **collection-level** requirement — a subject string or an OR-list, evaluated by {@see isAuthorized()} — or a normalised **structured** cascade gate for a polymorphic collection, `[ COLLECTION => subjects|null, MAP => [ type => subjects ], FALLBACK => bool|subjects|null ]`, gating the collection first (level 1) then each type (level 2). See {@see FederatedSearchParam::REQUIRES}.

public array<string, string|array<string|int, mixed>> $requires = []

$searchable

The federated search specification (`fields` + `analyzer`) applied uniformly across the aggregated collections.

public array<string, mixed> $searchable = []

$skin

The default skin (projection variant) used to rebuild the matched documents, overridable per request by `Arango::SKIN`.

public string|null $skin = \oihana\controllers\enums\Skin::DEFAULT

$view

The name of the `search-alias` view to query, or null when none is set.

public string|null $view = null

$found

The total number of matches of the last {@see find()} — before the LIMIT, exposed by {@see foundRows()}.

private int $found = 0

Methods

__construct()

Creates a new FederatedSearch engine.

public __construct(Container $container[, array<string, mixed> $init = [] ]) : mixed
Parameters
$container : Container

The DI container, used to resolve the database and the per-collection models.

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

The engine options:

Tags
throws
DependencyException
NotFoundException

find()

Stage 1 — *find*: runs one scored SEARCH over the `search-alias` view and returns the matches as a flat list ranked by relevance, each row holding only its provenance and score: `{ collection, key, score }`.

public find([array<string, mixed> $init = [] ]) : array<int, array<string, mixed>>

The query term is bound (never inlined); the search-alias view, the search spec, the database or the term being absent each yield an empty result set (nothing to search). The query runs with fullCount, so foundRows() returns the total number of matches before the LIMIT. The full documents are not fetched here — that is rebuild().

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

The request options:

  • Arango::SEARCH — the query term (a non-empty string).
  • Arango::LIMIT — the page size (default DEFAULT_LIMIT).
  • Arango::OFFSET — the page offset (default 0).
Tags
throws
ArangoException
BindException
ReflectionException
UnsupportedOperationException
Return values
array<int, array<string, mixed>>

The ranked { collection, key, score } rows.

foundRows()

Returns the total number of matches of the last {@see find()} / {@see search()} before the LIMIT — the federated counterpart of the model `foundRows()`, for "X results, page Y" pagination.

public foundRows() : int
Return values
int

getViewName()

Returns the name of the `search-alias` view the engine queries.

public getViewName() : string|null
Return values
string|null

rebuild()

Stage 2 — *rebuild*: re-hydrates a *find* result page into full documents, ranked by relevance. The matches are grouped by collection and each group is rebuilt **in one `list()` call per collection** (a `_key IN […]` filter) by the model that owns it — resolved through the collection → model registry — applying the resolved skin (request `Arango::SKIN` → the engine default → {@see Skin::DEFAULT}). The documents are then merged back in the find order, each wrapped as `{ collection, score, document }`.

public rebuild(array<int, array<string, mixed>> $matches[, array<string, mixed> $init = [] ]) : array<int, array<string, mixed>>

A match whose collection is not in the registry (or whose model does not resolve to a Documents, or whose document the model does not return — filtered out by its own rules) is dropped: the model stays authoritative.

Parameters
$matches : array<int, array<string, mixed>>

The find() rows.

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

The request options (Arango::SKIN overrides the engine default).

Tags
throws
ArangoException
BindException
ContainerExceptionInterface
DependencyException
NotFoundException
ReflectionException
NotFoundExceptionInterface
UnsupportedOperationException
ValidationException
ConstantException
Return values
array<int, array<string, mixed>>

The ranked { collection, score, document } rows.

Runs a federated search end to end: the *find* stage ranks and paginates the matches across every collection, the *rebuild* stage re-hydrates the page through each owning model. Returns a flat list ranked by relevance, each row `{ collection, score, document }`; {@see foundRows()} carries the total for pagination.

public search([array<string, mixed> $init = [] ]) : array<int, array<string, mixed>>
Parameters
$init : array<string, mixed> = []

The request options (the query term, pagination, the skin, …).

Tags
throws
ArangoException
BindException
ConstantException
ContainerExceptionInterface
DependencyException
NotFoundException
NotFoundExceptionInterface
ReflectionException
UnsupportedOperationException
ValidationException
Return values
array<int, array<string, mixed>>

The ranked { collection, score, document } rows.

initializeDatabase()

Resolves the {@see ArangoDB} façade used to run the search: an instance passed verbatim, or a container id resolved through the container. Any other value leaves the engine without a database.

protected initializeDatabase(array<string, mixed> $init) : static
Parameters
$init : array<string, mixed>
Tags
throws
DependencyException
NotFoundException
Return values
static

initializeModels()

Normalises the collection → model registry. A **direct** entry keeps its non-empty model-service-id string (`collection => 'model.x'`). A **composite** entry (a polymorphic collection routed by a discriminator field) is normalised to `[ DISCRIMINATOR => field, MAP => [type => id], FALLBACK => id|null ]`.

protected initializeModels(array<string, mixed> $init) : static

Malformed entries are dropped (the registry is config-trusted).

Parameters
$init : array<string, mixed>
Return values
static

initializeRequires()

Normalises the collection → required-permission registry. A **collection-level** entry — a subject string or an OR-list of subjects — is kept verbatim (unchanged). A **structured** entry (an associative array, the cascade gate of a polymorphic collection) is normalised to `[ COLLECTION => subjects|null, MAP => [ type => subjects ], FALLBACK => bool|subjects|null ]` by {@see normaliseCompositeRequire()}; a structured entry that gates nothing is dropped (the registry is config-trusted).

protected initializeRequires(array<string, mixed> $init) : static
Parameters
$init : array<string, mixed>
Return values
static

initializeSearchable()

Reads the federated search spec, ignoring a non-array declaration.

protected initializeSearchable(array<string, mixed> $init) : static
Parameters
$init : array<string, mixed>
Return values
static

initializeSkin()

Reads the engine default skin, keeping {@see Skin::DEFAULT} when none is declared (a non-string value is ignored).

protected initializeSkin(array<string, mixed> $init) : static
Parameters
$init : array<string, mixed>
Return values
static

initializeView()

Reads the `search-alias` view name, keeping only a non-empty string.

protected initializeView(array<string, mixed> $init) : static
Parameters
$init : array<string, mixed>
Return values
static

allowedCollections()

Returns the registered collections the request is authorized to search — the **level-1** (collection) gate. A collection passes when its declared collection-level requirement is granted by the request authorizer (`Arango::AUTHORIZER`), via {@see isAuthorized()}. The requirement is the value itself for a collection-level entry, or the {@see FederatedSearchParam::COLLECTION} sub-key of a structured (cascade) entry — read by {@see collectionLevelSubjects()}.

private allowedCollections(array<string, mixed> $init) : array<int, string>

A collection without a declared requirement is public; without an authorizer everything is allowed (fail-open). The level-2 (per type) gate is applied separately by buildTypeGate() on the collections this returns.

Parameters
$init : array<string, mixed>

The request options (Arango::AUTHORIZER).

Return values
array<int, string>

The allowed collection names.

analyzerName()

Returns the analyzer the federated search applies, defaulting to {@see AnalyzerType::IDENTITY} when the spec declares none.

private analyzerName() : string
Return values
string

bucketKeysByModel()

Buckets a composite collection's keys by the model resolved from each key's discriminator value — read in one lightweight lookup ({@see readDiscriminators()}). A key whose type maps to no model (and no fallback) is dropped.

private bucketKeysByModel(string $collection, array<string, mixed> $spec, array<int, string> $keys, array<string, mixed> $init) : array<string, array<int, string>>

A defensive per-type gate is also applied here : when the collection declares a structured requirement, a key whose discriminator value is not visible to the request (per the same level-2 policy as buildTypeGate()) is dropped before bucketing, so a denied type never reaches a model even if the SEARCH gate was bypassed (e.g. rebuild() called on its own).

Parameters
$collection : string

The polymorphic collection.

$spec : array<string, mixed>

The normalised composite spec.

$keys : array<int, string>

The matched document keys.

$init : array<string, mixed>

The request options (Arango::AUTHORIZER).

Tags
throws
ArangoException
BindException
ConstantException
ReflectionException
UnsupportedOperationException
ValidationException
Return values
array<string, array<int, string>>

A model-service-id => keys map.

buildSearchExpression()

Builds the SEARCH expression matching the bound term against every declared field: `doc.<field> IN TOKENS(@search, "<analyzer>")`, OR-combined. The term is bound (developer-trusted field names are inlined by {@see key()}). Returns null when no usable field is declared.

private buildSearchExpression(string $term, array<string, mixed> &$binds) : string|null
Parameters
$term : string

The query term.

$binds : array<string, mixed>

The bind variables, filled by reference.

Tags
throws
BindException
Return values
string|null

buildTypeGate()

Builds the **level-2** (per type) gate : the discriminator predicate ANDed onto the search for every authorized polymorphic collection that declares a structured requirement, or null when none applies (no structured/composite collection, or nothing to restrict). Each per-collection clause exploits **field absence** to scope itself — a non-polymorphic collection does not index the discriminator, so it is never touched — instead of the (removed) `IS_SAME_COLLECTION`. Two shapes, all kept **before the LIMIT** so the total stays exact, the type value matched under the `identity` analyzer : <ul> <li><b>permissive</b> (unlisted types visible) — hide only the denied types: `!ANALYZER(doc.<disc> IN @denied, "identity")` (omitted when nothing is denied);</li> <li><b>strict</b> (unlisted types hidden) — keep only the allowed types, plus any document with no discriminator (other collections, via field absence): `( ANALYZER(doc.<disc> IN @allowed, "identity") || !EXISTS(doc.<disc>) )` — collapsing to `!EXISTS(doc.<disc>)` when no type is allowed.</li> </ul>

private buildTypeGate(array<int, string> $allowed, array<string, mixed> $init, array<string, mixed> &$binds) : string|null
Parameters
$allowed : array<int, string>

The level-1 authorized collections.

$init : array<string, mixed>

The request options (Arango::AUTHORIZER).

$binds : array<string, mixed>

The bind variables, filled by reference.

Tags
throws
BindException
Return values
string|null

collectionLevelSubjects()

Returns the level-1 (collection) permission subject(s) declared for a collection: the requirement value itself for a collection-level entry (a subject string or an OR-list), or the {@see FederatedSearchParam::COLLECTION} sub-key for a structured (cascade) entry — null when the collection itself is public (absent, or a structured entry with no collection subject).

private collectionLevelSubjects(string $collection) : string|array<int, string>|null
Parameters
$collection : string
Return values
string|array<int, string>|null

discriminatorField()

Returns the discriminator field of a collection — read from its composite {@see FederatedSearchParam::MODELS} entry (the single source of truth, never redeclared in {@see FederatedSearchParam::REQUIRES}) — or null when the collection is not composite (a direct, non-polymorphic model).

private discriminatorField(string $collection) : string|null
Parameters
$collection : string
Return values
string|null

documentKey()

Returns the `_key` of a rebuilt document, reading it from an array or an object shape (a model hydrates to either). Null when absent.

private documentKey(mixed $document) : string|null
Parameters
$document : mixed
Return values
string|null

isTypeVisible()

Decides whether a document of the given discriminator value is visible to the request under a collection's structured (level-2) requirement — the rebuild counterpart of {@see buildTypeGate()}, kept in lock-step through the shared {@see partitionTypes()}. A scalar or an array of types is accepted (a multi-typed document is visible when **any** of its types is); a document with no discriminator value is always visible (it belongs to a non-polymorphic shape, matched by field absence in the SEARCH gate).

private isTypeVisible(array<string, mixed> $structured, mixed $type, array<string, mixed> $init) : bool
Parameters
$structured : array<string, mixed>

The normalised structured requirement.

$type : mixed

The discriminator value (string, array, or null).

$init : array<string, mixed>

The request options (Arango::AUTHORIZER).

Return values
bool

normaliseCompositeModel()

Normalises a composite (polymorphic) model spec, or null when it can never resolve (no mapping and no fallback). The discriminator field defaults to {@see FederatedSearchParam::DEFAULT_DISCRIMINATOR}; the `type => model-id` mapping keeps only non-empty string pairs (declaration order = priority).

private normaliseCompositeModel(array<string, mixed> $spec) : array<string, mixed>|null
Parameters
$spec : array<string, mixed>
Return values
array<string, mixed>|null

normaliseCompositeRequire()

Normalises a **structured** require entry — the cascade gate of a polymorphic collection — or null when it gates nothing (no collection subject, no type map, no fallback : equivalent to a public collection).

private normaliseCompositeRequire(array<string, mixed> $spec) : array<string, mixed>|null

The FederatedSearchParam::COLLECTION level-1 subject(s) and each FederatedSearchParam::MAP type => subjects pair are cleaned (a string or an OR-list of non-empty strings, declaration order kept). The FederatedSearchParam::FALLBACK governing the unlisted types keeps the literal true (public), or its cleaned subject(s), or null (hidden). The discriminator field is reused from the collection's composite FederatedSearchParam::MODELS entry — it is never declared here.

Parameters
$spec : array<string, mixed>
Return values
array<string, mixed>|null

normaliseSubjects()

Normalises a permission subject declaration to a non-empty subject string, a cleaned OR-list of non-empty subject strings, or null (nothing usable).

private normaliseSubjects(mixed $subjects) : string|array<int, string>|null
Parameters
$subjects : mixed
Return values
string|array<int, string>|null

partitionTypes()

Partitions a structured requirement's {@see FederatedSearchParam::MAP} into the type values the request **is** and **is not** authorized to see, and decides whether the **unlisted** types are visible (the {@see FederatedSearchParam::FALLBACK} is the literal `true`, or its subjects are granted). The single source of truth shared by the SEARCH gate ({@see buildTypeGate()}) and the rebuild gate ({@see isTypeVisible()}), so the two never diverge.

private partitionTypes(array<string, mixed> $structured, array<string, mixed> $init) : array{0: array, 1: array, 2: bool}
Parameters
$structured : array<string, mixed>

The normalised structured requirement.

$init : array<string, mixed>

The request options (Arango::AUTHORIZER).

Return values
array{0: array, 1: array, 2: bool}

[ allowedTypes, deniedTypes, unlistedVisible ].

readDiscriminators()

Reads the discriminator value of each matched key in one lightweight lookup (`FOR d IN @@collection FILTER d._key IN @keys RETURN { _key, discriminator }`), keyed by `_key`. Returns an empty map when no database is configured, so the resolution falls back per the registry spec. The field name is config-trusted but still guarded by {@see assertAttributeName()} before interpolation.

private readDiscriminators(string $collection, string $field, array<int, string> $keys) : array<string, mixed>
Parameters
$collection : string

The polymorphic collection.

$field : string

The discriminator field name.

$keys : array<int, string>

The matched document keys.

Tags
throws
ArangoException
BindException
ConstantException
ReflectionException
UnsupportedOperationException
ValidationException
Return values
array<string, mixed>

A _key => discriminator-value map.

resolveModelId()

Resolves the model-service-id for a hit from its registry spec and its discriminator value. A direct (string) spec returns it verbatim. A composite spec walks its `type => model-id` map in declaration order (priority) — accepting a scalar type or an array of types — and falls back to its fallback model-id, or null (the hit is dropped).

private resolveModelId(string|array<string, mixed> $spec, mixed $type) : string|null
Parameters
$spec : string|array<string, mixed>

The normalised registry spec.

$type : mixed

The document discriminator value (string, array, or null).

Return values
string|null

resolveModelInstance()

Resolves a model-service-id to its {@see Documents} instance through the container. Null when the service is missing or is not a {@see Documents}.

private resolveModelInstance(string $modelId) : Documents|null
Parameters
$modelId : string
Tags
throws
DependencyException
NotFoundException
Return values
Documents|null

returnExpression()

Builds the RETURN expression of the find query: the document provenance (`{ collection, key }` from `PARSE_IDENTIFIER(doc._id)`) merged with its relevance score.

private returnExpression() : string
Tags
throws
UnsupportedOperationException
Return values
string

structuredRequire()

Returns a collection's **structured** (cascade) requirement — the normalised `[ COLLECTION, MAP, FALLBACK ]` array — or null when the collection has no requirement, or only a collection-level one (a subject string or an OR-list).

private structuredRequire(string $collection) : array<string, mixed>|null
Parameters
$collection : string
Return values
array<string, mixed>|null
On this page

Search results