-
Notifications
You must be signed in to change notification settings - Fork 0
Validation
Razy provides a comprehensive validation system with field validators, nested data support (dot-notation wildcards), form requests, and 14 built-in rules. The system follows a pipeline pattern where rules execute sequentially with bail-on-failure support.
use Razy\Validation\Validator;
use Razy\Validation\Rule\{Required, Email, MinLength};
// One-shot validation
$result = Validator::make($_POST, [
'name' => [new Required(), new MinLength(2)],
'email' => [new Required(), new Email()],
]);
if ($result->fails()) {
$errors = $result->errors();
// ['name' => ['Name is required.'], 'email' => [...]]
}
$validated = $result->validated();
// ['name' => 'John', 'email' => 'john@example.com']The Validator class orchestrates multi-field validation with before/after hooks:
use Razy\Validation\Validator;
use Razy\Validation\Rule\{Required, Email, MinLength, Between};
$validator = new Validator($_POST);
// Define fields and their rules
$validator->field('username')
->rule(new Required())
->rule(new MinLength(3));
$validator->field('email')
->rule(new Required())
->rule(new Email());
$validator->field('age')
->rule(new Required())
->rule(new Between(18, 120));
// Set defaults for missing fields
$validator->defaults([
'role' => 'user',
'active' => true,
]);
// Stop on first field failure
$validator->stopOnFirstFailure();
// Before hook → transform data before validation
$validator->before(function (array &$data) {
$data['email'] = strtolower(trim($data['email'] ?? ''));
});
// After hook → post-validation processing
$validator->after(function (array $validated, array $errors) {
if (empty($errors)) {
// log successful validation
}
});
$result = $validator->validate();$validator->fields([
'name' => [new Required(), new MinLength(2)],
'email' => [new Required(), new Email()],
'role' => [new In(['admin', 'user', 'editor'])],
]);Each field gets its own FieldValidator that manages a rule pipeline:
use Razy\Validation\FieldValidator;
use Razy\Validation\Rule\{Required, MinLength, Regex};
$field = new FieldValidator('username');
// Add rules individually
$field->rule(new Required());
$field->rule(new MinLength(3));
$field->rule(new Regex('/^[a-zA-Z0-9_]+$/'));
// Or in bulk
$field->rules([new Required(), new MinLength(3)]);
// Bail on first failure (default: true)
$field->bail(true);
// Conditional rules
$field->when($isAdmin, function (FieldValidator $f) {
$f->rule(new MinLength(5));
});
// Validate a single value
$result = $field->validate('john_doe', $_POST);
// Returns: ['value' => 'john_doe', 'errors' => []]All 14 built-in rules extend ValidationRule and implement ValidationRuleInterface. Every rule (except Required) skips validation on null/empty values — add Required first if presence is mandatory.
use Razy\Validation\Rule\Required;
// Fails on null, empty string, empty array
$validator->field('name')->rule(new Required());use Razy\Validation\Rule\Email;
$validator->field('email')->rule(new Email());
// Uses FILTER_VALIDATE_EMAILuse Razy\Validation\Rule\Url;
$validator->field('website')->rule(new Url());
// Uses FILTER_VALIDATE_URLuse Razy\Validation\Rule\Numeric;
$validator->field('price')->rule(new Numeric());
// Uses is_numeric() → accepts ints, floats, numeric stringsuse Razy\Validation\Rule\{MinLength, MaxLength};
$validator->field('password')
->rule(new MinLength(8)) // mb_strlen >= 8
->rule(new MaxLength(128)); // mb_strlen <= 128use Razy\Validation\Rule\Between;
// Numeric value must be in [min, max] inclusive
$validator->field('age')->rule(new Between(18, 120));
$validator->field('price')->rule(new Between(0.01, 9999.99));use Razy\Validation\Rule\Regex;
$validator->field('slug')->rule(new Regex('/^[a-z0-9\-]+$/'));use Razy\Validation\Rule\In;
// Loose comparison (default)
$validator->field('status')->rule(new In(['active', 'inactive', 'pending']));
// Strict comparison
$validator->field('type')->rule(new In([1, 2, 3], strict: true));use Razy\Validation\Rule\IsArray;
$validator->field('tags')->rule(new IsArray());use Razy\Validation\Rule\Date;
// Any parseable date
$validator->field('birthday')->rule(new Date());
// Specific format
$validator->field('invoice_date')->rule(new Date('Y-m-d'));use Razy\Validation\Rule\Confirmed;
// Looks for {field}_confirmation in data
$validator->field('password')->rule(new Confirmed());
// Checks $_POST['password_confirmation'] matches
// Custom confirmation field name
$validator->field('password')->rule(new Confirmed('password_repeat'));use Razy\Validation\Rule\Callback;
// Custom validation logic
$validator->field('username')->rule(new Callback(function (mixed $value, string $field, array $data) {
// Return true/false
return !in_array($value, ['admin', 'root', 'system']);
}));
// Can also return transformed value
$validator->field('tags')->rule(new Callback(function (mixed $value) {
if (is_string($value)) {
return explode(',', $value); // transform string to array
}
return $value;
}));Validates each element of an array against a set of rules:
use Razy\Validation\Rule\{Each, Required, Email};
// Validate that every email in the list is valid
$validator->field('emails')
->rule(new IsArray())
->rule(new Each([new Required(), new Email()]));
// Access per-item errors
$each = new Each([new Required(), new MinLength(3)]);
$each->validate(['ab', '', 'hello'], 'items', []);
$itemErrors = $each->getItemErrors();
// [0 => ['items.0 must be at least 3 characters.'], 1 => ['items.1 is required.']]Create custom validation rules by extending ValidationRule:
use Razy\Validation\ValidationRule;
class UniqueEmail extends ValidationRule
{
public function __construct(
private readonly UserRepository $repo
) {}
public function validate(mixed $value, string $field, array $data): mixed
{
if ($this->repo->emailExists($value)) {
$this->fail();
}
return $value;
}
protected function defaultMessage(): string
{
return ':field is already taken.';
}
}
// Usage
$validator->field('email')
->rule(new Required())
->rule(new Email())
->rule(new UniqueEmail($userRepo));Override the default message on any rule:
$validator->field('email')
->rule((new Required())->withMessage('Please provide your email address.'))
->rule((new Email())->withMessage('That doesn\'t look like a valid email.'));The :field placeholder is automatically replaced with the field name.
An immutable value object containing validation results:
$result = $validator->validate();
// Pass/fail checks
$result->passes(); // bool
$result->fails(); // bool
// Get validated data (only fields that passed)
$data = $result->validated();
// ['name' => 'John', 'email' => 'john@example.com']
// Get a single validated value
$name = $result->get('name');
$role = $result->get('role', 'default');
// Error access
$allErrors = $result->errors();
// ['email' => ['Email is required.', 'Email is invalid.']]
$emailErrors = $result->errorsFor('email');
// ['Email is required.', 'Email is invalid.']
$firstEmailError = $result->firstError('email');
// 'Email is required.'
$firstErrors = $result->firstErrors();
// ['email' => 'Email is required.', 'name' => 'Name is required.']
$hasEmailError = $result->hasError('email'); // bool
// Flat list of all error messages
$flat = $result->allErrors();
// ['Email is required.', 'Email is invalid.', 'Name is required.']
$count = $result->errorCount(); // total number of errorsValidates deeply nested data structures using dot-notation paths and wildcard expansion (*):
use Razy\Validation\NestedValidator;
use Razy\Validation\Rule\{Required, Email, MinLength, Numeric, IsArray};
$data = [
'user' => [
'name' => 'John',
'email' => 'john@example.com',
],
'items' => [
['sku' => 'ABC', 'qty' => 2, 'price' => 19.99],
['sku' => 'DEF', 'qty' => 1, 'price' => 49.99],
],
'shipping' => [
'address' => [
'city' => 'Taipei',
'zip' => '100',
],
],
];
$validator = new NestedValidator($data);
// Dot-notation for nested fields
$validator->field('user.name', [new Required(), new MinLength(2)]);
$validator->field('user.email', [new Required(), new Email()]);
// Wildcard (*) expands to each array element
$validator->field('items.*.sku', [new Required()]);
$validator->field('items.*.qty', [new Required(), new Numeric()]);
$validator->field('items.*.price', [new Required(), new Numeric()]);
// Deeply nested
$validator->field('shipping.address.city', [new Required()]);
$validator->field('shipping.address.zip', [new Required()]);
$result = $validator->validate();$result = NestedValidator::make($data, [
'user.name' => [new Required(), new MinLength(2)],
'user.email' => [new Required(), new Email()],
'items.*.sku' => [new Required()],
'items.*.price' => [new Required(), new Numeric()],
]);The NestedValidator exposes static utilities for working with nested arrays:
use Razy\Validation\NestedValidator;
// Get value by dot-path
$city = NestedValidator::dataGet($data, 'shipping.address.city');
$sku = NestedValidator::dataGet($data, 'items.0.sku');
// Set value by dot-path
NestedValidator::dataSet($data, 'shipping.address.country', 'TW');
// Check existence
NestedValidator::dataHas($data, 'user.email'); // true
NestedValidator::dataHas($data, 'user.phone'); // falseAn abstract class for structured form validation with authorization, auto-population from HTTP input, and lifecycle hooks:
use Razy\Validation\FormRequest;
use Razy\Validation\Rule\{Required, Email, MinLength, Numeric, Between};
class CreateOrderRequest extends FormRequest
{
public function authorize(): bool
{
// Return false to block with 403
return true;
}
public function rules(): array
{
return [
'customer_email' => [new Required(), new Email()],
'items' => [new Required(), new IsArray()],
'total' => [new Required(), new Numeric(), new Between(0.01, 99999)],
];
}
public function defaults(): array
{
return [
'currency' => 'USD',
'status' => 'pending',
];
}
public function messages(): array
{
return [
// Override default messages per rule
];
}
public function prepareForValidation(array $data): array
{
// Transform data before validation
$data['customer_email'] = strtolower(trim($data['customer_email'] ?? ''));
return $data;
}
}// From $_GET + $_POST
$request = CreateOrderRequest::fromGlobals();
// From JSON body (php://input)
$request = CreateOrderRequest::fromJson();
// From array
$request = CreateOrderRequest::fromArray($data);$request = CreateOrderRequest::fromJson();
// Check authorization + validation
if ($request->fails()) {
$errors = $request->errors();
// If unauthorized: ['_authorization' => ['This action is unauthorized.']]
$json = $request->errorsAsJson(JSON_PRETTY_PRINT);
return;
}
// Get validated data
$validated = $request->validated();
$email = $request->input('customer_email');
// Data access helpers
$all = $request->all(); // raw input
$subset = $request->only(['customer_email', 'total']);
$without = $request->except(['_token']);
$exists = $request->has('customer_email'); // bool
$filled = $request->filled('customer_email'); // present and non-empty| Method | Signature | Description |
| --- | --- | --- |
| __construct | (array $data = []) | Create with input data |
| setData | (array $data): static | Replace input data |
| field | (string $name): FieldValidator | Get/create field validator |
| fields | (array $fieldRules): static | Bulk define fields |
| defaults | (array $defaults): static | Set default values |
| stopOnFirstFailure | (bool $stop = true): static | Stop at first failed field |
| before | (callable $callback): static | Pre-validation hook |
| after | (callable $callback): static | Post-validation hook |
| validate | (): ValidationResult | Run validation |
| make (static) | (array $data, array $rules): ValidationResult | One-shot factory |
| Method | Signature | Description |
| --- | --- | --- |
| rule | (ValidationRuleInterface $rule): static | Add a rule |
| rules | (array $rules): static | Add multiple rules |
| bail | (bool $stop = true): static | Stop on first failure |
| when | (bool $condition, callable $callback): static | Conditional rules |
| validate | (mixed $value, array $data = []): array | Returns {value, errors} |
| getField | (): string | Field name |
| getRules | (): array | Registered rules |
| Method | Signature | Description |
| --- | --- | --- |
| passes | (): bool | All passed |
| fails | (): bool | Any failed |
| errors | (): array | array<field, string[]> |
| errorsFor | (string $field): array | Errors for one field |
| firstError | (string $field): ?string | First error for field |
| firstErrors | (): array | array<field, string> |
| hasError | (string $field): bool | Check field has errors |
| validated | (): array | Validated data |
| get | (string $field, mixed $default = null): mixed | Single value |
| allErrors | (): array | Flat error list |
| errorCount | (): int | Total error count |
| Rule | Constructor | Validates |
| --- | --- | --- |
| Required | (none) | Non-null, non-empty |
| Email | (none) | Valid email format |
| Url | (none) | Valid URL format |
| Numeric | (none) | Numeric value |
| IsArray | (none) | Array type |
| MinLength | (int $min) | String length — min |
| MaxLength | (int $max) | String length — max |
| Between | (float\|int $min, float\|int $max) | Numeric in [min, max] |
| Regex | (string $pattern) | Matches regex |
| In | (array $allowed, bool $strict = false) | Value in allowed list |
| Date | (?string $format = null) | Valid date / format |
| Confirmed | (?string $confirmationField = null) | Cross-field match |
| Callback | (callable $callback) | Custom logic |
| Each | (array $rules) | Per-element validation |
| Problem | Cause | Fix |
| --- | --- | --- |
| Required rule ignored | Required placed after other rules | Always list Required first in the rule chain |
| Validation stops too early | Bail causes first failure to skip remaining rules | Remove Bail or reorder rules if you need all errors |
| Wildcard path not working | Using . instead of * for array items | Use items.*.name for nested array validation |
| Custom callback not called | Callback returns false without a message | Return a string message on failure, or throw an exception |
| Confirmed always fails | Confirmation field name doesn't match convention | Ensure password_confirmation field exists for password |