Documentation Index
Fetch the complete documentation index at: https://docs.apivalk.com/llms.txt
Use this file to discover all available pages before exploring further.
Flat properties (StringProperty, IntegerProperty, EnumProperty, …) cover most fields. When a payload carries structured data — a nested object or a list of objects — you compose it with three classes:
AbstractPropertyCollection — an iterable container of properties, mode-aware so you can vary fields between CREATE, UPDATE, VIEW, LIST, DELETE.
AbstractObjectProperty — a property whose value is an object; it returns a collection.
ArrayProperty — a property whose value is an array of objects; it wraps an AbstractObjectProperty.
Scenario
We’ll model a POST /api/v1/orders request that looks like this:
{
"customer": {
"email": "jane@example.com",
"shipping_address": {
"line1": "1 Main St",
"city": "Berlin",
"country": "DE"
}
},
"items": [
{"sku": "ABC-123", "quantity": 2},
{"sku": "XYZ-999", "quantity": 1}
]
}
Two nested shapes: a single customer object (with a nested shipping_address), and an items array of uniform objects.
1. Leaf collection: AddressPropertyCollection
Start with the innermost shape. A collection takes a mode constant in its constructor — you only need to branch when a field is present in some modes but not others.
<?php
declare(strict_types=1);
namespace App\Http\Schema\Order;
use apivalk\apivalk\Documentation\Property\AbstractPropertyCollection;
use apivalk\apivalk\Documentation\Property\EnumProperty;
use apivalk\apivalk\Documentation\Property\StringProperty;
class AddressPropertyCollection extends AbstractPropertyCollection
{
public function __construct(string $mode)
{
$this->addProperty(new StringProperty('line1', 'Street address line 1'));
$this->addProperty((new StringProperty('line2', 'Optional second line'))->setIsRequired(false));
$this->addProperty(new StringProperty('city', 'City'));
$this->addProperty(new EnumProperty('country', 'ISO-3166 alpha-2 code', ['DE', 'AT', 'CH', 'FR', 'NL']));
}
}
2. Object wrapper: AddressObjectProperty
The property that lives inside another schema and plugs the collection into the framework’s object machinery:
<?php
declare(strict_types=1);
namespace App\Http\Schema\Order;
use apivalk\apivalk\Documentation\Property\AbstractObjectProperty;
use apivalk\apivalk\Documentation\Property\AbstractPropertyCollection;
class AddressObjectProperty extends AbstractObjectProperty
{
/** @var string */
private $mode;
public function __construct(string $propertyName, string $description, string $mode)
{
parent::__construct($propertyName, $description);
$this->mode = $mode;
}
public function getPropertyCollection(): AbstractPropertyCollection
{
return new AddressPropertyCollection($this->mode);
}
public function toArray(): array
{
// Rarely used — subclass only needs it if you plan to emit instances from response classes.
return [];
}
}
3. Composite collection: CustomerPropertyCollection
Customer contains email plus the nested shipping_address object. Compose via addProperty(new AddressObjectProperty(...)):
<?php
declare(strict_types=1);
namespace App\Http\Schema\Order;
use apivalk\apivalk\Documentation\Property\AbstractPropertyCollection;
use apivalk\apivalk\Documentation\Property\StringProperty;
class CustomerPropertyCollection extends AbstractPropertyCollection
{
public function __construct(string $mode)
{
$this->addProperty(new StringProperty('email', 'Customer email'));
$this->addProperty(new AddressObjectProperty('shipping_address', 'Where to ship', $mode));
}
}
And the wrapper:
class CustomerObjectProperty extends AbstractObjectProperty
{
/** @var string */
private $mode;
public function __construct(string $propertyName, string $description, string $mode)
{
parent::__construct($propertyName, $description);
$this->mode = $mode;
}
public function getPropertyCollection(): AbstractPropertyCollection
{
return new CustomerPropertyCollection($this->mode);
}
public function toArray(): array
{
return [];
}
}
4. Line items: ArrayProperty of objects
For "items": [...] you wrap an AbstractObjectProperty in an ArrayProperty.
class OrderItemPropertyCollection extends AbstractPropertyCollection
{
public function __construct(string $mode)
{
$this->addProperty(new StringProperty('sku', 'Stock keeping unit'));
$this->addProperty(
(new IntegerProperty('quantity', 'Units to buy', IntegerProperty::FORMAT_INT32))
->setExample('1')
);
}
}
class OrderItemObjectProperty extends AbstractObjectProperty
{
/** @var string */
private $mode;
public function __construct(string $propertyName, string $description, string $mode)
{
parent::__construct($propertyName, $description);
$this->mode = $mode;
}
public function getPropertyCollection(): AbstractPropertyCollection
{
return new OrderItemPropertyCollection($this->mode);
}
public function toArray(): array
{
return [];
}
}
5. Use them in a request
<?php
declare(strict_types=1);
namespace App\Http\Request\Order;
use apivalk\apivalk\Documentation\ApivalkRequestDocumentation;
use apivalk\apivalk\Documentation\Property\AbstractPropertyCollection;
use apivalk\apivalk\Documentation\Property\ArrayProperty;
use apivalk\apivalk\Http\Request\AbstractApivalkRequest;
use App\Http\Schema\Order\CustomerObjectProperty;
use App\Http\Schema\Order\OrderItemObjectProperty;
class CreateOrderRequest extends AbstractApivalkRequest
{
public static function getDocumentation(): ApivalkRequestDocumentation
{
$doc = new ApivalkRequestDocumentation();
$doc->addBodyProperty(
new CustomerObjectProperty('customer', 'The purchaser', AbstractPropertyCollection::MODE_CREATE)
);
$doc->addBodyProperty(
new ArrayProperty(
'items',
'Line items to purchase',
new OrderItemObjectProperty('item', 'A single line item', AbstractPropertyCollection::MODE_CREATE)
)
);
return $doc;
}
}
Reading nested data in the controller
The ParameterBag magic getter returns the raw typed value. For nested objects and arrays of objects, it returns an array:
public function __invoke(ApivalkRequestInterface $request): AbstractApivalkResponse
{
$customer = $request->body()->customer; // array{email, shipping_address: array{...}}
$items = $request->body()->items; // list<array{sku, quantity}>
$shippingCountry = $customer['shipping_address']['country'];
$totalQty = \array_sum(\array_column($items, 'quantity'));
// ...
}
RequestValidationMiddleware already validated every level before you got here — line1 is a non-empty string, country is one of the enum values, quantity is an integer. If anything was wrong the client got a 422 with a field path like items.0.quantity.
Mode-specific fields
Branch on the $mode you pass down from the top:
class CustomerPropertyCollection extends AbstractPropertyCollection
{
public function __construct(string $mode)
{
// Show the customer_uuid in responses but not in create bodies
if (\in_array($mode, [self::MODE_VIEW, self::MODE_LIST], true)) {
$this->addProperty(new StringProperty('customer_uuid', 'Internal identifier'));
}
$this->addProperty(new StringProperty('email', 'Customer email'));
$this->addProperty(new AddressObjectProperty('shipping_address', 'Where to ship', $mode));
}
}
Pass AbstractPropertyCollection::MODE_VIEW when composing the response, MODE_CREATE when composing the request — same collection class, correct schema in each place.
Tips
- Keep wrappers boring. An
AbstractObjectProperty subclass usually only carries the $mode and delegates to a collection. Logic belongs in the collection (or the domain).
- Reuse wrappers across requests and responses.
CustomerObjectProperty used in CreateOrderRequest is the same class you’d use in a ViewOrderResponse — swap the mode.
- Validators stay per-property. Call
setMinLength, setMaxLength, setPattern, setIsRequired, etc. on the individual StringProperty / IntegerProperty / etc. instances inside the collection.
See also: the Property reference and Property Collection reference.