iviphp/validation

Validation rules and data validation for the IviPHP ecosystem.

Maintainers

Package info

github.com/iviphp/validation

pkg:composer/iviphp/validation

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v0.1.0 2026-07-21 11:30 UTC

This package is not auto-updated.

Last update: 2026-07-22 03:22:44 UTC


README

Validation rules and data validation for the IviPHP ecosystem.

Overview

iviphp/validation provides a small object-based validation system for IviPHP packages and applications.

It supports:

  • reusable validation rule objects;
  • nested fields through dot notation;
  • required and nullable fields;
  • validated-data extraction;
  • multiple errors per field;
  • optional early termination;
  • structured validation results;
  • validation exceptions.

The package is framework-independent and does not depend on HTTP, routing, controllers or global application state.

Installation

composer require iviphp/validation

Requirements

  • PHP 8.2 or later
  • Composer
  • iviphp/support

Basic validation

<?php

declare(strict_types=1);

use Ivi\Validation\Rules\Email;
use Ivi\Validation\Rules\Required;
use Ivi\Validation\Rules\StringType;
use Ivi\Validation\Validator;

$result = Validator::make(
    data: [
        'name' => 'Gaspard',
        'email' => 'gaspard@example.com',
    ],
    rules: [
        'name' => [
            new Required(),
            new StringType(),
        ],
        'email' => [
            new Required(),
            new Email(),
        ],
    ]
);

Check the result:

if ($result->passes()) {
    $validated = $result->validated();
}

if ($result->fails()) {
    $errors = $result->errors();
}

Creating a validator

A validator can be created and reused.

<?php

declare(strict_types=1);

use Ivi\Validation\Rules\Required;
use Ivi\Validation\Rules\StringType;
use Ivi\Validation\Validator;

$validator = new Validator([
    'title' => [
        new Required(),
        new StringType(),
    ],
]);

$result = $validator->validate([
    'title' => 'My article',
]);

Nested fields

Dot notation can validate nested input values.

$result = Validator::make(
    data: [
        'user' => [
            'name' => 'Gaspard',
            'email' => 'gaspard@example.com',
        ],
    ],
    rules: [
        'user.name' => [
            new Required(),
            new StringType(),
        ],
        'user.email' => [
            new Required(),
            new Email(),
        ],
    ]
);

Validated values preserve their nested structure.

$validated = $result->validated();

Result:

[
    'user' => [
        'name' => 'Gaspard',
        'email' => 'gaspard@example.com',
    ],
]

Required fields

use Ivi\Validation\Rules\Required;

$result = Validator::make(
    data: [
        'name' => '',
    ],
    rules: [
        'name' => [
            new Required(),
        ],
    ]
);

The Required rule rejects:

  • null;
  • empty strings;
  • strings containing only whitespace;
  • empty arrays.

The following values are accepted:

  • false;
  • 0;
  • '0';
  • non-empty arrays;
  • non-empty strings.

Nullable fields

use Ivi\Validation\Rules\Email;
use Ivi\Validation\Rules\Nullable;

$result = Validator::make(
    data: [
        'secondary_email' => null,
    ],
    rules: [
        'secondary_email' => [
            new Nullable(),
            new Email(),
        ],
    ]
);

When the value is null, Nullable causes the remaining rules to be skipped.

Empty strings are not treated as null.

[
    'secondary_email' => '',
]

This value still fails the Email rule.

Required takes precedence over Nullable.

'email' => [
    new Required(),
    new Nullable(),
    new Email(),
]

A null value still fails because the field is required.

String validation

use Ivi\Validation\Rules\StringType;

$result = Validator::make(
    data: [
        'name' => 'IviPHP',
    ],
    rules: [
        'name' => [
            new StringType(),
        ],
    ]
);

StringType accepts only real PHP strings.

It does not convert integers, booleans or other values automatically.

Integer validation

use Ivi\Validation\Rules\IntegerType;

$result = Validator::make(
    data: [
        'quantity' => 12,
    ],
    rules: [
        'quantity' => [
            new IntegerType(),
        ],
    ]
);

The string '12' does not pass IntegerType.

[
    'quantity' => '12',
]

Use Numeric when numeric strings should be accepted.

Numeric validation

use Ivi\Validation\Rules\Numeric;

$result = Validator::make(
    data: [
        'price' => '24.50',
    ],
    rules: [
        'price' => [
            new Numeric(),
        ],
    ]
);

Numeric accepts:

  • integers;
  • finite floating-point values;
  • valid numeric strings.

It rejects:

  • booleans;
  • empty strings;
  • arrays;
  • objects;
  • NaN;
  • infinite values.

Email validation

use Ivi\Validation\Rules\Email;

$result = Validator::make(
    data: [
        'email' => 'user@example.com',
    ],
    rules: [
        'email' => [
            new Email(),
        ],
    ]
);

Email validation uses PHP's FILTER_VALIDATE_EMAIL.

Leading or trailing whitespace is rejected instead of silently removed.

Minimum length

use Ivi\Validation\Rules\MinLength;

$result = Validator::make(
    data: [
        'password' => 'secret123',
    ],
    rules: [
        'password' => [
            new MinLength(8),
        ],
    ]
);

MinLength supports strings and arrays.

'tags' => [
    new MinLength(2),
]

Strings are measured with mb_strlen() when the Mbstring extension is available. Otherwise, byte length is used.

Maximum length

use Ivi\Validation\Rules\MaxLength;

$result = Validator::make(
    data: [
        'username' => 'gaspard',
    ],
    rules: [
        'username' => [
            new MaxLength(30),
        ],
    ]
);

Arrays can also be limited:

'categories' => [
    new MaxLength(5),
]

Allowed values

use Ivi\Validation\Rules\In;

$result = Validator::make(
    data: [
        'status' => 'published',
    ],
    rules: [
        'status' => [
            new In([
                'draft',
                'published',
                'archived',
            ]),
        ],
    ]
);

Strict comparison is enabled by default.

new In([1, 2, 3])

The string '1' does not match the integer 1.

Non-strict comparison can be enabled explicitly:

new In(
    allowedValues: [1, 2, 3],
    strict: false
)

Custom validation messages

Most built-in rules accept an optional custom message.

new Required(
    'Please provide :field.'
)
new MinLength(
    minimum: 8,
    customMessage: ':field must contain at least :min characters.'
)
new MaxLength(
    maximum: 100,
    customMessage: ':field cannot exceed :max characters.'
)
new In(
    allowedValues: [
        'draft',
        'published',
    ],
    customMessage: ':field must be one of: :values.'
)

Supported placeholders depend on the rule.

Common placeholders include:

  • :field;
  • :min;
  • :max;
  • :values.

Validation results

A validation operation returns a ValidationResult.

$result->passes();
$result->fails();

Return all errors:

$errors = $result->errors();

Example:

[
    'email' => [
        'The email field must be a valid email address.',
    ],
]

Check one field:

if ($result->has('email')) {
    // The email field has errors.
}

Return all messages for one field:

$messages = $result->errorsFor('email');

Return the first message for one field:

$message = $result->first('email');

Return the first message from the complete result:

$message = $result->first();

Return every message as a flat list:

$messages = $result->messages();

Return the total number of errors:

$count = $result->errorCount();

Validated data

Only fields declared in the validation rules are returned as validated data.

$result = Validator::make(
    data: [
        'name' => 'Gaspard',
        'email' => 'gaspard@example.com',
        'admin' => true,
    ],
    rules: [
        'name' => [
            new Required(),
        ],
        'email' => [
            new Required(),
            new Email(),
        ],
    ]
);

$validated = $result->validated();

Result:

[
    'name' => 'Gaspard',
    'email' => 'gaspard@example.com',
]

The undeclared admin value is excluded.

Retrieve one validated value:

$name = $result->value('name');

Nested dot notation is supported:

$email = $result->value('user.email');

Check whether validated data contains a key:

$result->hasValue('user.email');

Fields that fail validation are excluded from validated data.

Optional fields

A field that is absent and does not use Required is skipped.

$result = Validator::make(
    data: [],
    rules: [
        'nickname' => [
            new StringType(),
        ],
    ]
);

This validation passes because nickname is optional.

When the optional field exists, its rules are applied.

Stopping after the first error

By default, the validator collects every failure for each field.

$result = Validator::make(
    data: [
        'email' => '',
    ],
    rules: [
        'email' => [
            new Required(),
            new Email(),
            new MinLength(10),
        ],
    ]
);

Enable early termination per field:

$result = Validator::make(
    data: [
        'email' => '',
    ],
    rules: [
        'email' => [
            new Required(),
            new Email(),
            new MinLength(10),
        ],
    ],
    stopOnFirstFailure: true
);

A reusable validator can also be changed immutably.

$validator = new Validator($rules);

$fastValidator = $validator
    ->withStopOnFirstFailure();

$completeValidator = $fastValidator
    ->withoutStopOnFirstFailure();

Validation exceptions

A failed result can be wrapped in a ValidationException.

use Ivi\Validation\Exceptions\ValidationException;

$result = Validator::make(
    data: [
        'email' => 'invalid',
    ],
    rules: [
        'email' => [
            new Required(),
            new Email(),
        ],
    ]
);

if ($result->fails()) {
    throw ValidationException::fromResult($result);
}

Catch the exception:

try {
    throw ValidationException::fromResult($result);
} catch (ValidationException $exception) {
    $errors = $exception->errors();
    $first = $exception->first();
}

Available exception methods:

$exception->result();
$exception->errors();
$exception->errorsFor($field);
$exception->has($field);
$exception->first($field);
$exception->messages();
$exception->errorCount();
$exception->validated();

A ValidationException cannot be created from a successful result.

Custom rules

Custom validation rules implement RuleInterface.

<?php

declare(strict_types=1);

use Ivi\Validation\RuleInterface;

final readonly class StartsWith implements RuleInterface
{
    public function __construct(
        private string $prefix
    ) {}

    public function passes(
        string $field,
        mixed $value,
        array $data
    ): bool {
        return is_string($value)
            && str_starts_with(
                $value,
                $this->prefix
            );
    }

    public function message(string $field): string
    {
        return "The {$field} field must start with {$this->prefix}.";
    }
}

Use the custom rule normally:

$result = Validator::make(
    data: [
        'reference' => 'IVI-100',
    ],
    rules: [
        'reference' => [
            new Required(),
            new StartsWith('IVI-'),
        ],
    ]
);

The complete input array is provided to each rule.

This enables cross-field validation.

final readonly class MatchesField implements RuleInterface
{
    public function __construct(
        private string $otherField
    ) {}

    public function passes(
        string $field,
        mixed $value,
        array $data
    ): bool {
        return array_key_exists(
            $this->otherField,
            $data
        ) && $value === $data[$this->otherField];
    }

    public function message(string $field): string
    {
        return "The {$field} field must match {$this->otherField}.";
    }
}

Complete example

<?php

declare(strict_types=1);

use Ivi\Validation\Exceptions\ValidationException;
use Ivi\Validation\Rules\Email;
use Ivi\Validation\Rules\In;
use Ivi\Validation\Rules\IntegerType;
use Ivi\Validation\Rules\MaxLength;
use Ivi\Validation\Rules\MinLength;
use Ivi\Validation\Rules\Nullable;
use Ivi\Validation\Rules\Required;
use Ivi\Validation\Rules\StringType;
use Ivi\Validation\Validator;

$data = [
    'name' => 'Gaspard Kirira',
    'email' => 'gaspard@example.com',
    'age' => 28,
    'role' => 'admin',
    'bio' => null,
];

$result = Validator::make(
    data: $data,
    rules: [
        'name' => [
            new Required(),
            new StringType(),
            new MinLength(2),
            new MaxLength(100),
        ],
        'email' => [
            new Required(),
            new Email(),
        ],
        'age' => [
            new Required(),
            new IntegerType(),
        ],
        'role' => [
            new Required(),
            new In([
                'member',
                'admin',
            ]),
        ],
        'bio' => [
            new Nullable(),
            new StringType(),
            new MaxLength(500),
        ],
    ],
    stopOnFirstFailure: true
);

if ($result->fails()) {
    throw ValidationException::fromResult($result);
}

$validated = $result->validated();

Available classes

RuleInterface

$rule->passes($field, $value, $data);
$rule->message($field);

Validator

Validator::make(
    $data,
    $rules,
    $stopOnFirstFailure
);

$validator->validate($data);
$validator->rules();
$validator->stopsOnFirstFailure();
$validator->withStopOnFirstFailure();
$validator->withoutStopOnFirstFailure();

ValidationResult

$result->passes();
$result->fails();

$result->has($field);
$result->errors();
$result->errorsFor($field);
$result->first($field);
$result->messages();
$result->errorCount();

$result->validated();
$result->value($key, $default);
$result->hasValue($key);

ValidationException

ValidationException::fromResult(
    $result,
    $message,
    $previous
);

$exception->result();
$exception->errors();
$exception->errorsFor($field);
$exception->has($field);
$exception->first($field);
$exception->messages();
$exception->errorCount();
$exception->validated();

Built-in rules

Required

Requires a non-empty value.

new Required();
new Required($customMessage);

Nullable

Allows null and skips remaining rules.

new Nullable();

StringType

Requires a PHP string.

new StringType();
new StringType($customMessage);

IntegerType

Requires a PHP integer.

new IntegerType();
new IntegerType($customMessage);

Numeric

Requires an integer, finite float or numeric string.

new Numeric();
new Numeric($customMessage);

Email

Requires a valid email address.

new Email();
new Email($customMessage);

MinLength

Requires a minimum string character count or array element count.

new MinLength($minimum);
new MinLength($minimum, $customMessage);

MaxLength

Requires a maximum string character count or array element count.

new MaxLength($maximum);
new MaxLength($maximum, $customMessage);

In

Requires a value to exist in an allowed list.

new In($allowedValues);

new In(
    $allowedValues,
    $strict,
    $customMessage
);

Design principles

  • Object-based validation rules
  • Explicit and reusable rule behavior
  • Nested field support through dot notation
  • No global validator state
  • No automatic data coercion
  • Strict value comparison by default
  • Validated-data extraction
  • Structured errors per field
  • Framework-independent implementation
  • Clear extension through custom rules

This package validates values but does not transform them automatically.

Data normalization, sanitization and type conversion should happen explicitly before or after validation depending on application requirements.

Package boundaries

This package is responsible for:

  • validating input arrays;
  • applying reusable rule objects;
  • collecting validation errors;
  • returning validated values;
  • representing failed validation operations.

It is not responsible for:

  • reading HTTP requests;
  • generating HTTP responses;
  • database uniqueness checks;
  • authentication;
  • authorization;
  • form rendering;
  • translating messages;
  • global application state.

Database-aware and application-aware rules should be implemented in higher-level packages or application code.

Ecosystem

This package is part of the IviPHP ecosystem:

  • iviphp/contracts
  • iviphp/support
  • iviphp/config
  • iviphp/container
  • iviphp/http
  • iviphp/database
  • iviphp/cache
  • iviphp/view
  • iviphp/auth
  • iviphp/framework

Contributing

Contributions should preserve the focused scope of this package.

New rules should:

  • implement RuleInterface;
  • avoid hidden data mutation;
  • return deterministic results;
  • provide clear validation messages;
  • remain independent from the full framework;
  • avoid global service access;
  • avoid implicit database or network operations.

Security

Validation does not replace security controls such as:

  • authorization;
  • output escaping;
  • SQL parameter binding;
  • CSRF protection;
  • upload scanning;
  • rate limiting;
  • password hashing.

Input should be validated as close as possible to the application boundary.

Please report security issues privately to the Softadastra maintainers instead of opening a public issue.

License

This project is licensed under the MIT License.

Maintainer

Created and maintained by Softadastra.