iviphp / validation
Validation rules and data validation for the IviPHP ecosystem.
Requires
- php: >=8.2
- iviphp/support: ^0.1
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/contractsiviphp/supportiviphp/configiviphp/containeriviphp/httpiviphp/databaseiviphp/cacheiviphp/viewiviphp/authiviphp/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.