Oihana PHP Arango

ArrayPropertyController extends PropertyController uses ArrayPropertyControllerTrait

Exposes the element-level operations of an **embedded array property** of a document (a field declared in the model's `AQL::ARRAYS` option) as REST sub-resources.

It extends PropertyController — inheriting its full wiring plus get() (read the whole array) and patch() (replace the whole array) — and adds, through ArrayPropertyControllerTrait:

PATCH and PUT share the element path but not an intent: the verb disambiguates them — PATCH moves the element, PUT edits it. On the property path, PUT replaces the order of the whole array.

The six routes can be declared at once with ArrayPropertyRoute.

A write answers the array property. Declare self::RESPOND_WITH_OWNER to make it answer the owner document instead, and override ArrayPropertyControllerTrait::afterArrayWrite() to bring whatever the owner derives from that array up to date before the response is built.

Table of Contents

Constants

ADD_ITEM  : string = 'addItem'
The `addItem` controller method name (route binding).
HAS_ITEM  : string = 'hasItem'
The `hasItem` controller method name (route binding).
MOVE_ITEM  : string = 'moveItem'
The `moveItem` controller method name (route binding).
REMOVE_ITEM  : string = 'removeItem'
The `removeItem` controller method name (route binding).
REORDER_ITEMS  : string = 'reorderItems'
The `reorderItems` controller method name (route binding).
RESPOND_WITH_OWNER  : string = 'respondWithOwner'
The init key deciding what a write answers : the array property (default), or the **owner document** it belongs to.
UPDATE_ITEM  : string = 'updateItem'
The `updateItem` controller method name (route binding).

Properties

$payload  : string|array<string|int, mixed>|null
The initial payload definition to prepare a new document to insert in a collection with the POST/PATCH/PUT methods.
$respondWithOwner  : bool
Whether a write answers the owner document rather than the array property.

Methods

__construct()  : mixed
Creates a new ArrayPropertyController instance.
addItem()  : mixed
Adds one or several values to the array property of a document.
enforceI18nShape()  : ResponseInterface|null
Pre-validate the i18n-typed fields and short-circuit with a 422 if any field has an invalid shape.
generatePayload()  : array<string|int, mixed>
Prepares a key-value payload object based on the provided request and definitions.
get()  : mixed
Returns a specific document with a specific identifier.
hasItem()  : mixed
Tests whether the array property of a document contains a value.
initializePayload()  : static
Initialize the 'payload' definition used to prepare a document for insertion or replace/update.
initializeRespondWithOwner()  : static
Reads {@see self::RESPOND_WITH_OWNER} off the init, deciding what a write answers.
moveItem()  : mixed
Moves an existing value to a given position in the array property.
patch()  : mixed
Update a part of a document in a collection with a specific identifier (by default use the _key attribute).
preparePayload()  : array<string|int, mixed>
Prepare the 'payload' to insert or modify in the POST, PATCH or PUT methods.
prepareWritePayload()  : bool
Runs the full payload preparation of a write handler: the i18n shape guard, the payload extraction, the rule validation, and the relation stripping.
propertyPayload()  : mixed
Returns an associative array with a key/value definition based on the property name and the payload request object.
removeItem()  : mixed
Removes one or several values from the array property of a document.
reorderItems()  : mixed
Reorders the array property from a list of item keys — the whole new order in a single request, where {@see moveItem()} moves one element at a time.
stripRelationKeys()  : mixed
Removes from the payload the attributes registered as **relations**.
updateItem()  : mixed
Merges a partial patch into the element of the array property carrying the given item key — an **in-place edit**, where {@see moveItem()} only reorders and {@see removeItem()} only drops.
validateI18nShape()  : array<string, string>
Pre-validate the shape of i18n-typed fields in the request body.
afterArrayWrite()  : void
Runs after an array write has touched the document, and **before** the response is built. A no-op here, for a subclass to override.
beforeModelCall()  : void
Injects the request-scoped permission authorizer into the model `$init` payload before every model call.
bodyParam()  : mixed
Reads a single parameter from the parsed request body.
initializeAuthorizationContext()  : static
Resolves the capability enforcer and the permission-subject resolver from the container (each guarded by an `instanceof`, null when absent) and wires them through `initializeCapabilities()` and `initializePermissionSubjectResolver()`.
resolveItemKey()  : string|null
Resolves the item key of the array property — the attribute carried by each element that identifies it — honouring an `$init` override then the model configuration.
resolveItemValue()  : mixed
Resolves the array element value from the `{value}` route placeholder, falling back to the request body (key `value`) for complex values that cannot be in a URL.
alterPayload()  : mixed
Apply an alteration function to a payload value.
containsItemKey()  : bool
Tells whether one of the given elements carries `value` under the `itemKey` attribute (a dotted path is supported, like the model side).
extractCustomPayloadValue()  : mixed
Extract a custom type value (method-based or fallback).
extractEdgePayloadValue()  : string|null
Extract a payload 'EDGE' type value and register it in relations.
extractPayloadValue()  : mixed
Extract a single payload value based on its type definition.
extractSubPayloadValue()  : array<string|int, mixed>|null
Extract a payload 'PAYLOAD' type value (recursive payload generation).
isSimplePayload()  : bool
Determine if the payload definition is a simple value (not a complex document structure).
prefixPayloadDirectChildren()  : array<string|int, mixed>
Prefix only the direct children field names with parent key.
reloadOwner()  : object|null
Re-reads the owner document a write has just changed, **through the projection**.
reloadProperty()  : mixed
Re-reads the updated property so the response carries the stored value rather than the submitted one (`Arango::RAW` skips this round-trip).
respondAfterWrite()  : mixed
Builds the response of every array write : the hook first, the body second.
respondWithItem()  : mixed
Builds the response of an operation targeting an **existing** element: the updated array property, or a 404 when no element carries the requested item key.
runArrayOp()  : mixed
Shared skeleton for the array operations: asserts the property is configured and declared as an array field, enriches the init through {@see \oihana\controllers\traits\ModelCallTrait::beforeModelCall()}, verifies the owner document exists, then runs the given operation. Maps thrown exceptions to a standardized failure response.

Constants

MOVE_ITEM

The `moveItem` controller method name (route binding).

public string MOVE_ITEM = 'moveItem'

REMOVE_ITEM

The `removeItem` controller method name (route binding).

public string REMOVE_ITEM = 'removeItem'

REORDER_ITEMS

The `reorderItems` controller method name (route binding).

public string REORDER_ITEMS = 'reorderItems'

RESPOND_WITH_OWNER

The init key deciding what a write answers : the array property (default), or the **owner document** it belongs to.

public string RESPOND_WITH_OWNER = 'respondWithOwner'

🔑 Reach for it through the consuming class, never through this trait — ArrayPropertyController::RESPOND_WITH_OWNER. PHP 8.2+ refuses a trait constant accessed directly.

UPDATE_ITEM

The `updateItem` controller method name (route binding).

public string UPDATE_ITEM = 'updateItem'

Properties

$payload

The initial payload definition to prepare a new document to insert in a collection with the POST/PATCH/PUT methods.

public string|array<string|int, mixed>|null $payload = []
Tags
see
PayloadsTrait
example

$controller->payload = [ HttpMethod::ALL => [ Prop::NAME => AQLType::STRING , Prop::ALGORITHM => [ Arango::TYPE => AQLType::STRING , Arango::DEFAULT => JWTAlgorithm::HS256 ] , Prop::DESCRIPTION => [ Arango::TYPE => AQLType::I18N ] , Prop::ADDRESS => [ Arango::TYPE => AQLType::OBJECT , Arango::COMPRESS => true , Arango::PAYLOAD => [ Prop::STREET_ADDRESS => [ Arango::TYPE => AQLType::STRING ] , Prop::EXTENDED_ADDRESS => [ Arango::TYPE => AQLType::STRING ] , Prop::ADDRESS_LOCALITY => [ Arango::TYPE => AQLType::STRING ] , Prop::ADDRESS_COUNTRY => [ Arango::TYPE => AQLType::STRING ] , Prop::ADDRESS_DEPARTMENT => [ Arango::TYPE => AQLType::STRING ] , Prop::ADDRESS_REGION => [ Arango::TYPE => AQLType::STRING ] , Prop::POST_OFFICE_BOX_NUMBER => [ Arango::TYPE => AQLType::STRING ] , ] ] , ], HttpMethod::POST => [ Prop::IDENTIFIER => [ Arango::TYPE => AQLType::STRING ] , Prop::ACTIVE => [ Arango::VALUE => 1 ] , Prop::WITH_STATUS => [ Arango::VALUE => Status::PUBLISHED ] , Prop::ALLOW_OFFLINE_ACCESS => [ Arango::VALUE => true ] , Prop::RBAC => [ Arango::VALUE => true ] , Prop::SCOPE_HAS_PERMISSION => [ Arango::VALUE => true ] , Prop::SKIP_USER_CONSENT => [ Arango::VALUE => true ] , Prop::TOKEN_EXPIRATION => [ Arango::VALUE => 86400 ] , Prop::WEB_TOKEN_EXPIRATION => [ Arango::VALUE => 7200 ] , ], HttpMethod::PATCH => [ Prop::ALLOW_OFFLINE_ACCESS => [ Arango::TYPE => AQLType::BOOL ] , Prop::RBAC => [ Arango::TYPE => AQLType::BOOL ] , Prop::SCOPE_HAS_PERMISSION => [ Arango::TYPE => AQLType::BOOL ] , Prop::SKIP_USER_CONSENT => [ Arango::TYPE => AQLType::BOOL ] , Prop::TOKEN_EXPIRATION => [ Arango::TYPE => AQLType::INT ] , Prop::WEB_TOKEN_EXPIRATION => [ Arango::TYPE => AQLType::INT ] , ] ] ;

$respondWithOwner

Whether a write answers the owner document rather than the array property.

public bool $respondWithOwner = false

Declared by the route that mounts the controller, never by a client : it is the shape of a contract, not a per-request preference.

Methods

__construct()

Creates a new ArrayPropertyController instance.

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

The DI Container reference.

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

The optional properties to passed-in to initialize the object.

Tags
throws
ContainerExceptionInterface
DependencyException
NotFoundException
NotFoundExceptionInterface
ReflectionException

addItem()

Adds one or several values to the array property of a document.

public addItem([ServerRequestInterface|null $request = null ][, ResponseInterface|null $response = null ][, array<string|int, mixed> $args = [] ][, array<string|int, mixed> $init = [] ]) : mixed

POST /{collection}/{id}/{property} — the value(s) are read from the request body (key value); an optional side (left/right) controls the insertion end.

Parameters
$request : ServerRequestInterface|null = null
$response : ResponseInterface|null = null
$args : array<string|int, mixed> = []

Route placeholders (id).

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

Optional initialization options.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
mixed

The updated array property on success (200), or an error response (400/404).

enforceI18nShape()

Pre-validate the i18n-typed fields and short-circuit with a 422 if any field has an invalid shape.

public enforceI18nShape(ServerRequestInterface|null $request, ResponseInterface|null $response[, string|null $method = null ][, array<string|int, mixed> $init = [] ]) : ResponseInterface|null

Convenience wrapper around validateI18nShape() that builds the canonical "Unprocessable Entity" response when validation fails. Callers should return the response directly when this method returns a non-null value.

Parameters
$request : ServerRequestInterface|null

The current HTTP request.

$response : ResponseInterface|null

The current HTTP response.

$method : string|null = null

The HTTP method (POST, PATCH, PUT).

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

Optional override of the payload definitions.

Tags
throws
NotFoundException
Return values
ResponseInterface|null

Null when the body is well-formed, otherwise the 422 response to return.

generatePayload()

Prepares a key-value payload object based on the provided request and definitions.

public generatePayload(ServerRequestInterface $request[, array<string|int, mixed>|null $definitions = null ][, array<string|int, mixed> $args = [] ][, array<string|int, mixed> &$relations = [] ][, bool $throwable = false ]) : array<string|int, mixed>

This method processes the given definitions and extracts values from the request based on the type specified in the definitions.

If a type is not specified but a value is provided in the definitions, that value is directly assigned to the document.

Parameters
$request : ServerRequestInterface

The request object that contains the input data.

$definitions : array<string|int, mixed>|null = null

An array of definitions that specify the types and names of expected parameters or their predefined values. Each definition may include a type (e.g., BOOL, FLOAT, I18N, INT, etc.), a name, or a predefined value.

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

The optional arguments to initialize the document key/value.

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

The array reference to register all payload attributes with a relation behavior (edges).

$throwable : bool = false

Indicates if the method throws errors.

Tags
throws
DependencyException
NotFoundException
Return values
array<string|int, mixed>

An associative array containing the processed key-value pairs extracted or derived from the request and definitions.

get()

Returns a specific document with a specific identifier.

public get([ServerRequestInterface|null $request = null ][, ResponseInterface|null $response = null ][, array<string|int, mixed> $args = [] ][, array<string|int, mixed> $init = [] ]) : mixed

Ex: ../element?search=film Ex: ../element?facets={"location":12} Ex: ../element?facets={"type":"-event,visual/exhibition","eventStatus":"-scheduled"}

Parameters
$request : ServerRequestInterface|null = null
$response : ResponseInterface|null = null
$args : array<string|int, mixed> = []

An associative array that contains values for the current route’s named placeholders.

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

An optional associative array to initialize the method.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface

hasItem()

Tests whether the array property of a document contains a value.

public hasItem([ServerRequestInterface|null $request = null ][, ResponseInterface|null $response = null ][, array<string|int, mixed> $args = [] ][, array<string|int, mixed> $init = [] ]) : mixed

GET /{collection}/{id}/{property}/{value} — the value is read from the {value} placeholder (or the request body for complex values).

Parameters
$request : ServerRequestInterface|null = null
$response : ResponseInterface|null = null
$args : array<string|int, mixed> = []

Route placeholders (id, value).

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

Optional initialization options.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
mixed

200 when the value is present, 404 when it is absent (or 400/404 on guard failures).

initializePayload()

Initialize the 'payload' definition used to prepare a document for insertion or replace/update.

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

This method sets the $payload property based on the provided associative array. If the array contains the key Arango::PAYLOADS, its value will replace the current payload. Otherwise, the existing payload is kept.

Example:

$controller->initializePayloads
([
    Arango::PAYLOADS =>
    [
        HttpMethod::ALL =>
        [
            Prop::NAME => [ Arango::TYPE => AQLType::STRING ],
            Prop::ADDRESS     =>
            [
                Arango::TYPE     => AQLType::OBJECT ,
                Arango::COMPRESS => true ,
                Arango::PAYLOAD  =>
                [
                    Prop::STREET_ADDRESS         => [ Arango::TYPE => AQLType::STRING ] ,
                    Prop::EXTENDED_ADDRESS       => [ Arango::TYPE => AQLType::STRING ] ,
                    Prop::ADDRESS_LOCALITY       => [ Arango::TYPE => AQLType::STRING ] ,
                    Prop::ADDRESS_COUNTRY        => [ Arango::TYPE => AQLType::STRING ] ,
                    Prop::ADDRESS_DEPARTMENT     => [ Arango::TYPE => AQLType::STRING ] ,
                    Prop::ADDRESS_REGION         => [ Arango::TYPE => AQLType::STRING ] ,
                    Prop::POST_OFFICE_BOX_NUMBER => [ Arango::TYPE => AQLType::STRING ] ,
                ]
            ]
            // ... other field definitions
        ],
        HttpMethod::POST =>
        [
            Prop::ACTIVE => [ Arango::VALUE => 1 ],
            // ... other field definitions
        ],
    ],
]);
Parameters
$init : array<string|int, mixed> = []

Associative array containing the schema definition.

Return values
static

Returns the current instance for method chaining.

initializeRespondWithOwner()

Reads {@see self::RESPOND_WITH_OWNER} off the init, deciding what a write answers.

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

The controller init.

Return values
static

moveItem()

Moves an existing value to a given position in the array property.

public moveItem([ServerRequestInterface|null $request = null ][, ResponseInterface|null $response = null ][, array<string|int, mixed> $args = [] ][, array<string|int, mixed> $init = [] ]) : mixed

PATCH /{collection}/{id}/{property}/{value} — the value comes from the {value} placeholder (or body), the target index from the request body (key position). Unsupported on a sortedSet property (the sort order overrides positions) → 422.

On a property declaring an item key, {value} is that key and an unknown one answers 404 — the model rewrites the array unchanged rather than inserting a null, and the returned document carries the proof.

Parameters
$request : ServerRequestInterface|null = null
$response : ResponseInterface|null = null
$args : array<string|int, mixed> = []

Route placeholders (id, value).

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

Optional initialization options.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
mixed

The updated array property on success (200), or an error response (400/404/422).

patch()

Update a part of a document in a collection with a specific identifier (by default use the _key attribute).

public patch([ServerRequestInterface|null $request = null ][, ResponseInterface|null $response = null ][, array<string|int, mixed> $args = [] ][, array<string|int, mixed> $init = [] ]) : mixed

Example: PATCH ../collection/{id}

Parameters
$request : ServerRequestInterface|null = null
$response : ResponseInterface|null = null
$args : array<string|int, mixed> = []

An associative array that contains values for the current route’s named placeholders.

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

An optional associative array to initialize the method.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface

preparePayload()

Prepare the 'payload' to insert or modify in the POST, PATCH or PUT methods.

public preparePayload(ServerRequestInterface|null $request[, string|null $method = null ][, array<string|int, mixed> $init = [] ][, array<string|int, mixed> &$relations = [] ]) : array<string|int, mixed>

This method builds a document array based on the request body and the payload definitions corresponding to the current HTTP method.

It can optionally "compress" the document structure depending on the compress configuration.

Parameters
$request : ServerRequestInterface|null

The current HTTP request instance (may be null).

$method : string|null = null

The current HTTP method (e.g. HttpMethod::POST, HttpMethod::PATCH, HttpMethod::PUT).

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

Initialization options to customize behavior:

  • compress (array|bool) Compress behavior definition (default: false).
    • If true, the document is always compressed.
    • If an array, only compress when the current method is included (e.g. [HttpMethod::POST, HttpMethod::PATCH]).
  • payload (array) Definition to override the default payload settings.
$relations : array<string|int, mixed> = []

The array reference to register all payload attributes with a relation behavior (edges).

Tags
throws
DependencyException

If a required dependency cannot be resolved.

NotFoundException

If a required payload or service is not found.

Return values
array<string|int, mixed>

The prepared document ready for insertion or modification.

prepareWritePayload()

Runs the full payload preparation of a write handler: the i18n shape guard, the payload extraction, the rule validation, and the relation stripping.

public prepareWritePayload(ServerRequestInterface|null $request, ResponseInterface|null $response, string|null $method, array<string|int, mixed> $init, array<string|int, mixed> &$relations, mixed &$payload[, mixed &$failure = null ]) : bool

post() and update() performed these four steps identically, in the same order, with the same early returns. They are stated here once — the sequence matters (the shape guard must run before extraction, the stripping after validation, so the rules still see the relation attributes the caller sent).

It answers whether the write may proceed, and hands the response to return through $failure when it may not:

$relations = [] ;
$payload   = null ;
$failure   = null ;
$method    = $request?->getMethod() ;

if ( !$this->prepareWritePayload( $request , $response , $method , $init , $relations , $payload , $failure ) )
{
    return $failure ;
}

The verdict is a boolean, never the response object, and that is the point. fail() returns null when $response is null — the convention the controller tests rely on — so a caller branching on the truthiness of an error response cannot tell "it failed" from "it went fine": in that mode it would carry on and write a payload the rules had just refused. Production always supplies a response and never saw it; the test suite did, silently. Each guard below is therefore decided on its cause — a non-empty error list, fails() — and the response is only ever built afterwards, to be carried back.

Parameters
$request : ServerRequestInterface|null

The current HTTP request.

$response : ResponseInterface|null

The current HTTP response.

$method : string|null

The HTTP method (POST, PATCH, PUT).

$init : array<string|int, mixed>

Optional override of the payload definitions.

$relations : array<string|int, mixed>

Reference filled with the attributes registered as relations (edges).

$payload : mixed

Reference filled with the payload to write, relation keys already stripped.

$failure : mixed = null

Reference filled with the response to return when the verdict is false.

Tags
throws
DependencyException
NotFoundException
Return values
bool

True when the payload is ready to write, false when the caller must return $failure.

propertyPayload()

Returns an associative array with a key/value definition based on the property name and the payload request object.

public propertyPayload(ServerRequestInterface $request, string|null $property[, array<string|int, mixed> &$relations = [] ]) : mixed
Parameters
$request : ServerRequestInterface
$property : string|null
$relations : array<string|int, mixed> = []

removeItem()

Removes one or several values from the array property of a document.

public removeItem([ServerRequestInterface|null $request = null ][, ResponseInterface|null $response = null ][, array<string|int, mixed> $args = [] ][, array<string|int, mixed> $init = [] ]) : mixed

DELETE /{collection}/{id}/{property}/{value} — the value comes from the {value} placeholder (or the request body for complex values).

Parameters
$request : ServerRequestInterface|null = null
$response : ResponseInterface|null = null
$args : array<string|int, mixed> = []

Route placeholders (id, value).

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

Optional initialization options.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
mixed

The updated array property on success (200), or an error response (400/404).

reorderItems()

Reorders the array property from a list of item keys — the whole new order in a single request, where {@see moveItem()} moves one element at a time.

public reorderItems([ServerRequestInterface|null $request = null ][, ResponseInterface|null $response = null ][, array<string|int, mixed> $args = [] ][, array<string|int, mixed> $init = [] ]) : mixed

PUT /{collection}/{id}/{property} — the ordered keys are read from the request body (key value), like addItem(), the other operation that targets the property rather than one of its elements.

A partial list reorders what it names and keeps the rest, appended after it; unknown keys are skipped and an empty list changes nothing — a reorder never deletes. Requires the property to declare an Arango::ITEM_KEY, and is unsupported on a sortedSet property → 422 in both cases.

Parameters
$request : ServerRequestInterface|null = null
$response : ResponseInterface|null = null
$args : array<string|int, mixed> = []

Route placeholders (id).

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

Optional initialization options.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
mixed

The updated array property on success (200), or an error response (400/404/422).

stripRelationKeys()

Removes from the payload the attributes registered as **relations**.

public stripRelationKeys(mixed $payload, array<string|int, mixed> $relations) : mixed

An attribute declared with the EDGE payload type is not a field of the document: it names an edge to create, and preparePayload() / propertyPayload() register it in $relations rather than in the document. Writing it as a plain attribute would store the target reference twice — once in the edge, once inside the document — so the write handlers strip those keys before handing the payload to the model.

A no-op when nothing was registered, which is the common case.

Parameters
$payload : mixed

The payload about to be written.

$relations : array<string|int, mixed>

The relations registered during the payload extraction.

Return values
mixed

The payload without its relation keys.

updateItem()

Merges a partial patch into the element of the array property carrying the given item key — an **in-place edit**, where {@see moveItem()} only reorders and {@see removeItem()} only drops.

public updateItem([ServerRequestInterface|null $request = null ][, ResponseInterface|null $response = null ][, array<string|int, mixed> $args = [] ][, array<string|int, mixed> $init = [] ]) : mixed

PUT /{collection}/{id}/{property}/{value}{value} is the item key, and the request body is the patch itself ({"rating":5}, no envelope): the verb already says the element is being edited, so nothing has to name it again. The merge is partial — the attributes it carries overwrite theirs, the others are kept.

Requires the property to declare an Arango::ITEM_KEY (or to receive one through $init) → 422 otherwise: without a key an element could only be designated by a byte-for-byte copy of itself, which the patch being applied invalidates. An unknown key answers 404.

Parameters
$request : ServerRequestInterface|null = null
$response : ResponseInterface|null = null
$args : array<string|int, mixed> = []

Route placeholders (id, value).

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

Optional initialization options.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
mixed

The updated array property on success (200), or an error response (400/404/422).

validateI18nShape()

Pre-validate the shape of i18n-typed fields in the request body.

public validateI18nShape(ServerRequestInterface|null $request[, string|null $method = null ][, array<string|int, mixed> $init = [] ]) : array<string, string>

Inspects the payload definitions for fields typed as AQLType::I18N and checks the raw request body. If any such field is present with a non-array/object/null value (e.g. a flat string), an entry is returned for it. Callers should respond with a 422 when the result is non-empty, before invoking preparePayload() (which would otherwise drop the invalid value silently via filterLanguages()).

Parameters
$request : ServerRequestInterface|null

The current HTTP request.

$method : string|null = null

The HTTP method (POST, PATCH, PUT).

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

Optional override of the payload definitions (same shape as preparePayload's $init).

Tags
throws
NotFoundException
Return values
array<string, string>

Map of field name → error message. Empty when the body is well-formed.

afterArrayWrite()

Runs after an array write has touched the document, and **before** the response is built. A no-op here, for a subclass to override.

protected afterArrayWrite(ServerRequestInterface|null $request, array<string|int, mixed> $args, array<string|int, mixed> $init, object|null $document) : void

🔑 This is the seam the six operations lacked. ModelCallTrait::afterModelCall() is deliberately not invoked by self::runArrayOp() — the operations answer a response rather than a document, so it would have no consistent result to receive. This hook has one : the document the write returned.

⚠️ It runs before the response, which is the whole point. A controller whose owner document carries values derived from the array — totals, a count, a weight — recomputes them here, so that a response carrying the owner (self::RESPOND_WITH_OWNER) states what the write really produced rather than what stood one write ago.

It does not run when the operation answered a failure : an item key matching no element (self::respondWithItem()) touched nothing, so there is nothing to recompute.

🚨 The document it receives is the raw RETURN NEW — hydrated by the model's alters, but never passed through AQL::FIELDS. Read it for what the write changed ; never hand it back as a response. That is what the reload behind self::RESPOND_WITH_OWNER exists for.

Parameters
$request : ServerRequestInterface|null

The current PSR-7 request (null in CLI / test contexts).

$args : array<string|int, mixed>

Route placeholders (id).

$init : array<string|int, mixed>

The enriched init of the operation.

$document : object|null

The document the write returned, or null when it matched nothing.

beforeModelCall()

Injects the request-scoped permission authorizer into the model `$init` payload before every model call.

protected beforeModelCall(ServerRequestInterface|null $request, array<string, mixed> &$init) : void

Overrides the no-op ModelCallTrait::beforeModelCall(), invoked around each model call of this controller — get(), the post-write reload, the update() of patch(), and the existence probe that gates patch() and the six array operations of ArrayPropertyController. It builds a request-scoped Closure(string $subject): bool through PermissionAuthorizerTrait::buildPermissionAuthorizer() and stores it under Arango::AUTHORIZER, where the projection layer (isAuthorized()) consults it to enforce the field-level Field::REQUIRES and definition-level AQL::REQUIRES gates.

Strictly the behaviour of DocumentsController::beforeModelCall(), with the same two guards:

  • an authorizer already present in $init is left untouched (a caller, a unit test, or a subclass that set one earlier wins) ;
  • buildPermissionAuthorizer() returns null when there is no request, no enforcer, no resolver, or no authenticated user — nothing is then posed and the projection layer falls open, so a controller that never carries the authorization stack (CLI, tests) keeps its previous behaviour.
Parameters
$request : ServerRequestInterface|null

The current PSR-7 request (null in CLI / test contexts).

$init : array<string, mixed>

The init array forwarded to the model (by reference).

Tags
see
PermissionAuthorizerTrait::buildPermissionAuthorizer()
isAuthorized()

bodyParam()

Reads a single parameter from the parsed request body.

protected bodyParam(ServerRequestInterface|null $request, string $key) : mixed
Parameters
$request : ServerRequestInterface|null
$key : string
Return values
mixed

The body value, or null when absent.

initializeAuthorizationContext()

Resolves the capability enforcer and the permission-subject resolver from the container (each guarded by an `instanceof`, null when absent) and wires them through `initializeCapabilities()` and `initializePermissionSubjectResolver()`.

protected initializeAuthorizationContext([array<string, mixed> $init = [] ]) : static
Parameters
$init : array<string, mixed> = []

Same array passed to the controller constructor.

Tags
throws
DependencyException
NotFoundException
Return values
static

resolveItemKey()

Resolves the item key of the array property — the attribute carried by each element that identifies it — honouring an `$init` override then the model configuration.

protected resolveItemKey(Documents $model[, array<string|int, mixed> $init = [] ]) : string|null

Mirrors the model's own resolution, so the controller and the query it triggers always agree on what {value} designates. A null result means the property is targeted by value.

Parameters
$model : Documents
$init : array<string|int, mixed> = []
Return values
string|null

resolveItemValue()

Resolves the array element value from the `{value}` route placeholder, falling back to the request body (key `value`) for complex values that cannot be in a URL.

protected resolveItemValue(ServerRequestInterface|null $request, array<string|int, mixed> $args) : mixed
Parameters
$request : ServerRequestInterface|null
$args : array<string|int, mixed>

alterPayload()

Apply an alteration function to a payload value.

private alterPayload(mixed $value, mixed $alter) : mixed
Parameters
$value : mixed
$alter : mixed

containsItemKey()

Tells whether one of the given elements carries `value` under the `itemKey` attribute (a dotted path is supported, like the model side).

private containsItemKey(mixed $items, string $itemKey, mixed $value) : bool

The comparison is strict, which is what AQL's == does on a document attribute: a numeric key requested as the string "1" matches nothing there either, so both sides agree on what « found » means.

Parameters
$items : mixed

The array property as returned by the write.

$itemKey : string

The identifying attribute.

$value : mixed

The requested key.

Return values
bool

extractCustomPayloadValue()

Extract a custom type value (method-based or fallback).

private extractCustomPayloadValue(ServerRequestInterface $request, string|null $type, string $name, array<string|int, mixed> $args, array<string|int, mixed> $options, mixed $default) : mixed
Parameters
$request : ServerRequestInterface
$type : string|null
$name : string
$args : array<string|int, mixed>
$options : array<string|int, mixed>
$default : mixed

extractEdgePayloadValue()

Extract a payload 'EDGE' type value and register it in relations.

private extractEdgePayloadValue(ServerRequestInterface $request, string $name, string $key, array<string|int, mixed> $options, array<string|int, mixed> $args, array<string|int, mixed> &$relations, mixed $default, bool $throwable) : string|null
Parameters
$request : ServerRequestInterface
$name : string
$key : string
$options : array<string|int, mixed>
$args : array<string|int, mixed>
$relations : array<string|int, mixed>
$default : mixed
$throwable : bool
Tags
throws
NotFoundException
Return values
string|null

extractPayloadValue()

Extract a single payload value based on its type definition.

private extractPayloadValue(ServerRequestInterface $request, string $key, array<string|int, mixed> $options, array<string|int, mixed> $args, array<string|int, mixed> &$relations, bool $throwable) : mixed
Parameters
$request : ServerRequestInterface
$key : string
$options : array<string|int, mixed>
$args : array<string|int, mixed>
$relations : array<string|int, mixed>
$throwable : bool
Tags
throws
DependencyException
NotFoundException

extractSubPayloadValue()

Extract a payload 'PAYLOAD' type value (recursive payload generation).

private extractSubPayloadValue(ServerRequestInterface $request, string $parentKey, array<string|int, mixed> $options, array<string|int, mixed> $args, array<string|int, mixed> &$relations, bool $throwable) : array<string|int, mixed>|null

This method automatically prefixes nested field names with their parent key if no explicit Arango::NAME is provided.

The prefixing is done "just-in-time" only for direct children, not recursively. Nested PAYLOAD types will handle their own prefixing when they are processed.

Example:

  • Parent key: 'address'
  • Child key: 'postalCode'
  • Generated name: 'address.postalCode'
Parameters
$request : ServerRequestInterface
$parentKey : string

The parent key to use as prefix for nested fields

$options : array<string|int, mixed>
$args : array<string|int, mixed>
$relations : array<string|int, mixed>
$throwable : bool
Tags
throws
DependencyException
NotFoundException
Return values
array<string|int, mixed>|null

isSimplePayload()

Determine if the payload definition is a simple value (not a complex document structure).

private isSimplePayload(mixed $definition) : bool
Parameters
$definition : mixed
Return values
bool

prefixPayloadDirectChildren()

Prefix only the direct children field names with parent key.

private prefixPayloadDirectChildren(array<string|int, mixed> $definitions, string $prefix[, string $separator = '.' ]) : array<string|int, mixed>

Does NOT recursively process nested PAYLOAD types - they will be handled by their own extractSubPayloadValue call during generatePayload execution.

Automatically generates hierarchical names like 'address.postalCode' for fields that don't already have an explicit Arango::NAME.

Parameters
$definitions : array<string|int, mixed>

The nested payload definitions

$prefix : string

The parent key to use as prefix

$separator : string = '.'

The separator between parent and child keys (default: '.')

Return values
array<string|int, mixed>

The definitions with auto-generated names for direct children only

reloadOwner()

Re-reads the owner document a write has just changed, **through the projection**.

private reloadOwner(ServerRequestInterface|null $request, array<string|int, mixed> $args, array<string|int, mixed> $init) : object|null

🚨 The document a write returns is not the one a GET serves, and handing it back would be a quiet lie. An array write ends on RETURN NEW : the stored document, hydrated by the model's alters, but never passed through AQL::FIELDS. It therefore carries no rebuilt url, ignores Filter::TRANSLATE, exposes stored attributes the projection filters out, and — the one that bites — walks past the Field::REQUIRES gates. That last failure is not hypothetical : it is the very incident ReloadWrittenDocumentTrait was written for, on the document writes.

So the owner is read again, the way PropertyControllerGetTrait::get() reads it : beforeModelCall() poses the request-scoped authorizer and whatever scope a subclass adds, the model projects, afterModelCall() post-processes. The answer is identical to a GET by construction, because it is the same call.

⚠️ The skin is the one this controller's own get() would use — not a fixed one. A surface serving its array only in a wider skin must declare that skin, or the response will come back without the very property that was just written.

Parameters
$request : ServerRequestInterface|null
$args : array<string|int, mixed>

Route placeholders (id).

$init : array<string|int, mixed>

The enriched init of the operation.

Return values
object|null

The projected owner document, or null when it reads back as nothing.

reloadProperty()

Re-reads the updated property so the response carries the stored value rather than the submitted one (`Arango::RAW` skips this round-trip).

private reloadProperty(ServerRequestInterface|null $request, array<string|int, mixed> $args, array<string|int, mixed> $init, object|null $document) : mixed

It is a read, so it goes through the same hooks and carries the same Arango::CONDITIONS as PropertyControllerGetTrait::get() : a write whose response bypassed the scope would hand back exactly what the scope is meant to withhold.

Parameters
$request : ServerRequestInterface|null

The current PSR-7 request.

$args : array<string|int, mixed>

The route placeholders.

$init : array<string|int, mixed>

The method init array (source of the declared conditions).

$document : object|null

The document returned by the write.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
mixed

The stored property value, or null when the re-read returns nothing.

respondAfterWrite()

Builds the response of every array write : the hook first, the body second.

private respondAfterWrite(ServerRequestInterface|null $request, ResponseInterface|null $response, array<string|int, mixed> $args, array<string|int, mixed> $init, object|null $document) : mixed

The order is the reason this method exists. self::afterArrayWrite() may write to the owner document — recomputed totals, a refreshed count — and a response built before it would state the values of one write ago. Every write of this trait therefore ends here, and nowhere else.

Two shapes, decided once by the route rather than per request :

  • by default, the array property — what an element write has always answered ;
  • under self::RESPOND_WITH_OWNER, the owner document, re-read through the projection (self::reloadOwner()). One rule then holds across the surface : a write answers the new truth of the whole document, exactly as the document PATCH already does.
Parameters
$request : ServerRequestInterface|null
$response : ResponseInterface|null
$args : array<string|int, mixed>

Route placeholders (id).

$init : array<string|int, mixed>

The enriched init of the operation.

$document : object|null

The document the write returned (RETURN NEW).

respondWithItem()

Builds the response of an operation targeting an **existing** element: the updated array property, or a 404 when no element carries the requested item key.

private respondWithItem(ServerRequestInterface|null $request, ResponseInterface|null $response, array<string|int, mixed> $args, array<string|int, mixed> $init, object|null $document, string|null $itemKey, mixed $value) : mixed

The write has already run — it is guarded into a no-op on both sides (nothing merged by arrayUpdate(), nothing reordered by arrayMove()) — so the document it returned is enough to tell, at no extra query cost. A property targeted by value passes a null itemKey and skips the check entirely.

🔑 The 404 is decided before self::respondAfterWrite() is reached, so a key matching nothing neither fires self::afterArrayWrite() nor reloads the owner. The write touched no element : there is nothing to recompute, and nothing to read back.

Parameters
$request : ServerRequestInterface|null
$response : ResponseInterface|null
$args : array<string|int, mixed>

Route placeholders (id).

$init : array<string|int, mixed>

The enriched init of the operation.

$document : object|null

The document returned by the write (RETURN NEW).

$itemKey : string|null

The resolved item key, or null when the property is targeted by value.

$value : mixed

The requested item key.

runArrayOp()

Shared skeleton for the array operations: asserts the property is configured and declared as an array field, enriches the init through {@see \oihana\controllers\traits\ModelCallTrait::beforeModelCall()}, verifies the owner document exists, then runs the given operation. Maps thrown exceptions to a standardized failure response.

private runArrayOp(ServerRequestInterface|null $request, ResponseInterface|null $response, array<string|int, mixed> $args, array<string|int, mixed> $init, callable $operation) : mixed

The existence guard is the gate. The array queries build their own FILTER and do not read Arango::CONDITIONS — enriching their init would change nothing. exist() does read it (ExistQueryTrait), so an owner document outside the scope answers 404 here and the operation is never reached. That is why the guard runs for every operation, reads included: a membership answer on a document the caller may not see is itself a disclosure.

The enriched init is handed to the operation as its third argument rather than captured by the closure — a closure created at the call site captures $init by value before this method runs, so a captured copy would never see the enrichment.

afterModelCall() is deliberately not invoked here: the operations return a response, not a document, so the hook would have no consistent result to receive. Post-processing a read belongs to PropertyControllerGetTrait::get().

Parameters
$request : ServerRequestInterface|null
$response : ResponseInterface|null
$args : array<string|int, mixed>
$init : array<string|int, mixed>
$operation : callable

fn(mixed $owner, Documents $model, array $init): mixed — performs the model call and returns the response.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
On this page

Search results