Skip to content

Schema Export ​

RequestHandler::schema() reflects a request class into a JSON-serializable description of its input contract. It does not read or process any HTTP input — it only inspects the class's cached metadata, so it is safe to call anywhere (including outside a request).

The point: ship validation rules to the frontend from a single source of truth instead of duplicating them by hand.

php
public function schema(string $className): array
ParameterTypeDescription
$classNameclass-stringA Request subclass to describe

Returns: a map of input key → field schema — the same keys the client sends and ValidationException reports errors under.

Example ​

php
use Solo\RequestHandler\Attributes\{DateFormat, Items, Validate};
use Solo\RequestHandler\Attributes\Source\{FromBody, FromContext, FromRoute};
use Solo\RequestHandler\Request;

enum Visibility: string
{
    case Public = 'public';
    case Hidden = 'hidden';
}

final class UpdateProductRequest extends Request
{
    #[FromRoute]
    #[Validate('required|integer')]
    public int $id;

    #[Validate('required|string|max:255')]
    public string $name;

    #[FromBody('meta.status')]
    #[Validate('nullable|in:draft,published')]
    public ?string $status = null;

    #[Validate('nullable|string|required_if:status,published')]
    public ?string $slug = null;

    #[DateFormat('Y-m-d')]
    public ?DateTimeImmutable $publishAt = null;

    public Visibility $visibility = Visibility::Public;

    /** @var VariantRequest[] */
    #[Items(VariantRequest::class)]
    public array $variants = [];

    #[FromContext('authUserId')]
    public int $updatedBy;          // supplied by the application — not part of the schema
}

$schema = $handler->schema(UpdateProductRequest::class);

$schema is (rule lists shortened):

php
[
    'id' => [
        'property'   => 'id',
        'source'     => 'route',
        'type'       => 'int',
        'nullable'   => false,
        'required'   => true,
        'requiredIf' => null,
        'hasDefault' => false,
        'default'    => null,
        'rules'      => [['name' => 'required', 'args' => []], ['name' => 'integer', 'args' => []]],
    ],
    'name' => [
        'property' => 'name', 'source' => null, 'type' => 'string', 'nullable' => false,
        'required' => true, 'requiredIf' => null, 'hasDefault' => false, 'default' => null,
        'rules' => [/* required, string, max:255 */],
    ],
    'meta.status' => [
        'property' => 'status', 'source' => 'body', 'type' => 'string', 'nullable' => true,
        'required' => false, 'requiredIf' => null, 'hasDefault' => true, 'default' => null,
        'rules' => [/* nullable, in:draft,published */],
    ],
    'slug' => [
        // … 'requiredIf' references the input key of `status`:
        'requiredIf' => ['field' => 'meta.status', 'value' => 'published'],
    ],
    'publishAt' => [
        // … 'type' => 'DateTimeImmutable',
        'format' => 'Y-m-d',
    ],
    'visibility' => [
        // … 'type' => 'App\Visibility', 'hasDefault' => true,
        'default' => 'public',
        'cases'   => ['public', 'hidden'],
    ],
    'variants' => [
        // … 'type' => 'array',
        'items' => [
            'sku' => [/* field schema of VariantRequest::$sku */],
        ],
    ],
]

Field schema ​

KeyTypeDescription
propertystringThe PHP property name
source'body' | 'route' | nullWhere the client sends it; null = the handle method's default bag (body or query)
type?stringDeclared PHP type (null when untyped)
nullableboolWhether the type allows null
requiredboolLiteral required rule present
requiredIf{field, value} | nullFirst well-formed required_if; field is the referenced field's input key
hasDefaultboolWhether the property declares a default
defaultmixedThe default (an enum case as its value); null without one
ruleslist<{name, args}>Full normalized rule list
formatstringOnly for #[DateFormat] dates
caseslist<int|string>Only for BackedEnum properties: the allowed values
caststringOnly when the built-in cast differs from type: json (any JSON value, stored encoded), or the #[Cast] of a union / mixed property
emptyToNulltrueOnly with #[EmptyToNull]: a blank string is read as no value
noTrimtrueOnly with #[NoTrim]: the string is kept as sent instead of auto-trimmed
fieldsfield schemasOnly for a nested DTO property
itemsfield schemasOnly for an #[Items] property: the schema of one element
alsolist<field schema>Only when several properties read this input key: the others, in declaration order

Notes ​

Only what the client sends ​

Generated (#[Generator]) and #[FromContext] properties are left out — the client never sends them. #[Exclude] describes output (toArray()), not input, so excluded properties are included.

required vs requiredIf ​

required reflects the literal required rule only. Conditional requiredness lives in requiredIf — the first well-formed required_if, as {field, value} — and the rule itself is still present in rules. A required_if missing an argument is treated as absent.

Shared input keys ​

Two properties may read the same input — #[FromRoute] on $id and #[FromRoute('id')] on $productId, say. The field is described by the first one in declaration order; the others follow under also, each with its own property, rules and the rest. The client has to satisfy all of them, as validation errors for the key are merged too.

Nested schemas and recursion ​

fields and items hold the full schema of the nested class. A class that recurs on its own nesting path (a category tree with #[Items(CategoryRequest::class)] inside CategoryRequest) is referenced as ['$ref' => 'App\Requests\CategoryRequest'] instead of being expanded again.

Rule normalization ​

Each rule string is split with the same semantics the handler uses internally:

  • the raw string is split on |;
  • name is the part before the first :;
  • args is the comma-split remainder (an empty list when there is no :).

JSON-safe by construction ​

Every emitted value is a scalar or array — no ReflectionProperty, enums, objects, or closures leak through — so the result round-trips losslessly through json_encode / json_decode.

Changes from 3.x ​

The schema is keyed by input key (3.x used the property name), lists only client inputs (3.x included context and generated properties), dropped the exclude flag, and gained property, default, format, cases, cast, emptyToNull, noTrim, fields, items and also.

Released under the MIT License.