On this page
The six validators#
RTLY-Kit has six validators for data that Iranian apps handle every day. They all work the same way. Once you know one, you know them all.
| Class | Checks | Page |
|---|---|---|
NationalCode | 10 digits, mod-11 check digit, place-of-issue hint | National code |
Sheba | IR + 24 digits, ISO 7064 mod-97-10, bank lookup | Sheba and bank card |
BankCard | 16 digits, Luhn checksum, bank lookup by BIN | Sheba and bank card |
Mobile | Shape of 09xxxxxxxxx in five input forms, operator hint, allocation flag | Mobile, postal code, plate |
PostalCode | 10 digits, first digit not 0 | Mobile, postal code, plate |
VehiclePlate | Passenger-car plate shape, split into parts | Mobile, postal code, plate |
All of them live in the RtlyKit\Validation namespace. They accept Persian and Arabic digits as users type them, so you can pass form input straight in.
Two ways to call#
Every validator gives you a plain boolean and a structured result. Use isValid() when you only need yes or no. Use validate() when you want to tell the user why a value was rejected.
<?php
require 'vendor/autoload.php';
use RtlyKit\Validation\NationalCode;
use function RtlyKit\is_national_code;
var_dump(NationalCode::isValid('۰۴۹۹۳۷۰۸۹۹')); // bool(true)
var_dump(is_national_code('0499370899')); // bool(true)
$result = NationalCode::validate('0499370898');
var_dump($result->isValid()); // bool(false)
print_r($result->errors()); // Array ( [0] => invalid_checksum )
The helpers (is_national_code(), validate_sheba() and the others) are namespaced. Import them with use function RtlyKit\is_national_code;. Short global names are opt-in. See Helpers and globals.
The Validator contract#
Every validator implements RtlyKit\Contracts\Validator. It has two static methods:
validate(mixed $value): ResultisValid(mixed $value): bool, a shortcut forvalidate($value)->isValid()
Validators keep no state, so the methods are static. That lets you pick a validator at run time from a class name, for example from a config array:
use RtlyKit\Contracts\Validator;
use RtlyKit\Validation\Mobile;
use RtlyKit\Validation\NationalCode;
use RtlyKit\Validation\PostalCode;
/** @var array<string, class-string<Validator>> $rules */
$rules = [
'national_code' => NationalCode::class,
'mobile' => Mobile::class,
'postal' => PostalCode::class,
];
$input = ['national_code' => '0499370899', 'mobile' => '0912', 'postal' => '1234567890'];
foreach ($rules as $field => $class) {
$result = $class::validate($input[$field]);
echo $field, ': ', $result->isValid() ? 'ok' : implode(',', $result->errors()), "\n";
}
// national_code: ok
// mobile: invalid_format
// postal: ok
null, arrays, objects, oversized strings and invalid UTF-8, gives an invalid Result with a stable error key. Exceptions are for programmer errors elsewhere in the library. See Error handling.The Result object#
RtlyKit\Validation\Result is an immutable value object with three read methods:
| Method | Returns |
|---|---|
isValid() | bool |
errors() | list<string>: stable error keys, empty when valid |
details() | array<string, mixed>: facts the validator found while checking |
Details are present on valid and invalid results, as far as the validator got before it stopped. So you can show the cleaned value or a bank name even next to an error:
use RtlyKit\Validation\BankCard;
$result = BankCard::validate('6037991234567890');
var_dump($result->isValid()); // bool(false)
print_r($result->errors()); // Array ( [0] => invalid_checksum )
echo json_encode($result->details(), JSON_UNESCAPED_UNICODE);
// {"normalized":"6037991234567890","bin":"603799","bank_name":"بانک ملی ایران"}
Detail keys per validator#
| Validator | Detail keys |
|---|---|
NationalCode | normalized, location ({province, city} or null) |
Sheba | normalized, bank_code, bank_name |
BankCard | normalized, bin, bank_name |
Mobile | normalized, operator, allocated |
PostalCode | normalized |
VehiclePlate | normalized, and when the shape matched: two_digit, letter, three_digit, region |
Lookup values (location, bank_name, operator) are null when the table has no entry. A missing entry means "unknown", never "invalid".
Error keys#
Errors are stable keys that a program can read. They do not change between releases, so you can map them to your own messages or translations. The Laravel integration does this. See Laravel validation and cast.
| Key | Meaning | Used by |
|---|---|---|
invalid_type | The value is not a string, an int or an integral finite float | all |
input_too_long | A string longer than 4096 bytes | all |
invalid_format | The shape is wrong (characters, digit count, pattern) | all except PostalCode, which uses it for a leading 0 |
invalid_length | The digit count is not 10 | PostalCode |
repeated_digits | Every digit is the same (1111111111) | NationalCode, BankCard |
invalid_checksum | The check digit or digits do not match | NationalCode, Sheba, BankCard |
invalid_region | A numeric part of the plate is all zeros | VehiclePlate |
A result carries at most one error today, the first check that failed. errors() returns a list so that later versions can report several.
Input handling#
Before any rule runs, the value goes through one shared gate:
- Strings up to 4096 bytes pass. Persian and Arabic digits take two bytes each, so the cap is about 2000 such digits. A longer string gives
input_too_long. intis converted with(string).floatmust be finite, whole, and below 1015 in size. It is converted without an exponent.NaN,INF,1.5and huge floats giveinvalid_type.- Everything else (
null,bool, arrays, objects) givesinvalid_type.
use RtlyKit\Validation\NationalCode;
use RtlyKit\Validation\PostalCode;
print_r(NationalCode::validate(null)->errors()); // [invalid_type]
print_r(NationalCode::validate(1.5)->errors()); // [invalid_type]
print_r(NationalCode::validate(str_repeat('1', 4097))->errors()); // [input_too_long]
print_r(NationalCode::validate("\xff\xfe")->errors()); // [invalid_format]
var_dump(PostalCode::validate(1234567890)->isValid()); // bool(true)
499370899 is not 0499370899, and the validator only sees nine digits. Keep identifiers as strings from the form or database column all the way to the validator.Cleaning the input#
After the gate, each validator cleans the string with its own public normalize() method. They share these rules:
- Persian (
۰-۹) and Arabic-Indic (٠-٩) digits become English digits, usingDigits::toEnglish(). - Separators that users type are removed: spaces, hyphens and the half-space (ZWNJ).
Mobile,BankCardandPostalCodego further and keep only digits. Shebaalso upper-cases the text and adds theIRprefix to a bare 24-digit string.
The cleaned value is always in details()['normalized']. Store that, not the raw input, so the same number is never saved in two spellings.
Lookup tables and where they come from#
Four validators attach a lookup: bank by card BIN, bank by Sheba code, place of issue by national-code prefix, and operator by mobile prefix. The lookups are extras on top of validation. Validity never depends on them.
- Bank card BINs: 39 entries. Sheba bank codes: 38 entries. National-code prefixes: 547 entries.
- An entry is included only when at least two independent sources agree. A missing entry means unknown.
- The 19 Sheba codes in the Central Bank's published specification all match our table, and every mobile prefix we list lies inside a block that the national numbering plan names for mobile service.
Where to go next#
- National code: the algorithm, cleaning and the place-of-issue hint.
- Sheba and bank card: mod-97 and Luhn, bank lookups.
- Mobile, postal code and vehicle plate.
- Limits: every size cap in one place.