elie29 / validator
A library for validating a context (POST, GET etc...) by running given rules.
Installs: 8 743
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 1
Forks: 1
Open Issues: 0
pkg:composer/elie29/validator
Requires (Dev)
- phpunit/phpunit: ^11.5
- symfony/var-dumper: ^6.4
README
Introduction
A library for validating a context (POST, GET, etc...) by running given rules.
Installation
Run the command below to install via Composer:
composer require elie29/validator
Getting Started
Validator requires one or several rules (constraints) to validate a given context.
A basic example with $_POST
<?php use Elie\Validator\Rule\EmailRule; use Elie\Validator\Rule\MultipleAndRule; use Elie\Validator\Rule\NumericRule; use Elie\Validator\Rule\RangeRule; use Elie\Validator\Rule\StringRule; use Elie\Validator\Validator; /** * A key could have multiple rules * - name could not be empty (required and minimum 1 character length) * - age could be empty (non-existent, null or '') otherwise NumericRule is applied * - age could be empty or among several values * - email is required and should be a valid string email */ $rules =[ ['name', StringRule::class, StringRule::MIN => 1, StringRule::REQUIRED => true], ['age', NumericRule::class, NumericRule::MAX => 60], ['age', RangeRule::class, RangeRule::RANGE => [30, 40, 50]], // Use composition instead of validating the key twice ['email', MultipleAndRule::class, MultipleAndRule::REQUIRED => true, MultipleAndRule::RULES => [ [StringRule::class, StringRule::MAX => 255], [EmailRule::class], ]], ]; $validator = new Validator($_POST, $rules, true); // stop processing on error. if ($validator->validate()) { // Validation passed - use validated context $validatedData = $validator->getValidatedContext(); // Process $validatedData safely } else { // Validation failed - handle errors $errors = $validator->getErrors(); // array of error messages // or get formatted string: echo $validator->getImplodedErrors(); // errors separated by <br/> }
Common Usage Patterns
Basic Form Validation
use Elie\Validator\Validator; use Elie\Validator\Rule\{EmailRule, StringRule, NumericRule}; // Validate a contact form $rules = [ ['name', StringRule::class, StringRule::MIN => 2, StringRule::MAX => 100, StringRule::REQUIRED => true], ['email', EmailRule::class, EmailRule::REQUIRED => true], ['age', NumericRule::class, NumericRule::MIN => 18, NumericRule::MAX => 120], ['message', StringRule::class, StringRule::MIN => 10, StringRule::MAX => 1000], ]; $validator = new Validator($_POST, $rules); if ($validator->validate()) { $data = $validator->getValidatedContext(); // All values are trimmed by default and validated sendEmail($data['email'], $data['name'], $data['message']); } else { // Display all errors to user foreach ($validator->getErrors() as $error) { echo "<p class='error'>$error</p>"; } }
API Request Validation
use Elie\Validator\Validator; use Elie\Validator\Rule\{ChoicesRule, BooleanRule, NumericRule}; // Validate API query parameters $rules = [ ['page', NumericRule::class, NumericRule::MIN => 1, NumericRule::CAST => true], ['limit', NumericRule::class, NumericRule::MIN => 1, NumericRule::MAX => 100, NumericRule::CAST => true], ['sort', ChoicesRule::class, ChoicesRule::LIST => ['asc', 'desc']], ['active', BooleanRule::class, BooleanRule::CAST => true], ]; $validator = new Validator($_GET, $rules); $validator->appendExistingItemsOnly(true); // Don't include missing optional parameters if ($validator->validate()) { $params = $validator->getValidatedContext(); // $params only contains provided parameters, properly cast $results = fetchRecords($params); return json_encode($results); } else { http_response_code(400); return json_encode(['errors' => $validator->getErrors()]); }
Working with Optional Fields
// Fields that aren't required can be omitted or empty $rules = [ ['username', StringRule::class, StringRule::MIN => 3, StringRule::REQUIRED => true], ['bio', StringRule::class, StringRule::MAX => 500], // Optional, only validated if provided ['website', EmailRule::class], // Optional URL field ]; $validator = new Validator($data, $rules); $validator->appendExistingItemsOnly(true); // Exclude missing optional fields from output if ($validator->validate()) { $validatedData = $validator->getValidatedContext(); // $validatedData only contains 'username' and any optional fields that were provided }
Conditional Validation
use Elie\Validator\Rule\{MultipleOrRule, EmailRule, MatchRule}; // Accept either email OR phone number $rules = [ ['contact', MultipleOrRule::class, MultipleOrRule::REQUIRED => true, MultipleOrRule::RULES => [ [EmailRule::class], [MatchRule::class, MatchRule::PATTERN => '/^\+?[1-9]\d{1,14}$/'], // E.164 phone format ]], ]; $validator = new Validator(['contact' => 'user@example.com'], $rules); $validator->validate(); // true - email is valid $validator->setContext(['contact' => '+1234567890']); $validator->validate(); // true - phone is valid
Custom Error Messages
use Elie\Validator\Rule\{StringRule, RuleInterface}; $rules = [ ['username', StringRule::class, StringRule::MIN => 3, StringRule::MAX => 20, StringRule::REQUIRED => true, RuleInterface::MESSAGES => [ RuleInterface::EMPTY_KEY => 'Username is required', StringRule::INVALID_MIN_VALUE => 'Username must be at least 3 characters', StringRule::INVALID_MAX_VALUE => 'Username cannot exceed 20 characters', ] ], ]; $validator = new Validator(['username' => 'ab'], $rules); if (!$validator->validate()) { echo $validator->getImplodedErrors(); // "Username must be at least 3 characters" }
Handling Whitespace
// By default, all string values are trimmed $rules = [ ['title', StringRule::class, StringRule::MIN => 1], // Leading/trailing spaces removed ]; // To preserve whitespace: $rules = [ ['code', StringRule::class, StringRule::MIN => 1, 'trim' => false], ]; $validator = new Validator(['title' => ' Hello '], $rules); $validator->validate(); $validated = $validator->getValidatedContext(); // With trim=true (default): $validated['title'] = 'Hello' // With trim=false: $validated['title'] = ' Hello '
Stop on First Error
// Third parameter controls error handling behavior $validator = new Validator($data, $rules, true); // Stops at first error $validator->validate(); // OR set it later $validator = new Validator($data, $rules); $validator->setStopOnError(true); if (!$validator->validate()) { // Only the first error will be in getErrors() $firstError = $validator->getErrors()[0]; }
Available rules
- All Rules accept
required,trimandmessagesoptions.requiredis false by default whiletrimis true. - ArrayRule accepts
minandmaxoptions. Empty value is cast to an empty array []. - BicRule validates Bank Identifier Code ( SWIFT-BIC).
- BooleanRule accepts
castoption. - CallableRule accepts
callablefunction. - ChoicesRule accepts
listoption. - CollectionRule accepts
rulesandjsonoptions. - CompareRule accepts
signandexpectedoptions.signis CompareRule::EQ by default,expectedis null by default. - DateRule accepts
formatandseparatoroptions. - EmailRule validates email addresses.
- IpRule accepts
flagoption. - JsonRule accepts
decodeoption. - MatchRule requires
patternoption. - MultipleAndRule requires
rulesoption (all rules must pass). - MultipleOrRule requires
rulesoption (at least one rule must pass). - NumericRule accepts
min,maxandcastoptions. - RangeRule accepts
rangeoption. - StringCleanerRule removes invisible characters from strings.
- StringRule accepts
minandmaxoptions. - TimeRule validates time format.
- Your own rule
How to add a new rule
You need to implement RuleInterface or to extend AbstractRule
<?php use Elie\Validator\Rule\AbstractRule; class MyValueRule extends AbstractRule { public const INVALID_MY_VALUE = 'invalidMyValue'; protected mixed $my_value = null; public function __construct(int|string $key, mixed $value, array $params = []) { parent::__construct($key, $value, $params); if (isset($params['my_value'])) { $this->my_value = $params['my_value']; } // + to add a non-existent key $this->messages += [ $this::INVALID_MY_VALUE => '%key%: %value% my message %my_value%' ]; } public function validate(): int { $run = parent::validate(); if ($run !== $this::CHECK) { return $run; } if ($this->value !== $this->my_value) { return $this->setAndReturnError($this::INVALID_MY_VALUE, [ '%my_value%' => $this->stringify($this->my_value) ]); } return $this::VALID; } }
Understanding Validated Context
The validated context is the processed output after validation runs. It's not the same as your input data:
Key Differences from Input
use Elie\Validator\Rule\{StringRule, NumericRule, BooleanRule}; $input = [ 'name' => ' John Doe ', // Has whitespace 'age' => '25', // String representation 'active' => 'true', // String boolean ]; $rules = [ ['name', StringRule::class], ['age', NumericRule::class, NumericRule::CAST => true], ['active', BooleanRule::class, BooleanRule::CAST => true], ]; $validator = new Validator($input, $rules); $validator->validate(); $validated = $validator->getValidatedContext(); // Results: // $validated['name'] = 'John Doe' // Trimmed // $validated['age'] = 25 // Cast to int // $validated['active'] = true // Cast to bool // Original input is unchanged: // $input['name'] = ' John Doe ' // $input['age'] = '25'
Controlling Output Keys
By default, all rules' keys appear in validated context, even if not in input:
$input = ['username' => 'john']; $rules = [ ['username', StringRule::class], ['email', EmailRule::class], // Not in input ]; $validator = new Validator($input, $rules); $validator->validate(); // Default behavior: $validated = $validator->getValidatedContext(); // $validated = ['username' => 'john', 'email' => null] // To exclude missing keys: $validator->appendExistingItemsOnly(true); $validator->validate(); $validated = $validator->getValidatedContext(); // $validated = ['username' => 'john']
When to Use appendExistingItemsOnly(true)
- API endpoints - Only return fields that were provided
- Partial updates - PATCH operations where only some fields update
- Optional configurations - Settings where absence has meaning
// Example: Partial user profile update $rules = [ ['name', StringRule::class, StringRule::MAX => 100], ['bio', StringRule::class, StringRule::MAX => 500], ['website', StringRule::class, StringRule::MAX => 255], ]; $validator = new Validator($_POST, $rules); $validator->appendExistingItemsOnly(true); if ($validator->validate()) { $updates = $validator->getValidatedContext(); // Only update fields that were actually submitted updateUserProfile($userId, $updates); }
Assertion Integration
Instead of using assertion key by key, you can validate the whole context and then use Assertion or Assert as follows:
<?php use Assert\Assertion; use Elie\Validator\Rule\EmailRule; use Elie\Validator\Rule\NumericRule; use Elie\Validator\Rule\RuleInterface; use Elie\Validator\Rule\StringRule; use Elie\Validator\Validator; use Webmozart\Assert\Assert; $rules =[ ['age', NumericRule::class, NumericRule::MAX => 60], ['name', StringRule::class, StringRule::MIN => 1, StringRule::REQUIRED => true], ['email', EmailRule::class, EmailRule::REQUIRED => true], ]; $validator = new Validator($_POST, $rules); // Using webmozart/assert Assert::true($validator->validate(), $validator->getImplodedErrors()); // OR using beberlei/assert Assertion::true($validator->validate(), $validator->getImplodedErrors()); // OR using PHPUnit in tests $this->assertSame(RuleInterface::VALID, $validator->validate(), $validator->getImplodedErrors());
Partial Validation
Sometimes we need to validate the context partially, whenever we have a JSON item or keys that depend on each other.
The following is an example when a context - e.g., $_POST - should contain JSON user data:
use Elie\Validator\Rule\JsonRule; use Elie\Validator\Rule\MatchRule; use Elie\Validator\Rule\NumericRule; use Elie\Validator\Validator; $rules = [ ['user', JsonRule::class, JsonRule::REQUIRED => true], ]; $validator = new Validator($_POST, $rules); Assertion::true($validator->validate()); // this assertion validates that the user is in JSON format $validatedPost = $validator->getValidatedContext(); // But we need to validate user data as well (suppose it should contain name and age): $rules = [ ['name', MatchRule::class, MatchRule::PATTERN => '/^[a-z]{1,20}$/i'], ['age', NumericRule::class, NumericRule::MAX => 80], ]; $validator->setRules($rules); // Decode user as it is a valid JSON $user = json_decode($validatedPost['user'], true); $validator->setContext($user); // the new context is now user data Assertion::true($validator->validate()); // this assertion validates user data /* Validate accepts a boolean argument - mergedValidatedContext - which is false by default. If set to true, $validator->getValidatedContext() would return: array:4 [▼ "email" => "elie29@gmail.com" "user" => "{"name": "John", "age": 25}" "name" => "John" "age" => 25 ] */
Partial Validation with a multidimensional array
Usually with JsonRule, we could expect a multidimensional array. In this case, the validation process will be similar to Partial Validation without merging data:
$rules = [ // With json-decode, a validated value will be decoded into an array ['users', JsonRule::class, JsonRule::REQUIRED => true, JsonRule::DECODE => true], ]; $validator = new Validator([ 'users' => '[{"name":"John","age":25},{"name":"Brad","age":42}]' ], $rules); Assertion::true($validator->validate()); // this validates that users is a valid JSON format // But we need to validate all user data as well (suppose it should contain name and age): $validator->setRules([ ['name', MatchRule::class, MatchRule::PATTERN => '/^[a-z]{1,20}$/i'], ['age', NumericRule::class, NumericRule::MAX => 80], ]); $validatedContext = $validator->getValidatedContext(); $users = $validatedContext['users']; Assertion::isArray($users); foreach ($users as $user) { // each user is a new context $validator->setContext($user); // do not merge data !! Assertion::true($validator->validate()); // we could validate all users and determine which ones are invalid! }
A new CollectionRule has been added to validate collection data (array or JSON) as follows:
$rules = [ ['users', CollectionRule::class, CollectionRule::JSON => true, CollectionRule::RULES => [ ['name', MatchRule::class, MatchRule::PATTERN => '/^[a-z]{1,20}$/i'], ['age', NumericRule::class, NumericRule::MAX => 80], ]], ]; $data = [ 'users' => '[{"name":"John","age":25},{"name":"Brad","age":42}]' ]; $validator = new Validator($data, $rules); $this->assertSame(RuleInterface::VALID, $validator->validate()); $users = $validator->getValidatedContext()['users']; $this->assertCount(2, $users);
Best Practices and Tips
Rule Parameter Keys
Always use class constants for rule parameters, not strings:
// ✅ CORRECT ['age', NumericRule::class, NumericRule::MIN => 18, NumericRule::MAX => 120] // ❌ WRONG - 'min' and 'max' as strings won't work ['age', NumericRule::class, 'min' => 18, 'max' => 120]
Validation Flow Pattern
// Recommended pattern for all validations: $validator = new Validator($inputData, $rules); if (!$validator->validate()) { // Handle errors first logErrors($validator->getErrors()); return ['success' => false, 'errors' => $validator->getErrors()]; } // Only proceed with validated data $safeData = $validator->getValidatedContext(); processData($safeData);
Reusing Validator Instances
Validators can be reused for multiple validations:
$validator = new Validator([], $userRules); foreach ($batchData as $userData) { $validator->setContext($userData); if ($validator->validate()) { processUser($validator->getValidatedContext()); } else { logErrors($userData['id'], $validator->getErrors()); } }
Composition vs Multiple Rules
// When rules must ALL pass, use MultipleAndRule: ['email', MultipleAndRule::class, MultipleAndRule::RULES => [ [StringRule::class, StringRule::MAX => 255], [EmailRule::class], ]] // When ANY rule can pass, use MultipleOrRule: ['identifier', MultipleOrRule::class, MultipleOrRule::RULES => [ [EmailRule::class], [MatchRule::class, MatchRule::PATTERN => '/^\d{10}$/'], // 10-digit ID ]] // Avoid chaining the same key twice - use composition instead: // ❌ Less efficient: ['email', StringRule::class, StringRule::MAX => 255], ['email', EmailRule::class], // ✅ Better - uses composition: ['email', MultipleAndRule::class, MultipleAndRule::RULES => [ [StringRule::class, StringRule::MAX => 255], [EmailRule::class], ]]
Testing Your Validations
use PHPUnit\Framework\TestCase; use Elie\Validator\Rule\RuleInterface; class UserValidatorTest extends TestCase { public function testValidUserData(): void { $validator = new Validator( ['username' => 'john', 'age' => 25], $this->getUserRules() ); $this->assertTrue($validator->validate()); $this->assertEmpty($validator->getErrors()); } public function testInvalidAge(): void { $validator = new Validator( ['username' => 'john', 'age' => 150], $this->getUserRules() ); $this->assertFalse($validator->validate()); $this->assertNotEmpty($validator->getErrors()); $this->assertStringContainsString('age', $validator->getImplodedErrors()); } }
Performance Considerations
- Use
stopOnError => truefor large datasets when you only need to know if validation passed - Use
appendExistingItemsOnly(true)to reduce memory footprint with sparse data - Reuse validator instances instead of creating new ones in loops
- For nested/complex validations, use CollectionRule instead of manual iteration
Development Prerequisites
Text file encoding
- UTF-8
Composer commands
composer test: Runs unit tests without coveragecomposer test-coverage: Runs unit tests with code coverage (requires Xdebug)composer cover: Runs tests with coverage and starts a local server to view coverage report at http://localhost:5001composer clean: Cleans all generated files (build directory and clover.xml)