Validators overview

How the six Iranian validators work. The shared Validator contract, the Result object, stable error keys, input cleaning and size limits.

On this page
  1. The six validators
  2. Two ways to call
  3. The Validator contract
  4. The Result object
    1. Detail keys per validator
  5. Error keys
  6. Input handling
  7. Cleaning the input
  8. Lookup tables and where they come from
  9. Where to go next

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.

ClassChecksPage
NationalCode10 digits, mod-11 check digit, place-of-issue hintNational code
ShebaIR + 24 digits, ISO 7064 mod-97-10, bank lookupSheba and bank card
BankCard16 digits, Luhn checksum, bank lookup by BINSheba and bank card
MobileShape of 09xxxxxxxxx in five input forms, operator hint, allocation flagMobile, postal code, plate
PostalCode10 digits, first digit not 0Mobile, postal code, plate
VehiclePlatePassenger-car plate shape, split into partsMobile, 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): Result
  • isValid(mixed $value): bool, a shortcut for validate($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
Never throws. Neither method throws for bad input. Any value, including 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:

MethodReturns
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#

ValidatorDetail keys
NationalCodenormalized, location ({province, city} or null)
Shebanormalized, bank_code, bank_name
BankCardnormalized, bin, bank_name
Mobilenormalized, operator, allocated
PostalCodenormalized
VehiclePlatenormalized, 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.

KeyMeaningUsed by
invalid_typeThe value is not a string, an int or an integral finite floatall
input_too_longA string longer than 4096 bytesall
invalid_formatThe shape is wrong (characters, digit count, pattern)all except PostalCode, which uses it for a leading 0
invalid_lengthThe digit count is not 10PostalCode
repeated_digitsEvery digit is the same (1111111111)NationalCode, BankCard
invalid_checksumThe check digit or digits do not matchNationalCode, Sheba, BankCard
invalid_regionA numeric part of the plate is all zerosVehiclePlate

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.
  • int is converted with (string).
  • float must be finite, whole, and below 1015 in size. It is converted without an exponent. NaN, INF, 1.5 and huge floats give invalid_type.
  • Everything else (null, bool, arrays, objects) gives invalid_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)
Good to know. A national code or postal code passed as an integer loses its leading zeros. 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, using Digits::toEnglish().
  • Separators that users type are removed: spaces, hyphens and the half-space (ZWNJ). Mobile, BankCard and PostalCode go further and keep only digits.
  • Sheba also upper-cases the text and adds the IR prefix 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.
Good to know. The lookup tables come from public lists. They are not an official registry. Banks merge and rename, and operators are portable. Treat a lookup result as a hint. Do not base a legal, financial or identity decision on it. Sources and dates are in Accuracy and data.

Where to go next#