Validator

Overview

The Validator class provides a fluent, rule-based validation engine. Pass any data array and a rules map — it checks every field, collects all errors, and gives you a clean API to inspect results or throw a ValidationException automatically.

  • Class: JiFramework\Core\Utilities\Validator
  • Access: $app->validator (lazy-loaded instance)
  • Rules: pipe-separated string 'required|email|max:255' or an array
  • Dot notation: nested arrays supported — 'user.email' maps to $data['user']['email']
  • Custom rules: register any closure with addRule()
  • Throws: JiFramework\Exceptions\ValidationException (HTTP 422, carries getErrors())
$v = $app->validator->make($_POST, [
    'email'    => 'required|email|max:255',
    'password' => 'required|strongPassword|confirmed',
    'age'      => 'required|integer|minValue:18',
    'website'  => 'nullable|url',
]);

if ($v->fails()) {
    $app->json(422, ['errors' => $v->errors()]);
}

// Or throw automatically
$v->throw();

make()

make(array $data, array $rules, array $messages = []): static

Bind data and rules, run all validations, and return the instance for fluent use. This is the primary entry point for validating a full data set (e.g. $_POST, a decoded JSON body, or any associative array).

  • $data(array) The data to validate. Supports nested arrays via dot notation.
  • $rules(array) Rules map: ['field' => 'rule1|rule2:param']. Values can also be arrays of rule strings.
  • $messages(array) Optional custom error messages. Keys are 'field.rule' or just 'rule'.

Returns: static — the same instance, ready for passes(), fails(), errors(), etc.

$v = $app->validator->make(
    ['name' => 'Alice', 'email' => 'bad-email'],
    ['name'  => 'required|min:2',
     'email' => 'required|email']
);

$v->fails();   // true
$v->errors();  // ['email' => ['The Email field must be a valid email address.']]

passes() / fails()

Check the outcome of the last make() call.

passes(): bool

Returns true if all rules passed (no errors).

fails(): bool

Returns true if any rule failed.

$v = $app->validator->make($data, $rules);

if ($v->passes()) {
    // all good -- save to database
}

if ($v->fails()) {
    // return errors to the user
    $app->json(422, ['errors' => $v->errors()]);
}

errors()

errors(): array

Return all collected validation errors as a structured array. Each field maps to an array of error messages (one per failed rule, unless stopOnFirstFailure() is enabled).

Returns: ['field' => ['message1', 'message2', ...], ...]. Empty array if validation passed.

$v->make(
    ['email' => '', 'age' => 'abc'],
    ['email' => 'required|email', 'age' => 'required|integer']
);

$v->errors();
// [
//   'email' => ['The Email field is required.', 'The Email field must be a valid email address.'],
//   'age'   => ['The Age field must be an integer.']
// ]

first()

first(?string $field = null): string|array

Get the first error message(s) — useful when you want to show a single message per field rather than the full list.

  • Called with no argument: returns an array with the first message for each failed field — ['field' => 'first message', ...]
  • Called with a field name: returns that field's first error message as a string, or an empty string if the field has no errors.
$v->make(
    ['email' => '', 'age' => 'abc'],
    ['email' => 'required|email', 'age' => 'required|integer']
);

$v->first();
// ['email' => 'The Email field is required.', 'age' => 'The Age field must be an integer.']

$v->first('email');
// 'The Email field is required.'

$v->first('unknown');
// ''  (empty string -- no errors for this field)

throw()

throw(): void

Throw a ValidationException if validation failed. Does nothing if validation passed. The exception carries the full errors array, accessible via getErrors(). The exception code is 422.

Use this when you want the framework's error handler to catch validation failures automatically instead of checking fails() manually.

// Fluent one-liner -- throws automatically if invalid
$app->validator->make($_POST, [
    'email' => 'required|email',
    'name'  => 'required|min:2',
])->throw();

// Catching it manually
try {
    $app->validator->make($data, $rules)->throw();
} catch (JiFrameworkExceptionsValidationException $e) {
    $errors = $e->getErrors(); // full structured errors array
    $app->json(422, ['errors' => $errors]);
}

check() / checkOrFail()

Validate a single value without affecting the instance's stored data or errors. Ideal for quick inline checks.

check(mixed $value, string|array $rules): bool

Returns true if the value passes all rules, false otherwise. The instance state (data, rules, errors) is fully restored after the call.

checkOrFail(mixed $value, string|array $rules): void

Same as check() but throws a ValidationException on failure.

// Quick boolean check
if (!$app->validator->check($email, 'required|email')) {
    echo 'Invalid email';
}

// Throw if invalid
$app->validator->checkOrFail($token, 'required|size:64');

// Works alongside make() without polluting errors
$v = $app->validator->make($data, $rules);
$extraValid = $app->validator->check($someValue, 'integer|minValue:1');

stopOnFirstFailure()

stopOnFirstFailure(bool $stop = true): static

When enabled, validation stops after the first failed rule for each field — only one error message is collected per field instead of all failures. Useful when error messages are shown inline next to each field.

Disabled by default — all rules run and all errors are collected.

Returns: static for chaining.

// Only the first error per field
$v = $app->validator
    ->stopOnFirstFailure()
    ->make($data, $rules);

// Re-enable collecting all errors
$app->validator->stopOnFirstFailure(false);

addMessages()

addMessages(array $messages): static

Merge custom error messages into the validator. Messages are matched in order of specificity: field.rule (most specific) overrides rule (global override).

Custom messages are merged persistently on the instance, so you can set them once and reuse across multiple make() calls. You can also pass messages directly as the third argument to make() for one-off overrides.

Placeholders available in messages:

  • :field — the field name, formatted (underscores and dots replaced with spaces, first letter capitalised)
  • :param0, :param1 — rule parameters by index
  • :values — all rule parameters joined by ', ' (useful for in / notIn)
$app->validator->addMessages([
    'email.required'  => 'We need your email address to continue.',
    'email.email'     => 'That doesn't look like a valid email.',
    'required'        => ':field is required.',            // global override
    'min'             => ':field must be at least :param0 characters long.',
]);

$v = $app->validator->make($data, $rules);

addRule()

addRule(string $ruleName, callable $callback): static

Register a custom validation rule. The callback receives four arguments and must return a boolean.

  • $ruleName(string) The rule name as it will appear in rule strings.
  • $callback(callable) Signature: function(string $field, mixed $value, array $params, array $data): bool

Register a matching error message with addMessages() to show a meaningful error when the rule fails.

$app->validator->addRule('even', function($field, $value, $params, $data) {
    return is_numeric($value) && (int)$value % 2 === 0;
});

$app->validator->addMessages([
    'even' => 'The :field field must be an even number.',
]);

// Use it like any built-in rule
$v = $app->validator->make(
    ['count' => '3'],
    ['count' => 'required|integer|even']
);

$v->first('count'); // 'The Count field must be an even number.'

Rule: nullable

Mark a field as optional. When nullable is present in the rule list and the field value is empty (null, '', or []), all other rules for that field are skipped and the field is considered valid.

If the field contains a non-empty value, all remaining rules still apply normally.

$rules = [
    'website' => 'nullable|url',   // empty = valid, non-empty must be a URL
    'bio'     => 'nullable|max:500', // optional but max 500 chars if provided
];

$app->validator->make(['website' => '', 'bio' => ''], $rules)->passes(); // true
$app->validator->make(['website' => 'not-a-url'], $rules)->fails();          // true

Rule: required

The field must be present and non-empty. Fails for null, empty string '', and empty array [].

$rules = ['name' => 'required'];

$app->validator->check('Alice', 'required'); // true
$app->validator->check('',      'required'); // false
$app->validator->check(null,    'required'); // false
$app->validator->check([],      'required'); // false

Rules: Type Checks

Validate the type of a field's value.

numeric

Value is numeric — integer, float, or numeric string like '3.14'.

integer

Value is a valid whole number (uses FILTER_VALIDATE_INT). Rejects floats like '1.5'.

float

Value is a valid decimal number (uses FILTER_VALIDATE_FLOAT).

boolean

Value is a boolean-like: true, false, 1, 0, '1', '0', 'true', 'false', 'on', 'off', 'yes', 'no'.

array

Value must be a PHP array.

$app->validator->check('42',   'numeric');  // true
$app->validator->check('5',    'integer');  // true
$app->validator->check('5.5',  'integer');  // false
$app->validator->check('3.14', 'float');    // true
$app->validator->check('yes',  'boolean'); // true
$app->validator->check([1,2],   'array');    // true

Rules: String Format

Validate the format of a string value.

email

Must be a valid email address.

url

Must be a valid URL (requires scheme, e.g. https://).

ip / ipv4 / ipv6

ip accepts both IPv4 and IPv6. ipv4 and ipv6 restrict to a specific version.

alpha

Only Unicode letters — works with Arabic, Hindi, and all scripts (/^p{L}+$/u).

alphaNum

Only Unicode letters and numbers (/^[p{L}p{N}]+$/u).

alphaDash

Unicode letters, numbers, hyphens, and underscores. Good for usernames and slugs.

json

Must be a valid JSON string.

uuid

Must be a valid UUID (any version, case-insensitive).

slug

Lowercase letters, digits, and hyphens only — no leading or trailing hyphen (e.g. 'my-post-title').

phone

Basic international phone number: optional +, 7–20 digits, spaces, hyphens, and parentheses allowed.

$app->validator->check('[email protected]',  'email');    // true
$app->validator->check('https://site.com',  'url');      // true
$app->validator->check('192.168.1.1',       'ipv4');     // true
$app->validator->check('مرحبا',             'alpha');    // true (Arabic letters)
$app->validator->check('alice_99',          'alphaDash'); // true
$app->validator->check('{"k":"v"}',         'json');     // true
$app->validator->check('my-post-123',       'slug');     // true
$app->validator->check('+8801712345678',    'phone');    // true

Rules: regex / strongPassword

regex:/pattern/

Value must match the given regular expression. Pass the full pattern including delimiters. Invalid patterns return false safely without emitting a PHP warning.

strongPassword

Value must be at least 8 characters and contain all four of: an uppercase letter, a lowercase letter, a digit, and a special character. Multibyte-safe length check.

// regex
$app->validator->check('ABC-123', 'regex:/^[A-Z0-9-]+$/'); // true
$app->validator->check('abc',     'regex:/^[A-Z0-9-]+$/'); // false

// strongPassword
$app->validator->check('Secret@123', 'strongPassword'); // true
$app->validator->check('simple',     'strongPassword'); // false -- too short, no upper/special
$app->validator->check('ALLUPPER1!', 'strongPassword'); // false -- no lowercase

$rules = [
    'password' => 'required|strongPassword|confirmed',
];

Rules: String Length

All three length rules are multibyte-safe — they count characters (not bytes), so Arabic, Hindi, and other Unicode strings are measured correctly.

min:n

String must be at least n characters long.

max:n

String must not exceed n characters.

size:n

String must be exactly n characters long.

$app->validator->check('hello',   'min:3');   // true  (5 chars)
$app->validator->check('hi',      'min:3');   // false (2 chars)
$app->validator->check('مرحبا',   'min:5');   // true  (5 Arabic chars, not 10 bytes)
$app->validator->check('hello',   'max:10');  // true
$app->validator->check('hello',   'size:5');  // true
$app->validator->check('hello',   'size:3');  // false

Rules: Numeric Range

Validate the numeric value of a field (not its string length).

minValue:n

Numeric value must be greater than or equal to n.

maxValue:n

Numeric value must be less than or equal to n.

between:min,max

Numeric value must be between min and max (inclusive).

$app->validator->check('18',  'minValue:18');       // true
$app->validator->check('17',  'minValue:18');       // false
$app->validator->check('99',  'maxValue:100');      // true
$app->validator->check('50',  'between:1,100');     // true
$app->validator->check('0',   'between:1,100');     // false

// Common usage: age validation
$rules = ['age' => 'required|integer|minValue:18|maxValue:120'];

Rules: Array Count

Validate the number of items in an array field. Always combine with the array rule.

minCount:n

Array must contain at least n items.

maxCount:n

Array must contain at most n items.

$rules = [
    'tags'   => 'required|array|minCount:1|maxCount:5',
    'photos' => 'nullable|array|maxCount:10',
];

$app->validator->check(['php', 'laravel'], 'array|minCount:1'); // true
$app->validator->check([],                    'array|minCount:1'); // false

Rules: in / notIn / accepted / declined

in:val1,val2,...

Value must be one of the provided values (strict string comparison).

notIn:val1,val2,...

Value must not be in the provided list.

accepted

Value must be one of: true, 1, '1', 'true', 'yes', 'on'. Use for checkbox or consent fields.

declined

Value must be one of: false, 0, '0', 'false', 'no', 'off'.

$app->validator->check('admin',  'in:admin,editor,viewer');  // true
$app->validator->check('guest',  'in:admin,editor,viewer');  // false
$app->validator->check('admin',  'notIn:root,superuser');     // true

$app->validator->check('yes',    'accepted');  // true
$app->validator->check('1',      'accepted');  // true
$app->validator->check('no',     'declined');  // true

// Useful for GDPR consent
$rules = ['terms_accepted' => 'required|accepted'];

Rules: confirmed / same / different

Rules that compare a field against another field in the same data set.

confirmed

Value must match the field named {field}_confirmation. Classic use case: password confirmation.

same:otherField

Value must equal the value of otherField. More flexible than confirmed — works with any field name.

different:otherField

Value must be different from otherField.

// confirmed -- looks for 'password_confirmation' automatically
$rules = ['password' => 'required|strongPassword|confirmed'];
$data  = ['password' => 'Secret@1', 'password_confirmation' => 'Secret@1'];

// same -- explicit field name
$rules = ['confirm_email' => 'required|same:email'];

// different -- new password must differ from old
$rules = ['new_password' => 'required|strongPassword|different:old_password'];

Rules: Date Validation

date

Value must be any string parseable by PHP's strtotime(). Accepts a wide range of formats: '2024-03-15', 'March 15 2024', 'next Monday', etc.

dateFormat:format

Value must match an exact date format (uses DateTime::createFromFormat()). Stricter than date — use when you need a specific format.

before:date

Value must be a date that comes before the given date. Accepts any strtotime()-parseable date, including 'today'.

after:date

Value must be a date that comes after the given date.

$app->validator->check('2024-03-15',  'date');               // true
$app->validator->check('not-a-date',  'date');               // false

$app->validator->check('2024-03-15',  'dateFormat:Y-m-d');   // true
$app->validator->check('15/03/2024',  'dateFormat:Y-m-d');   // false

$app->validator->check('2020-01-01',  'before:2025-01-01');  // true
$app->validator->check('2023-06-01',  'after:2020-01-01');   // true

// Date of birth must be in the past
$rules = ['dob' => 'required|date|before:today'];

Dot Notation — Nested Arrays

Field names in the rules map support dot notation for validating nested array keys. Any depth is supported.

$data = [
    'user' => [
        'email' => '[email protected]',
        'age'   => '25',
    ],
    'address' => [
        'country' => 'BD',
    ],
];

$rules = [
    'user.email'      => 'required|email',
    'user.age'        => 'required|integer|minValue:18',
    'address.country' => 'required|size:2|alpha',
];

$v = $app->validator->make($data, $rules);
$v->passes(); // true

// Errors reference the dotted key
$v->errors();
// ['user.email' => ['...'], ...]

Examples

Practical examples combining multiple rules and features.

User registration form
$v = $app->validator->make($_POST, [
    'username' => 'required|alphaDash|min:3|max:30',
    'email'    => 'required|email|max:255',
    'password' => 'required|strongPassword|confirmed',
    'age'      => 'required|integer|minValue:18|maxValue:120',
    'website'  => 'nullable|url',
    'role'     => 'required|in:admin,editor,viewer',
]);

if ($v->fails()) {
    $app->json(422, ['errors' => $v->errors()]);
}
API endpoint — throw automatically
$body = json_decode(file_get_contents('php://input'), true);

$app->validator->make($body ?? [], [
    'product_id' => 'required|integer|minValue:1',
    'quantity'   => 'required|integer|between:1,99',
])->throw();

// Execution only reaches here if valid
$productId = (int) $body['product_id'];
Custom rule — unique username (with DB check)
$app->validator->addRule('uniqueUsername', function($field, $value, $params, $data) use ($app) {
    $count = $app->db->table('users')->where('username', $value)->count();
    return $count === 0;
});

$app->validator->addMessages([
    'uniqueUsername' => 'That username is already taken.',
]);

$v = $app->validator->make($_POST, [
    'username' => 'required|alphaDash|min:3|uniqueUsername',
]);
Single field quick check
// In a helper or middleware
if (!$app->validator->check($request->age, 'integer|minValue:18')) {
    $app->abort(403, 'You must be 18 or older.');
}
Custom error messages per field
$v = $app->validator->make($_POST, [
    'email'    => 'required|email',
    'password' => 'required|min:8',
], [
    'email.required'    => 'We need your email to create your account.',
    'email.email'       => 'That doesn't look like a valid email address.',
    'password.min'      => 'Your password must be at least 8 characters.',
]);