DownloadContextual Validator ? Cross-Field Coherence Checker for PHP
Most PHP validation libraries check one field at a time: is this a valid
email, is this string non-empty, is this number in range. That catches
typos, but it misses an entire class of bugs where each field is
individually valid, yet the form as a whole is inconsistent.
This class validates relationships between fields instead of fields in
isolation.
The problem
All of these pass every ordinary field-by-field validator, yet every one
of them is wrong:
-
postal code `90210` submitted with country `France`
-
an `end_date` earlier than `start_date`
-
a `birthdate` of 1990 combined with a declared `age` of 15
-
a phone number with a US-length number paired with `country = Germany`
-
a `password_confirmation` that doesn't match `password`
-
a `min_price` filter greater than the `max_price` filter
Installation
Copy src/ContextualValidator.php into your project, or require it
directly. No dependencies.
Usage
require 'src/ContextualValidator.php';
use ContextValidate\ContextualValidator;
$validator = new ContextualValidator($_POST);
$validator
->postalCodeMatchesCountry('zip', 'country')
->dateRange('start_date', 'end_date')
->ageMatchesBirthdate('age', 'birthdate')
->phoneMatchesCountry('phone', 'country')
->fieldsMatch('password', 'password_confirmation')
->numericRange('min_price', 'max_price');
if ($validator->fails()) {
foreach ($validator->errors() as $field => $messages) {
echo "$field: " . implode(', ', $messages) . "\n";
}
}
See examples/example.php for a full runnable demo with a form that
fails every single rule at once.
Built-in rules
| Method | Checks |
|---|---|
| postalCodeMatchesCountry($postalField, $countryField) | Postal code format matches the ISO country code (25+ countries built in) |
| dateRange($startField, $endField, $allowEqual = true) | Start date is not after end date |
| ageMatchesBirthdate($ageField, $birthdateField, $toleranceYears = 0, $referenceDate = null) | Declared age matches computed age from birthdate |
| phoneMatchesCountry($phoneField, $countryField) | Phone number digit count is plausible for the given country |
| fieldsMatch($fieldA, $fieldB, $message = null) | Two fields hold identical values (e.g. password confirmation) |
| numericRange($minField, $maxField) | A "min" field is not greater than a "max" field |
| custom($field, $callable) | Any custom cross-field rule; return true or an error string |
Every rule skips silently (does not add an error) when a required
field is empty or when the country isn't in the built-in list - it's
not this class's job to require fields or judge unknown countries; pair
it with your usual field-level validator for that.
Extending country coverage
ContextualValidator::addPostalCodePattern('XX', '/^\d{5}$/');
Limitations
-
Phone validation checks digit count plausibility only, not real
numbering-plan rules. For full phone number validation, pair this
with a dedicated library (e.g. a libphonenumber port) and use
`custom()` to plug it in as a cross-field rule.
-
Postal code coverage is currently ~25 countries; unknown countries
are skipped rather than flagged, by design.
Tests
php tests/run-tests.php
License
MIT ? see LICENSE.
|