On this page
Quick start#
<?php
require 'vendor/autoload.php';
use RtlyKit\Validation\Mobile;
use RtlyKit\Validation\PostalCode;
use RtlyKit\Validation\VehiclePlate;
use function RtlyKit\is_mobile;
use function RtlyKit\is_postal_code;
use function RtlyKit\is_vehicle_plate;
var_dump(is_mobile('+98 912 123 4567')); // bool(true)
var_dump(is_postal_code('۱۲۳۴۵-۶۷۸۹۰')); // bool(true)
var_dump(is_vehicle_plate('12ب345 ایران 67')); // bool(true)
echo Mobile::getOperator('09121234567'), "\n"; // همراه اول
All three implement the shared Validator contract. Their helpers are validate_mobile(), validate_postal_code() and validate_vehicle_plate().
Mobile numbers#
Accepted forms#
Mobile turns every input into the national form 09xxxxxxxxx (11 digits). It accepts these five spellings, with Persian or Arabic digits, and with spaces, hyphens or parentheses mixed in:
| Input | normalized |
|---|---|
09121234567 | 09121234567 |
9121234567 | 09121234567 |
989121234567 | 09121234567 |
+989121234567 | 09121234567 |
00989121234567 | 09121234567 |
۰۹۱۲ ۱۲۳ ۴۵۶۷ | 09121234567 |
(0912) 123-4567 | 09121234567 |
Mobile keeps only digits, so any other separator is dropped. An integer such as 9121234567 is accepted too. It has no leading zero to lose, so it is read as the 10-digit form.
The rule#
Validity is a shape check. After cleaning, the number must be 09, then a digit from 0 to 4 or 9, then eight more digits. These are the blocks 090x to 094x and 099x. Anything else, such as 0950... or a 10-digit number, returns invalid_format.
$r = Mobile::validate('09121234567');
echo json_encode($r->details(), JSON_UNESCAPED_UNICODE);
// {"normalized":"09121234567","operator":"همراه اول","allocated":true}
print_r(Mobile::validate('09501234567')->errors()); // [invalid_format]
print_r(Mobile::validate('0912123456')->errors()); // [invalid_format]
echo Mobile::normalize('+98 912 123 4567'); // 09121234567
Operator hint#
details()['operator'] and Mobile::getOperator() look the number up in a table of well-established prefixes: Hamrah-e Aval (0910-0919, 0990-0994), Irancell (0900-0905, 0930, 0933, 0935-0939), RighTel (0920-0923), Espadan (0931), Taliya (0932), TeleKish (0934), Shatel Mobile (09981, 09982) and Aptel (09991). The longest matching prefix wins. For a valid number whose prefix is not in the table, the operator is null:
echo Mobile::getOperator('09351234567'), "\n"; // ایرانسل
echo Mobile::getOperator('09981234567'), "\n"; // شاتل موبایل
echo Mobile::getOperator('09991012345'), "\n"; // آپتل
var_dump(Mobile::validate('09951234567')->isValid()); // bool(true)
var_dump(Mobile::getOperator('09951234567')); // NULL, valid but not in the table
var_dump(Mobile::getOperator('09501234567')); // NULL, invalid number
Number portability lets people keep their number when they change operator. So a prefix is a hint about the original operator, not a promise about the current one. Blocks with no operator we can confirm (for example 0940-0949 and most of 0995-0999) return null, though the number is still valid.
The allocation flag#
Every valid result also has allocated. It is true when the prefix lies in a block that the national numbering plan lists for mobile service. The Communications Regulatory Authority filed that plan with the ITU on 24 August 2026. Mobile::isAllocated() returns the same flag as a plain bool, and is false for anything that is not a well-formed number.
var_dump(Mobile::isAllocated('09121234567')); // bool(true)
var_dump(Mobile::isAllocated('09951234567')); // bool(false), valid shape, prefix not in the plan
var_dump(Mobile::isAllocated('09501234567')); // bool(false), not a valid number
var_dump(Mobile::validate('09951234567')->details()['allocated']); // bool(false)
allocated = false on a valid result does not make the number invalid. The prefix may be newer than the plan in our data, or lie in a block such as 0906-0909 or 094x that the plan does not list as mobile. Validity is still the shape check.
Details: normalized, operator, allocated. The only error keys are invalid_format, invalid_type and input_too_long. Landline and international numbers are not accepted.
Postal code#
An Iranian postal code has 10 digits and never starts with 0. PostalCode converts Persian and Arabic digits, drops separators such as spaces and hyphens, and checks the length and the first digit:
$r = PostalCode::validate('۱۲۳۴۵-۶۷۸۹۰');
echo json_encode($r->details()); // {"normalized":"1234567890"}
print_r(PostalCode::validate('12345')->errors()); // [invalid_length]
print_r(PostalCode::validate('123456789012')->errors()); // [invalid_length]
print_r(PostalCode::validate('0123456789')->errors()); // [invalid_format]
| Error | Meaning |
|---|---|
invalid_length | The digit count is not 10 |
invalid_format | The code starts with 0 |
invalid_type, input_too_long | See Input handling |
0 is valid even if Iran Post has not assigned it. An integer such as 1234567890 is accepted because a valid code never starts with zero, but keeping postal codes as strings is still the safer habit.Vehicle plate#
Shape#
VehiclePlate understands the common passenger-car plate: two digits, one letter, three digits and a two-digit region code, written as 12ب345 ایران 67. The word «ایران», spaces, hyphens, underscores and half-spaces are ignored, so all of these mean the same plate:
echo VehiclePlate::normalize('۱۲ ب ۳۴۵ ایران ۶۷'); // 12ب34567
$r = VehiclePlate::validate('12ب345 ایران 67');
echo json_encode($r->details(), JSON_UNESCAPED_UNICODE);
// {"normalized":"12ب34567","two_digit":"12","letter":"ب","three_digit":"345","region":"67"}
print_r(VehiclePlate::parse('12ب345 ایران 67'));
// Array ( [two_digit] => 12 [letter] => ب [three_digit] => 345 [region] => 67 )
var_dump(VehiclePlate::parse('xx')); // NULL
Letters#
The letter must be one of: الف ب پ ت ث ج د ز س ش ص ط ع ف ق ک گ ل م ن و ه ی ژ. The long form الف counts as one letter. The Latin D and S are accepted for diplomatic and special plates. Arabic letter variants are converted first, so ك and ي are read as ک and ی:
echo json_encode(VehiclePlate::validate('12ي34567')->details(), JSON_UNESCAPED_UNICODE);
// {"normalized":"12ی34567","two_digit":"12","letter":"ی","three_digit":"345","region":"67"}
var_dump(VehiclePlate::isValid('12D34567')); // bool(true)
var_dump(VehiclePlate::isValid('12الف34567')); // bool(true)
Errors#
invalid_format: the shape does not match (wrong digit counts, a letter that is not allowed, a missing part).invalid_region: the shape matches, but the region is00, the two-digit part is00, or the three-digit part is000. The parsed parts are still indetails().
print_r(VehiclePlate::validate('12ب345')->errors()); // [invalid_format]
print_r(VehiclePlate::validate('12ب34500')->errors()); // [invalid_region]
print_r(VehiclePlate::validate('00ب34567')->errors()); // [invalid_region]
VehiclePlate::parse() returns the four parts as an array, or null when the plate is not valid by the same rules as validate().