Mobile, postal code and plate

Validate Iranian mobile numbers in every common input form, postal codes and vehicle plates, with operator hints, an allocation flag and parsing.

On this page
  1. Quick start
  2. Mobile numbers
    1. Accepted forms
    2. The rule
    3. Operator hint
    4. The allocation flag
  3. Postal code
  4. Vehicle plate
    1. Shape
    2. Letters
    3. Errors

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:

Inputnormalized
0912123456709121234567
912123456709121234567
98912123456709121234567
+98912123456709121234567
0098912123456709121234567
۰۹۱۲ ۱۲۳ ۴۵۶۷09121234567
(0912) 123-456709121234567

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.

Good to know. The numbering plan confirms that a prefix is a mobile allocation. It does not name the operators. Operator names come from public tables that agree with each other, and the operators' own pages for Irancell, Aptel and Shatel Mobile. Do not route SMS or billing by the operator hint. See Accuracy and data.

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]
ErrorMeaning
invalid_lengthThe digit count is not 10
invalid_formatThe code starts with 0
invalid_type, input_too_longSee Input handling
Shape only. A postal code has no check digit, and there is no region-range check. A 10-digit code that does not start with 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 is 00, the two-digit part is 00, or the three-digit part is 000. The parsed parts are still in details().
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]
Good to know. The validator checks the shape and that the numbers are not zero. It does not know which region codes exist or which letters belong to which kind of vehicle. Other plate families (motorcycles, older layouts, free-zone and government plates) are not covered. A valid result means the plate is well formed. It does not mean the plate was issued.

VehiclePlate::parse() returns the four parts as an array, or null when the plate is not valid by the same rules as validate().