validate()¶
Validates a raw data array against the type hints and attributes of a BaseModel instance, and returns the hydrated model.
<?php
namespace Attributes\Strictify;
function validate(array $rawData, BaseModel $model): BaseModel
Parameters¶
| Parameter | Type | Description |
|---|---|---|
$rawData |
array |
The input data, keyed by property name (or by alias). Missing keys fail validation unless the property is nullable or declares a default value. |
$model |
BaseModel |
The model instance to hydrate. Its public property type hints and attribute declarations define the validation schema. |
Return value¶
Returns the passed $model instance with every property populated from $rawData. Values are written with the type declared on the property:
- Scalars are coerced to the declared type in loose mode.
DateTimeInterface-typed properties hydrate from date strings such as1994-01-01T09:00:00+00:00.- Properties typed with a
BaseModelclass hydrate recursively from nested arrays. - Union-typed properties keep the raw value's type when it already matches one of the arms; otherwise the first arm that can coerce it wins.
Exceptions¶
| Exception | Thrown when |
|---|---|
Attributes\Strictify\Exceptions\ValidationException |
Any field fails validation. Call getErrors() on the exception for the error list. |
ValueError |
An attribute spec is malformed, for example an unresolvable class-name arm, an unsupported Type value in an array attribute, or a Dict key type outside integer and string. |
ValidationException::getErrors() returns an array keyed by field path, each value holding one or more messages:
<?php
use Attributes\Strictify\Exceptions\ValidationException;
try {
validate($rawData, new User());
} catch (ValidationException $e) {
$e->getErrors();
// [
// 'name' => ['Field is required'],
// 'address.street' => ['Must be string'],
// ]
}
Nested structures report dot-notation paths, including array element keys:
Validation rules¶
Required fields¶
Every typed, non-nullable property is required. A missing key produces a Field is required error. Nullable properties accept a missing key or an explicit null, and properties with a declared default value keep it when the key is missing.
Strict and loose mode¶
Validation is strict by default: a value must already have the declared type. Declare loose mode on the model to enable scalar coercion:
<?php
use Attributes\Strictify\ModelConfigs;
#[ModelConfigs(strict: false)]
class User extends BaseModel
{
public int $age;
public string $name;
}
validate(['age' => '30', 'name' => 123], new User())->age; // int(30)
// ->name is the string "123"
In loose mode:
- Exact type matches always win over coercion, regardless of declaration order.
- When coercion is needed on a union property, arms are tried in declaration order:
int|boolcoerces'1'toint(1), whilebool|intcoerces it tobool(true).
DateTimeInterface properties coerce from date strings in both modes.
Aliases¶
Map an input key to a property with the Alias attribute:
<?php
use Attributes\Strictify\Fields\Alias;
class User extends BaseModel
{
#[Alias('user_name')]
public string $name;
}
ModelConfigs also offers automatic alias generators (aliasGenerator: 'snake' | 'camel' | 'pascal').
Error messages¶
Default messages follow the Must be <type> pattern, joined for unions (Must be boolean, integer or string). Override them per property with the ErrorMessage attribute:
<?php
use Attributes\Strictify\Fields\ErrorMessage;
class User extends BaseModel
{
#[ErrorMessage(required: 'Name is mandatory', type: 'Expected {expected}')]
public string $name;
}
The {expected} placeholder renders the expected type list.
Constraints¶
Value-level rules are declared with constraint attributes from Attributes\Strictify\Fields\Constraints. They run after the property's type check, before the value is written, and each failing constraint reports its own error:
<?php
use Attributes\Strictify\Fields\Constraints\Between;
use Attributes\Strictify\Fields\Constraints\Regex;
class User extends BaseModel
{
#[Between(18, 99)]
public int $age;
#[Regex('/^[a-z][a-z0-9-]*$/')]
public string $username;
}
Constraints apply to the property value itself, not to Sequence/Dict elements. Custom constraints can be written in PHP userland by implementing Attributes\Strictify\Fields\Constraint. The constraints reference lists every built-in with its semantics and default message, and the userland contract.
Array type validation¶
Typed array properties declare their element shape with the Sequence, Union, Intersection and Dict attributes:
<?php
use Attributes\Strictify\Fields\Sequence;
use Attributes\Strictify\Fields\Union;
use Attributes\Strictify\Type;
class Blog extends BaseModel
{
#[Sequence(Type::string)]
public array $tags;
#[Union(int, string, sequence)]
public array $mixed;
#[Dict(key: Type::int, of: stdClass::class)]
public array $lookup;
}
Element types come from the Attributes\Strictify\Type enum (Type::int, Type::string, Type::bool, Type::float, Type::sequence, Type::object, Type::dict) or the matching namespace constants (int, string, bool, float, sequence, object), and may combine with the bitwise OR operator (bool | int). Class, interface and enum names are accepted as strings (DateTime::class) and autoload on use.
Sequencevalidates every element against one element type.Unionaccepts an element matching any declared arm.Intersectionrequires an element to match every declared class or interface. Its arms must name classes or interfaces only — enums are rejected, mirroring PHP intersection type hints.Dictvalidates keys against$key(integer and string types only) and values against$of.
The same attributes compose with new expressions to describe nested arrays:
Arguments of a composed new expression are validated at construction: an unsupported argument type throws a TypeError and a string that names no loadable class, interface or enum throws a ValueError.
Hooks¶
Models may override two protected hooks, called by validate() around the validation run:
<?php
abstract class BaseModel
{
protected function beforeValidation(array $rawData, ModelConfigs $configs): array {}
protected function afterValidation(array $rawData, ModelConfigs $configs): void {}
}
Serializing back to an array: toDict()¶
BaseModel::toDict() returns the model's public properties as an array, keyed by the same resolved field names validate() expects (aliases and aliasGenerator applied — nested models with their own #[ModelConfigs(aliasGenerator: ...)] use it). DateTimeInterface values are formatted as ISO 8601 strings (Y-m-d\TH:i:sP), so the output round-trips through validate():
<?php
class User extends BaseModel
{
public string $name;
public ?DateTime $birthday;
}
$user = validate(['name' => 'John', 'birthday' => '1994-01-01T09:00:00+00:00'], new User());
$user->toDict();
// ['name' => 'John', 'birthday' => '1994-01-01T09:00:00+00:00']
validate($user->toDict(), new User()); // works
When a property has a #[DateTime(format: ..., timezone: ...)] constraint, toDict() uses that format and timezone instead of the ISO 8601 default. The same format is also enforced during validation: on a DateTime-typed property, only strings matching the constraint's format exactly are accepted for coercion:
<?php
class Event extends BaseModel
{
#[DateTime(format: 'Y-m-d', timezone: 'Europe/Madrid')]
public ?DateTime $startsAt;
}
validate(['startsAt' => '1994-01-01'], new Event()); // ok
validate(['startsAt' => '1994-01-01T09:00:00+00:00'], new Event()); // ValidationException
$event = validate(['startsAt' => '1994-01-01'], new Event());
$event->toDict();
// ['startsAt' => '1994-01-01'] (Madrid is UTC+1 in January, formatted as Y-m-d)
Nested BaseModel properties are serialized recursively. Uninitialized typed properties are omitted from the output.