wscore / leanvalidator
AI-friendly & simple validator
Requires
- php: >=8.1
- ext-mbstring: *
Requires (Dev)
- phpunit/phpunit: ^10.5
README
An AI-friendly and simple validator for PHP.
Features
- Fluent Interface: Easy to read and write validation rules.
- Whitelist Validation:
getValidatedData()returns only the data that has been validated. - Nested Structures: Supports validation of arrays and nested objects using
asList()andasObject(). - Flexible Rules: Use built-in rules, closures, or any PHP callable.
- AI-Friendly: Simple and consistent API that is easy for AI to understand and generate.
Sanitizer:
This package also provides a sanitization utility that can be used to clean and transform input data.
- Sanitization: Automatic UTF-8 conversion and trimming of strings. Supports custom rules and nested data using dot-notation and wildcards.
Installation
Use composer to install the package.
composer require wscore/leanvalidator
Basic Usage
Initialization
You can create a validator instance using the make method.
use Wscore\LeanValidator\Validator; $data = [ 'name' => 'John Doe', 'age' => 25, 'email' => 'john@example.com' ]; $v = Validator::make($data);
Defining Rules
Use field to specify the field and chain validation rules.
$v->field('name')->required()->string(); $v->field('age')->int(18, 99); $v->field('email')->email();
Checking Results
if ($v->isValid()) { // Get only the validated data $validated = $v->getValidatedData(); } else { // Get error messages as a flat array [key => message] $errors = $v->getErrorsFlat(); }
Rules and Messages
Default Error Messages
The validator provides two default error messages for common validation rules.
required: "This field is required."email: "Please enter a valid email address."
To override these messages, set field with a custom message.
$v->field('age', 'Please enter your age (must be above 18)') ->required('You must be at least 18 years old.') ->int(18);
You can change the default messages simply by modifying the messages.
$v->defaultMessage = 'Invalid input!'; $v->defaultMessageRequired = 'Cannot skip this field!';
Custom Error Messages
Use message() to set a custom message for the next rule in the chain.
$v->field('age', 'Please enter your age (must be above 18)') ->required() ->message('Age must be a number')->int() ->message('You must be at least 18')->int(18);
Optional Fields
$v->field('extra_info')->optional()->string(); // With default value $v->field('status')->optional('active')->string();
Required Fields
$v->field('name')->required()->string(); // With a custom message $v->field('title')->required('Title is required')->string();
Conditional Required Fields (requiredIf, requiredWith, requiredWithout)
Use these methods to make a field required based on another field.
requiredIf(string $otherKey, mixed $expect, ?string $msg = null, mixed $elseOverwrite = null)
Required if $otherKey's value matches $expect.
// 'state' is required only if 'country' is 'US' $v->field('state')->requiredIf('country', 'US')->string();
requiredUnless(string $otherKey, mixed $expect, ?string $msg = null, mixed $elseOverwrite = null)
Required unless $otherKey's value matches $expect.
// 'state' is required only if 'country' is 'US' $v->field('state')->requiredUnless('country', 'US')->string();
requiredWith(string $otherKey, ?string $msg = null, mixed $elseOverwrite = null)
Required if $otherKey exists in the input data (even if it's null).
// 'confirm_password' is required if 'password' exists $v->field('confirm_password')->requiredWith('password')->string();
requiredWithout(string $otherKey, ?string $msg = null, mixed $elseOverwrite = null)
Required if $otherKey does not exist in the input data.
// 'guest_email' is required if 'user_id' is missing $v->field('guest_email')->requiredWithout('user_id')->email();
requiredWhen(callable $call, ?string $msg = null, mixed $elseOverwrite = null)
Required if the callback $call($data) returns true. The $data contains all input data.
// 'name' is required only if 'type' is 'personal' $v->field('name')->requiredWhen(function($data) { return ($data['type'] ?? '') === 'personal'; })->string();
elseOverwrite
For all these methods, if the condition is not met, you can provide an elseOverwrite value. The field will be set to this value and further validation rules in the chain will be skipped.
$v->field('type')->requiredIf('category', 'special', 'Required', 'default-type')->string();
Built-in Rules
string(): Checks if value is a string.int(?int $min, ?int $max): Checks if value is an integer and within range.float(): Validates if the value is a float.email(): Validates email format.url(): Validates URL format.regex(string $pattern): Validates against a regular expression.alnum(): Validates alphanumeric characters.alpha(): Validates alphabetic characters.numeric(): Validates numeric characters.in(array $choices): Validates if the value is within the given choices.contains(string $needle): Validates if the value contains the needle.equalTo(mixed $expect): Validates if the value is equal to the expected value.length(?int $min, ?int $max): Checks the length of a string.filterVar(int $filter): Uses PHP'sfilter_var.
Advanced Validation
Validating Arrays of Scalars
Use asList() to apply a rule to every element in an array.
$v->field('tags')->asList('string'); // Or with parameters $v->field('scores')->asList('int', 0, 100);
Validating Array Size
Use arrayCount() to validate the number of elements in an array.
$v->field('tags')->arrayCount(1, 5, 'Please provide 1 to 5 tags');
Validating Nested Objects (Associative Arrays)
Use asObject() to validate a nested associative array without using dot-notation in field.
Only the validated fields will be included in getValidatedData() (whitelist).
$data = ['address' => [ 'post_code' => '123-1234', 'town' => 'TOKYO', 'city' => 'Meguro', ]]; $v = Validator::make($data); $v->field('address')->required()->asObject(function (Validator $child) { $child->field('post_code')->required()->regex('/^\d{3}-\d{4}$/'); $child->field('town')->required()->string(); $child->field('city')->required()->string(); }); if ($v->isValid()) { $validated = $v->getValidatedData(); } // ['address' => ['post_code' => '123-1234', 'town' => 'TOKYO', 'city' => 'Meguro']] }
Validating Arrays of Objects (Nested Data)
Use asListObject() to validate complex nested structures.
$data = [ 'users' => [ ['name' => 'John', 'email' => 'john@example.com'], ['name' => 'Jane', 'email' => 'jane@example.com'], ] ]; $v = Validator::make($data); $v->field('users')->asListObject(function(Validator $child) { $child->field('name')->required()->string(); $child->field('email')->required()->email(); });
Custom Validators
You can use apply() to use any callable, closure, or rule class as a validation rule.
// Using a closure $v->field('username')->apply(fn($value) => !in_array($value, ['admin', 'root'])); // Using a closure that operates on the validator instance $v->field('zip')->apply(function() { $this->regex('/^\d{3}-\d{4}$/'); }); // Using external functions $v->field('count')->apply('is_numeric'); // Using first-class callables $v->field('price')->apply($myValidator->checkPrice(...)); // Using external rule classes (instantiated automatically) $v->field('token')->apply(MyCustomRule::class, $options); // Using invokable objects $v->field('price')->apply(new MyInvokableRule(), $minPrice);
Extending: Custom Rules and IDE Completion
You can add project-specific rules with IDE code completion by extending ValidatorRules and overriding createRules() on your Validator (or ValidatorData) subclass.
- Extend ValidatorRules and add your methods (or register names in
$rulesand use@methodin the docblock). - Override
createRules()in your Validator subclass to return your rules instance. - Override
field()with@return YourValidatorRulesso that$v->field('x')is inferred as your class and your custom methods appear in autocomplete.
use Wscore\LeanValidator\Validator; use Wscore\LeanValidator\ValidatorData; use Wscore\LeanValidator\ValidatorRules; class MyValidatorRules extends ValidatorRules { public function postCode(): static { return $this->regex('/^\d{3}-\d{4}$/'); } public function hiragana(): static { return $this->regex('/^[\x{3040}-\x{309F}\s]+$/u'); } } class MyValidator extends Validator { protected function createRules(): ValidatorRules { return new MyValidatorRules($this); } /** @return MyValidatorRules */ public function field(string $key, ?string $errorMsg = null): ValidatorRules { return parent::field($key, $errorMsg); } } // Usage: IDE will suggest postCode() and hiragana() $v = MyValidator::make($data); $v->field('zip')->required()->postCode(); $v->field('name_kana')->required()->hiragana();
Language- or context-specific Rules (Japanese, etc.)
For Japanese-specific validations, use the Wscore\LeanValidator\Rule\Ja class.
use Wscore\LeanValidator\Rule\Ja; $v = Validator::make($data); $v->field('name_kana')->required()->apply(Ja::kana()); $v->field('zip')->required()->apply(Ja::zip());
Available rules in Ja class:
hiragana(): Hiragana only.katakana(): Katakana only.kana(): Hiragana and Katakana.hankakuKana(): Hankaku-Katakana.kanji(): Kanji only.zenkaku(): Zenkaku characters (non-ASCII).zip(): Japanese Zip code (000-0000).tel(): Japanese Phone number.
Network-specific Rules (IP, UUID, etc.)
For network-related validations, use the Wscore\LeanValidator\Rule\Net class.
use Wscore\LeanValidator\Rule\Net; use Wscore\LeanValidator\Rule\Date; $v = Validator::make($data); $v->field('ip_address')->required()->apply(Net::ip()); $v->field('uuid')->required()->apply(Net::uuid()); $v->field('start_date')->required()->apply(Date::htmlDate()); $v->field('start_date')->apply(Date::notFutureDate());
Available rules in Net class:
ip(int $flags = 0): IP address (v4 or v6).ipv4(): IPv4 address.ipv6(): IPv6 address.mac(): MAC address.uuid(): UUID.domain(): Domain name.
Date-specific Rules (HTML date/time, etc.)
For HTML date/time formats and date constraints, use the Wscore\LeanValidator\Rule\Date class.
use Wscore\LeanValidator\Rule\Date; $v = Validator::make($data); $v->field('birthday')->required()->apply(Date::htmlDate()); $v->field('appointment_at')->apply(Date::htmlDateTimeLocal()); $v->field('work_time')->apply(Date::htmlTime()); $v->field('billing_month')->apply(Date::htmlMonth()); $v->field('reporting_week')->apply(Date::htmlWeek()); // Disallow future dates (including today is allowed) $v->field('birthday')->apply(Date::notFutureDate());
Available rules in Date class:
htmlDate(): HTML5<input type="date">(Y-m-d).htmlMonth(): HTML5<input type="month">(Y-m).htmlTime(): HTML5<input type="time">(H:iorH:i:s).htmlDateTimeLocal(): HTML5<input type="datetime-local">(Y-m-d\TH:iorY-m-d\TH:i:s).htmlWeek(): HTML5<input type="week">(YYYY-Www).notFutureDate(?string $format = 'Y-m-d'): Rejects dates strictly in the future, using the given format.
You can also create a Rules subclass for better IDE support:
class ValidatorRulesJa extends ValidatorRules { public function hiragana(): static { return $this->apply(Ja::hiragana()); } public function zip(): static { return $this->apply(Ja::zip()); } }
Rules added in a ValidatorRules subclass by merging into $this->rules in the constructor (e.g. $this->rules = array_merge($this->rules, [ 'ip' => ['filterVar', [FILTER_VALIDATE_IP]] ]);) work with apply('name') and __call, but will not show in IDE completion unless you add a corresponding @method on your Rules subclass.
API Reference
Validator::make(array|string|numeric $data): static
Creates a validator. If a string or number is passed, it treats it as a single item to be validated.
field(string $key, ?string $errorMsg = null): static
Specifies the field to validate. Optionally sets a default error message for any rule in the chain.
required(?string $msg = null): static
Marks the field as required.
optional(mixed $default = null): static
Marks the field as optional. If the field is missing, it will be set to the default value.
requiredIf(string $otherKey, mixed $expect, ?string $msg = null, mixed $elseOverwrite = null): static
Marks the field as required if the value of $otherKey matches $expect.
- If matched: acts like
required($msg). - If not matched:
- If
$elseOverwriteis provided, sets the field to this value and skips further validation. - Otherwise, acts like
optional().
- If
requiredWith(string $otherKey, ?string $msg = null, mixed $elseOverwrite = null): static
Marks the field as required if $otherKey exists.
- If exists: acts like
required($msg). - Otherwise: acts like
optional()or overwrites if$elseOverwriteis provided.
requiredWithout(string $otherKey, ?string $msg = null, mixed $elseOverwrite = null): static
Marks the field as required if $otherKey does not exist.
- If not exists: acts like
required($msg). - Otherwise: acts like
optional()or overwrites if$elseOverwriteis provided.
requiredWhen(callable $call, ?string $msg = null, mixed $elseOverwrite = null): static
Marks the field as required if $call($data) returns true.
- If true: acts like
required($msg). - Otherwise: acts like
optional()or overwrites if$elseOverwriteis provided.
isValid(): bool
Returns true if there are no validation errors.
getValidatedData(): array
Returns the validated data. Throws RuntimeException if validation failed.
getErrorsFlat(): array
Returns a flat array of error messages where keys are the field names.
Sanitizer Features
The Sanitizer class provides various methods to clean and transform input data.
Default Sanitization
- UTF-8 Conversion: Ensures strings are valid UTF-8.
- Trimming: Removes surrounding whitespace using Unicode-aware regex.
Built-in Rules
Use these methods to apply specific transformations:
toUtf8(...$fields): Ensures valid UTF-8.toTrim(...$fields): Trims whitespace.toDigits(...$fields): Removes all non-digit characters.toLower(...$fields): Converts to lowercase.toUpper(...$fields): Converts to uppercase.toKana(...$fields): Converts to Zenkaku-Kana (needsmbstring).toHankaku(...$fields): Converts to Hankaku-Kana (needsmbstring).toZenkaku(...$fields): Converts to Zenkaku-Kana/Alphanumeric (needsmbstring).
Skipping Sanitization
skip(...$fields): Skips all sanitization (useful for passwords).skipTrim(...$fields): Skips only the trimming process.
Dot-Notation and Wildcards
You can target nested data using dot-notation and wildcards.
$s->toDigits('user.tel'); // Target 'tel' inside 'user' $s->toDigits('items.*.code'); // Target 'code' in all elements of 'items' $s->skipTrim('user.*'); // Skip trim for all direct children of 'user'
Adding Custom Rules
You can add your own sanitization rules using addRule().
$s = new Sanitizer(); $s->addRule('stars', function($value) { return str_repeat('*', strlen($value)); }); $s->apply('stars', 'password');
Data Sanitization
By default, the Validator can sanitize input data using the Sanitizer class.
To apply sanitization, you must explicitly call the sanitize() method before validation.
use Wscore\LeanValidator\Sanitizer; $data = [ 'name' => ' John Doe ', 'tel' => '03-1234-5678', 'password' => ' secret ', 'items' => [ ['code' => ' A-123 '], ['code' => ' B-456 '], ] ]; $s = new Sanitizer(); // Configure sanitization $s->skip('password') // Do not trim or clean password ->toDigits('tel') // Remove non-digit characters ->toDigits('items.*.code'); // Use dot-notation and wildcards for nested data // Apply sanitization $cleanData = $s->clean($data); // $cleanData['name'] => 'John Doe' // $cleanData['tel'] => '0312345678' // $cleanData['password'] => ' secret ' // $cleanData['items'][0]['code'] => '123'
By default, the Sanitizer converts strings to UTF-8 and trims surrounding whitespace.
If you don't call sanitize(), the validation will be performed on the raw input data.