mini\Validator namespace

Validator - JSON Schema Validation

Philosophy

Mini provides composable, type-safe validation with JSON Schema-compatible rules. Build validation rules with a fluent API, export them as JSON Schema for client-side validation, and customize error messages for perfect UX.

Key Principles:

  • JSON Schema compatible - Export validation rules for client-side reuse
  • Fully immutable - Chain validators without side effects
  • Custom error messages - User-friendly validation feedback via x-error
  • Context-aware validation - Access parent objects for relational validation
  • Type-specific rules - Constraints only apply to matching types
  • Zero dependencies - Pure PHP validation, no external libraries

Setup

use function mini\validator;

Basic Usage

Validators work out of the box - just create and use:

// Create a validator
$validator = validator()
    ->type('string')
    ->minLength(3)
    ->maxLength(20);

// Validate a value
$error = $validator->isInvalid('ab');
if ($error) {
    echo "Error: $error"; // "Must be at least 3 characters long."
}

Return Values

isInvalid($value) returns null if valid, or a ValidationError if invalid. The ValidationError class implements:

  • Stringable: Cast to string for the error message
  • ArrayAccess: Access property errors via $error['fieldName']
  • IteratorAggregate: Iterate over property errors with foreach
  • JsonSerializable: Export for API responses
// Scalar validation
$error = $stringValidator->isInvalid($value);
if ($error) {
    echo $error; // "Must be at least 3 characters."
}

// Object validation - access property errors
$error = $objectValidator->isInvalid($data);
if ($error) {
    echo $error;              // "Validation failed."
    echo $error['username'];  // "Username is required."
    echo $error['email'];     // "Invalid email format."

    // Iterate all errors
    foreach ($error as $field => $fieldError) {
        echo "$field: $fieldError\n";
    }

    // Nested objects
    echo $error['address']['city']; // drills down

    // JSON for APIs
    echo json_encode($error);
}

Common Validation Patterns

String Validation:

$usernameValidator = validator()
    ->type('string')
    ->minLength(3, 'Username must be at least 3 characters.')
    ->maxLength(20, 'Username cannot exceed 20 characters.')
    ->pattern('/^[a-zA-Z0-9_]+$/', 'Only letters, numbers, and underscores allowed.')
    ->required('Username is required.');

Numeric Validation:

$ageValidator = validator()
    ->type('integer')
    ->minimum(18, 'You must be at least 18 years old.')
    ->maximum(120, 'Please enter a valid age.');

Email Validation:

$emailValidator = validator()
    ->type('string')
    ->format('email')
    ->required('Email address is required.');

Array Validation:

$tagsValidator = validator()
    ->type('array')
    ->items(validator()->type('string'))
    ->minItems(1, 'Please select at least one tag.')
    ->maxItems(5, 'Maximum 5 tags allowed.')
    ->uniqueItems();

Attribute-Based Validation

Define validation schemas using PHP 8 attributes on classes and interfaces:

use mini\Validator\Attributes as V;

// Define validation on an interface (no properties needed!)
#[V\Field(name: 'username', type: 'string', minLength: 3, maxLength: 20, required: true)]
#[V\Field(name: 'email', type: 'string', format: 'email', required: true)]
#[V\Field(name: 'age', type: 'integer', minimum: 18, maximum: 120)]
interface LoginForm {}

// Get validator from class/interface
$validator = validator(LoginForm::class);
$errors = $validator->isInvalid($_POST);

Property-Based Attributes

Add validation directly to class properties:

use mini\Validator\Attributes as V;

class User {
    #[V\Type('string')]
    #[V\MinLength(3, 'Username must be at least 3 characters.')]
    #[V\MaxLength(20, 'Username cannot exceed 20 characters.')]
    #[V\Pattern('/^[a-zA-Z0-9_]+$/', 'Only letters, numbers, and underscores allowed.')]
    #[V\Required('Username is required.')]
    public string $username;

    #[V\Type('string')]
    #[V\Format('email')]
    #[V\Required('Email is required.')]
    public string $email;

    #[V\Type('integer')]
    #[V\Minimum(18, 'Must be at least 18 years old.')]
    public int $age;
}

$validator = validator(User::class);
$errors = $validator->isInvalid($_POST);

Field Attribute (Property-less)

The #[Field] attribute allows defining validation schemas without actual properties. Perfect for:

  • Interfaces - Define validation contracts
  • DTOs - Lean data transfer objects
  • Form schemas - Validation without class structure
// Complex validation on empty class
#[V\Field(name: 'id', type: 'string', pattern: '/^[A-Z]{2}\d{6}$/', required: true)]
#[V\Field(name: 'status', type: 'string', enum: ['active', 'pending', 'disabled'], required: true)]
#[V\Field(name: 'tags', type: 'array', minItems: 1, maxItems: 5, uniqueItems: true)]
class AccountDTO {}

All validation rules can be passed as named parameters to #[Field]:

  • name - Field name (required)
  • type - JSON Schema type
  • minLength, maxLength - String constraints
  • minimum, maximum, exclusiveMinimum, exclusiveMaximum - Numeric constraints
  • multipleOf - Number must be multiple of value
  • pattern - Regex pattern
  • format - String format (email, uri, date, uuid, etc.)
  • minItems, maxItems, uniqueItems - Array constraints
  • minProperties, maxProperties - Object constraints
  • required - Mark field as required
  • const - Exact value match
  • enum - Array of allowed values
  • message - Custom error message

Mixed Approach

Combine #[Field] attributes with property attributes:

#[V\Field(name: 'metadata', type: 'object', minProperties: 1)]
class Product {
    #[V\Type('string'), V\MinLength(3), V\Required()]
    public string $name;

    #[V\Type('number'), V\Minimum(0)]
    public float $price;
}

// Validates: name, price (properties) + metadata (Field attribute)
$validator = validator(Product::class);

Reusable Validator Library

Define a library of reusable field validators and compose them into form validators:

use mini\Validator\Attributes as V;

// Define reusable validators as class properties
class Validators {
    #[V\Type('string')]
    #[V\MinLength(3, 'Username must be at least 3 characters.')]
    #[V\MaxLength(20, 'Username cannot exceed 20 characters.')]
    #[V\Pattern('/^[a-zA-Z0-9_]+$/', 'Only letters, numbers, and underscores.')]
    #[V\Required('Username is required.')]
    public string $username;

    #[V\Type('string')]
    #[V\Format('email', 'Please enter a valid email.')]
    #[V\Required('Email is required.')]
    public string $email;

    #[V\Type('string')]
    #[V\MinLength(8, 'Password must be at least 8 characters.')]
    #[V\Required('Password is required.')]
    public string $password;
}

// Compose into different forms by picking the validators you need
$loginForm = validator()
    ->type('object')
    ->forProperty('username', validator(Validators::class)->username)
    ->forProperty('password', validator(Validators::class)->password);

$registrationForm = validator()
    ->type('object')
    ->forProperty('username', validator(Validators::class)->username)
    ->forProperty('email', validator(Validators::class)->email)
    ->forProperty('password', validator(Validators::class)->password);

// Each form has its own validation with shared, consistent rules
$errors = $loginForm->isInvalid($_POST);

Property validators are accessed via magic __get, making composition feel natural.

Validator Registry & Caching

Validators are automatically cached for performance:

// First call: builds from attributes
$validator = validator(User::class);

// Subsequent calls: returns cached validator
$validator = validator(User::class);

Manual registration for custom validators:

use mini\Validator\ValidatorStore;

$store = Mini::$mini->get(ValidatorStore::class);
$store['custom-validator'] = validator()
    ->type('string')
    ->minLength(5);

// Retrieve custom validator
$validator = validator('custom-validator');

Available Attributes

All validator rules have corresponding attributes:

Type & Required:

  • #[Type(string $type, ?string $message)]
  • #[Required(?string $message)]

String Constraints:

  • #[MinLength(int $min, ?string $message)]
  • #[MaxLength(int $max, ?string $message)]
  • #[Pattern(string $pattern, ?string $message)]
  • #[Format(string $format, ?string $message)]

Numeric Constraints:

  • #[Minimum(int|float $min, ?string $message)]
  • #[Maximum(int|float $max, ?string $message)]
  • #[ExclusiveMinimum(int|float $min, ?string $message)]
  • #[ExclusiveMaximum(int|float $max, ?string $message)]
  • #[MultipleOf(int|float $divisor, ?string $message)]

Array Constraints:

  • #[MinItems(int $min, ?string $message)]
  • #[MaxItems(int $max, ?string $message)]
  • #[UniqueItems()]

Object Constraints:

  • #[MinProperties(int $min, ?string $message)]
  • #[MaxProperties(int $max, ?string $message)]

Value Constraints:

  • #[Const(mixed $value, ?string $message)]
  • #[Enum(array $values, ?string $message)]

Virtual Fields:

  • #[Field(...)] - Define field without property (repeatable)

Object Validation

Validate complex nested objects with property-specific validators:

$userValidator = validator()
    ->type('object')
    ->forProperty('username',
        validator()
            ->type('string')
            ->minLength(3)
            ->required()
    )
    ->forProperty('email',
        validator()
            ->type('string')
            ->format('email')
            ->required()
    )
    ->forProperty('age',
        validator()
            ->type('integer')
            ->minimum(18)
    );

// Validate user data
$userData = ['username' => 'ab', 'email' => 'invalid'];
$errors = $userValidator->isInvalid($userData);

if ($errors) {
    // $errors = ['username' => 'Must be at least 3 characters long.',
    //            'email' => 'Invalid email format.']
}

Custom Validation

Philosophy: Client-side validation is for UX, server-side validation is for security. You should always validate on the server. The custom() validator enables server-side checks that can't be offloaded to the client - like checking if a username already exists in the database.

Custom validators receive the field value and parent object/array:

->custom(function (mixed $value, mixed $parent): bool {
    // Return true if valid, false if invalid
})

The callback returns true for valid, false for invalid (uses default "Validation failed." message). Custom validators are server-side only and not exported to JSON Schema.

Database validation example:

$registrationValidator = validator()
    ->type('object')
    ->forProperty('username',
        validator()
            ->type('string')
            ->minLength(3)
            ->required()
            ->custom(fn($username) => !userExists($username))
    )
    ->forProperty('email',
        validator()
            ->type('string')
            ->format('email')
            ->required()
            ->custom(fn($email) => !emailTaken($email))
    );

Password confirmation example:

$validator = validator()
    ->type('object')
    ->forProperty('password',
        validator()->type('string')->minLength(8)->required()
    )
    ->forProperty('password_confirmation',
        validator()
            ->type('string')
            ->required('Please confirm your password.')
            ->custom(fn($confirmation, $data) =>
                isset($data['password']) && $confirmation === $data['password']
            )
    );

Use Cases:

  • Database uniqueness checks (username, email)
  • Password confirmation
  • Conditional required fields (e.g., state required if country is US)
  • Cross-field validation
  • External API validation

JSON Schema Export

Export validators as JSON Schema for client-side validation:

$validator = validator()
    ->type('string')
    ->minLength(5, 'Too short!')
    ->maxLength(100, 'Too long!')
    ->format('email');

$schema = json_encode($validator, JSON_PRETTY_PRINT);

Output:

{
    "type": "string",
    "minLength": 5,
    "maxLength": 100,
    "format": "email",
    "x-error": {
        "minLength": "Too short!",
        "maxLength": "Too long!"
    }
}

The x-error extension maps JSON Schema keyword names (minLength, pattern, etc.) to their custom error messages. Keywords without custom messages are omitted. Note that custom() validators are server-side only and not included in the exported schema.

Available Validators

Type Validators

->type('string')           // String type
->type('integer')          // Integer type
->type('number')           // Number (int or float)
->type('boolean')          // Boolean type
->type('array')            // Array type (sequential)
->type('object')           // Object type (associative array or PHP object)
->type('null')             // Null type
->type(['string', 'null']) // Multiple types (string OR null)

String Constraints

->minLength(int $min, ?string $message)
->maxLength(int $max, ?string $message)
->pattern(string $regex, ?string $message)
->format(string $format, ?string $message) // 'email', 'uri', 'date', 'uuid', etc.

Numeric Constraints

->minimum(int|float $min, ?string $message)
->maximum(int|float $max, ?string $message)
->exclusiveMinimum(int|float $min, ?string $message)
->exclusiveMaximum(int|float $max, ?string $message)
->multipleOf(int|float $divisor, ?string $message)

Array Constraints

->minItems(int $min, ?string $message)
->maxItems(int $max, ?string $message)
->uniqueItems()
->items(Validator $validator)              // All items match validator
->items([Validator, Validator, ...])       // Tuple validation
->additionalItems(Validator|bool)          // Validate items beyond tuple
->minContains(int $min, Validator $validator)
->maxContains(int $max, Validator $validator)

Object Constraints

->minProperties(int $min, ?string $message)
->maxProperties(int $max, ?string $message)
->forProperty(string $property, Validator $validator)
->properties(array $validators)            // ['prop' => Validator, ...]
->patternProperties(string $pattern, Validator $validator)
->additionalProperties(Validator|bool)
->dependentRequired(string $property, array $requiredProps)
->withFields(array $fields)                // Clone with only these properties
->withoutFields(array $fields)             // Clone excluding these properties

Other Validators

->const(mixed $value, ?string $message)    // Exact value match
->enum(array $values, ?string $message)    // Value in allowed list
->required(?string $message)               // Mark field as required
->custom(Closure $callback)                // Custom validation logic

Combinators

->allOf([Validator, ...])  // Must match all validators
->anyOf([Validator, ...])  // Must match at least one validator
->oneOf([Validator, ...])  // Must match exactly one validator
->not(Validator)           // Must NOT match validator

Advanced Examples

Conditional Validation

$addressValidator = validator()
    ->type('object')
    ->forProperty('country', validator()->type('string'))
    ->forProperty('state',
        validator()
            ->type('string')
            ->custom(fn($state, $address) =>
                // State required only for US addresses
                ($address['country'] ?? null) !== 'US' || !empty($state)
            )
    );

Tuple Validation

// Validate [string, integer, boolean] tuples
$tupleValidator = validator()
    ->type('array')
    ->items([
        validator()->type('string'),
        validator()->type('integer'),
        validator()->type('boolean'),
    ])
    ->additionalItems(false); // No additional items allowed

Multiple Types with Type-Specific Constraints

// Accept string OR integer, with type-specific rules
$idValidator = validator()
    ->type(['string', 'integer'])
    ->minLength(5)     // Only applies to strings
    ->multipleOf(2);   // Only applies to integers

$idValidator->isInvalid('abc');   // Invalid (string too short)
$idValidator->isInvalid('hello'); // Valid (string meets minLength)
$idValidator->isInvalid(3);       // Invalid (integer not multiple of 2)
$idValidator->isInvalid(10);      // Valid (integer is multiple of 2)

Nested Object Validation

$orderValidator = validator()
    ->type('object')
    ->forProperty('customer',
        validator()
            ->type('object')
            ->forProperty('name', validator()->type('string')->required())
            ->forProperty('email', validator()->type('string')->format('email'))
    )
    ->forProperty('items',
        validator()
            ->type('array')
            ->items(
                validator()
                    ->type('object')
                    ->forProperty('product_id', validator()->type('integer'))
                    ->forProperty('quantity', validator()->type('integer')->minimum(1))
            )
    );

Partial Validation

Create validators for update operations by excluding or selecting specific fields:

// Full user validator from attributes
$userValidator = validator(User::class);

// For update: exclude password (not required for profile updates)
$profileUpdateValidator = $userValidator->withoutFields(['password']);

// For password change: only validate password fields
$passwordChangeValidator = $userValidator->withFields(['password', 'password_confirmation']);

Immutability

All validator methods return new instances - the original is never modified:

$baseValidator = validator()->type('string');
$emailValidator = $baseValidator->format('email');
$usernameValidator = $baseValidator->minLength(3);

// $baseValidator is unchanged
// $emailValidator and $usernameValidator are independent

This makes validators safe to reuse and compose:

$stringValidator = validator()->type('string');

$userSchema = validator()
    ->type('object')
    ->forProperty('username', $stringValidator->minLength(3))
    ->forProperty('bio', $stringValidator->maxLength(500));

// $stringValidator remains unchanged

Error Messages

Custom error messages are stored in the x-error extension:

$validator = validator()
    ->type('string')
    ->minLength(8, 'Password must be at least 8 characters.')
    ->pattern('/[A-Z]/', 'Password must contain at least one uppercase letter.')
    ->pattern('/[0-9]/', 'Password must contain at least one number.');

$error = $validator->isInvalid('short');
// Returns: "Password must be at least 8 characters."

$schema = json_encode($validator);
// Includes: "x-error": {"minLength": "Password must be...", "pattern": "..."}

Integration

Form Validation

// Define schema
$userSchema = validator()
    ->type('object')
    ->forProperty('username', validator()->type('string')->minLength(3)->required())
    ->forProperty('email', validator()->type('string')->format('email')->required());

// Validate POST data
$errors = $userSchema->isInvalid($_POST);

if ($errors) {
    // Display errors in template
    foreach ($errors as $field => $message) {
        echo "<p class='error'>$field: $message</p>";
    }
} else {
    // Process valid data
    createUser($_POST);
}

API Response Validation

$responseValidator = validator()
    ->type('object')
    ->forProperty('status', validator()->enum(['success', 'error']))
    ->forProperty('data', validator()->type('object'));

$response = json_decode($apiResponse, true);
$error = $responseValidator->isInvalid($response);

if ($error) {
    throw new \RuntimeException("Invalid API response: $error");
}

Best Practices

  1. Reuse validators - Create base validators and compose them
  2. Use custom messages - Provide user-friendly error messages
  3. Export schemas - Share validation rules with client-side code
  4. Type-first - Always specify type before constraints
  5. Immutability - Take advantage of safe validator composition

Performance

  • Cached validators - Validators built from attributes are cached; subsequent calls return clones instantly
  • Fail-fast - Validation stops at first error for simple values
  • Efficient - Shallow clones for immutability (no deep copying)
  • Zero overhead - JSON Schema export via simple array serialization

Sub-namespaces (1)

Attributes namespace

Classes (5)

AttributeValidatorFactory

Builds validators from PHP class attributes

Purpose

Standard validation purposes for entity lifecycle operations

final
ValidationError

Represents validation errors for both scalar values and complex objects

Validator

Composable validation builder

ValidatorStore

Registry for validator instances with auto-building from attributes