Quick Start
Declare an Input Class
Mark the class #[Input] and declare its fields as constructor parameters. A final readonly class keeps the DTO unchangeable once it is built.
<?php
declare(strict_types=1);
namespace App\Inputs;
use Solo\Input\Attributes\{Input, Validate};
#[Input]
final readonly class CreateProductInput
{
public function __construct(
#[Validate('required|string|max:255')]
public string $name,
#[Validate('required|numeric|min_value:0')]
public float $price,
#[Validate('integer|min_value:0')]
public int $stock = 0,
#[Validate('nullable|string')]
public ?string $description = null,
) {
}
}Build It from the Request
InputFactory reads the PSR-7 request, validates and casts every field, then calls the constructor:
use App\Inputs\CreateProductInput;
use Solo\Input\InputFactory;
use Solo\Input\Exceptions\ValidationException;
final class ProductController
{
public function __construct(
private readonly InputFactory $factory,
private readonly ProductService $products,
) {
}
public function store(ServerRequestInterface $request): ResponseInterface
{
try {
$input = $this->factory->fromBody(CreateProductInput::class, $request);
} catch (ValidationException $e) {
return $this->json(['errors' => $e->getErrors()], 422);
}
$this->products->create($input);
return $this->json(['status' => 'created'], 201);
}
}Reading the Input
$input = $factory->fromBody(CreateProductInput::class, $request);
$input->name; // typed field access
$input->description; // null when the client left it out
Inputs::isProvided($input, 'description'); // was it sent — even as null?
Inputs::toArray($input); // every field but #[Exclude] ones
Inputs::toArray($input, onlyProvided: true); // only what the client sentEvery field is initialized: the constructor ran with the client's values and the defaults of the fields it left out. There is no "missing" state to check before reading a field.
Required vs Optional Fields
// Required: no default — absent fails `required`
#[Validate('string')]
public string $username,
// Required, but may be null: the key must be sent
public ?string $nickname,
// Optional: absent takes the default
#[Validate('nullable|string')]
public ?string $bio = null,
#[Validate('integer')]
public int $page = 1,
// No #[Validate] — the factory reads and casts the value, the validator never sees a rule for it
public ?string $internal = null,PHP expects required parameters before optional ones, so declare the fields without defaults first. A field required only under a condition (required_if) is optional to PHP: give it a default and let the rule decide.
Null vs Empty String
The factory distinguishes absent fields, explicit null and empty string "". See Validation — Absent vs Null vs Empty String.
Next Steps
- Configuration —
fromBody/fromQuery/ route and context - Type Casting — Automatic type conversion
- Validation — Validation rules and messages
- Processing stages — Normalize, cast and post-process