solophp/request-handler

Robust request validation & authorization layer for HTTP inputs with type-safe handlers and modern PHP 8+ architecture

Maintainers

Package info

github.com/SoloPHP/Request-Handler

pkg:composer/solophp/request-handler

Statistics

Installs: 70

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v3.1.0 2026-05-29 12:13 UTC

This package is auto-updated.

Last update: 2026-05-29 12:15:24 UTC


README

Type-safe Request DTOs for PHP 8.2+ with focused per-property attributes, declarative validation, type casting, and full IDE support.

Latest Version on Packagist PHP Version License

Features

  • Per-property source binding — default source comes from the handle method (handleBody → Body, handleQuery → Query); #[FromRoute] and #[FromContext] override per property. No implicit merging, no surprises.
  • Attribute-composed DTOs — each concern is its own focused attribute: #[Validate], #[Cast], #[PostProcess], #[Generator], #[Items], …
  • Convention over configuration — every public non-static property is managed; attributes only add behaviour
  • Automatic type casting — driven by the property's declared type; built-ins for int, float, bool, string, array, DateTime + custom casters
  • Validation rules — passed verbatim to your ValidatorInterface; route and context values land in the validation payload as regular fields, so cross-field rules (exists:…,scope_col,{field}) work without special syntax
  • Generators — auto-generate UUIDs, sequences, custom values via #[Generator]
  • Pre/post processing#[PreProcess] / #[PostProcess] for transformation
  • Nested items#[Items] validates arrays of nested Request DTOs with dot-notation error paths
  • Fail-fast — every misconfiguration throws ConfigurationException at metadata-build time, not at first request
  • Schema exportschema() emits JSON-serializable per-field metadata (type, nullability, requiredness incl. conditional required_if, normalized rules) so the frontend can treat the backend as the single source of truth

Installation

composer require solophp/request-handler

Quick Example

use Solo\RequestHandler\Attributes\{Validate, Generator, Items, Exclude};
use Solo\RequestHandler\Attributes\Source\{FromRoute, FromContext};
use Solo\RequestHandler\Request;

final class UpdateProductRequest extends Request
{
    // PUT /products/{id}
    #[FromRoute]
    #[Validate('required|integer|exists:products,id')]
    public int $id;

    // default body — no source attribute needed
    #[Validate('required|string|max:255')]
    public string $name;

    #[Validate('required|numeric|min:0')]
    public float $price;

    #[Validate('nullable|integer|min:0')]
    public int $stock = 0;

    #[FromContext('tenantId')]
    #[Validate('required|integer')]
    public int $tenantId;

    #[FromContext('authUserId')]
    #[Validate('required|integer')]
    public int $updatedBy;

    #[Validate('required|string|min:8')]
    #[Exclude]              // validated/processed but hidden from toArray()
    public string $newPassword;
}

// Controller
$dto = $requestHandler->handleBody(
    UpdateProductRequest::class,
    $request,
    route:   ['id' => $productId],
    context: ['tenantId' => $tenant->id(), 'authUserId' => $auth->id()],
);

$dto->id;          // int — pulled from URL path
$dto->name;        // string — from body
$dto->tenantId;    // int — from application context
$dto->updatedBy;   // int — from application context
$dto->toArray();   // ['id'=>…, 'name'=>…, 'price'=>…, 'stock'=>…, 'tenantId'=>…, 'updatedBy'=>…] — no newPassword

Attribute family

Attribute Purpose
#[Validate('rules')] Validation rules (passed to your ValidatorInterface)
#[FromRoute(?string $key = null)] Read from the $route argument (URL path params)
#[FromContext(string $key)] Read from the $context argument (auth user, tenant, …)
#[FromBody(string $path)] Read from a nested body path (customer.id); flat keys may also be remapped this way
#[Cast('int'|'float'|'datetime:Y-m-d'|...)] Explicit built-in cast
#[Caster(MoneyCaster::class)] Custom CasterInterface implementation
#[PreProcess(MyProc::class)] Run handler BEFORE validation
#[PostProcess(MyProc::class, config: [...])] Run handler AFTER validation, with optional config
#[Generator(UuidGenerator::class, options: [...])] Value is generated, not read from any source
#[Items(OrderItemRequest::class)] Property is an array of nested DTOs
#[Group('criteria', mapTo: 'positions.id')] Group membership + remap for Request::group()
#[Exclude] Request::toArray() skips this property (still validated & processed)
#[Ignore] Property is invisible to the handler AND to toArray()/has()/get()/group()

Properties without a source attribute read from the default-source bag (Body for handleBody/handleArray, Query for handleQuery) by name. For values that don't fit the four bags (Body, Query, Route, Context) — request headers, cookies, file uploads — extract them in the controller and pass them via route: [...] or context: [...].

To opt a public property out of input processing entirely, mark it with #[Ignore] — the handler skips it and toArray()/has()/get()/group() will not see it either. Or change its visibility to protected/private — the handler only touches public properties.

Schema introspection

schema() reflects a request class into a JSON-serializable description of its input contract — without touching any HTTP input. Hand it to the frontend so validation rules live in exactly one place.

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

// [
//   'id' => [
//     'type' => 'int', 'nullable' => false, 'required' => true,
//     'requiredIf' => null, 'hasDefault' => false, 'exclude' => false,
//     'source' => 'route',
//     'rules' => [['name' => 'required', 'args' => []], ['name' => 'integer', 'args' => []]],
//   ],
//   'name' => [
//     'type' => 'string', 'nullable' => false, 'required' => true,
//     'requiredIf' => null, 'hasDefault' => false, 'exclude' => false, 'source' => null,
//     'rules' => [['name' => 'required', 'args' => []], ['name' => 'string', 'args' => []], ['name' => 'max', 'args' => ['255']]],
//   ],
//   'slug' => [
//     'type' => 'string', 'nullable' => true, 'required' => false,
//     'requiredIf' => ['field' => 'status', 'value' => 'published'],
//     'hasDefault' => true, 'exclude' => false, 'source' => null,
//     'rules' => [/* … incl. ['name' => 'required_if', 'args' => ['status', 'published']] */],
//   ],
// ]

echo json_encode($schema); // round-trips losslessly — scalars and arrays only

Every managed property is emitted (excluded/#[FromRoute]/#[FromContext] fields included) — exclude and source are reported as flags so the consumer decides what to render. Notes:

  • 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.
  • source is 'body' | 'query' | 'route' | 'context', or null when the field uses the handle method's default bag.
  • rules is the full normalized rule list, split with the same semantics the handler uses internally: name is the part before the first :, args is the comma-split remainder.

Documentation

Full Documentation

Requirements

  • PHP 8.2+
  • PSR-7 HTTP Message implementation
  • Validator implementing Solo\Contracts\Validator\ValidatorInterface

We recommend solophp/validator.

License

MIT License. See LICENSE for details.