Browse project documentation

National ID validation

Abzar0.8.1View sourceEnglish / Persian

Validate Iranian national IDs and inspect issuing-area lookup warnings.

Eram\Abzar\Validation\NationalId validates the Iranian national ID (کد ملی): 10 digits ending in a mod-11 check digit, whose first three digits name the issuing city.

Minimal example

Save beside vendor/ and run with PHP:

<?php
require 'vendor/autoload.php';

use Eram\Abzar\Validation\NationalId;

$id = NationalId::from('۰۰۱۳۵۴۲۴۱۹');
echo $id->value(), "\n";
echo $id->city(), "\n";
0013542419
تهران مرکزی

More examples

use Eram\Abzar\Validation\NationalId;

NationalId::validate('0013542419')->isValid();   // true
NationalId::validate('۰۰۱۳۵۴۲۴۱۹')->isValid();   // true — Persian digits accepted
NationalId::validate('001-354241-9')->isValid(); // true — dashes and spaces stripped
NationalId::validate('1234567890')->isValid();   // false — bad check digit

$id = NationalId::tryFrom('0013542419');           // NationalId or null
$id = NationalId::from('0013542419');              // NationalId, or throws ValidationException

Rules

  • Persian and Arabic digits are folded to ASCII. Whitespace (including NBSP), dashes and invisible marks (ZWNJ, bidi marks) are stripped.
  • 8 or 9 digits are rejected as LIKELY_TRUNCATED. Leading zeros are commonly lost in CSV, Excel or intval() round-trips. abzar doesn’t pad them back, because that would hide the upstream bug. str_pad($id, 10, '0', STR_PAD_LEFT) before retrying if you know the source dropped them.
  • Exactly 10 digits are required.
  • All-same digits (1111111111) and 0123456789 are rejected.
  • Digits 4–9 must not all be zero.
  • The last digit must match the mod-11 check digit of the first nine.

Warnings

A checksum-valid ID whose 3-digit prefix isn’t in the bundled city table is valid with a warning, and city / province are null:

$r = NationalId::validate('2540201288');
$r->isValid();         // true
$r->isStrictlyValid(); // false — has a warning
$r->warningCodes();    // [ErrorCode::NATIONAL_ID_UNKNOWN_CITY_CODE]

from(), tryFrom() and extractAll() accept warning-bearing IDs. Check isStrictlyValid() first if only catalogued prefixes are acceptable.

Error codes

CodeKindWhen
NATIONAL_ID.EMPTYerrorInput is empty after normalization
NATIONAL_ID.LIKELY_TRUNCATEDerror8 or 9 digits, probably with leading zeros lost
NATIONAL_ID.WRONG_LENGTHerrorDoesn’t reduce to exactly 10 digits
NATIONAL_ID.ALL_SAME_DIGITSerrorAll ten digits are the same
NATIONAL_ID.SEQUENTIAL_DIGITSerror0123456789
NATIONAL_ID.MIDDLE_ZEROSerrorDigits 4–9 are all zero
NATIONAL_ID.INVALID_CHECKSUMerrorLast digit doesn’t match the mod-11 check digit
NATIONAL_ID.UNKNOWN_CITY_CODEwarningValid, but the 3-digit prefix isn’t in the city table

ALL_SAME_DIGITS, SEQUENTIAL_DIGITS, MIDDLE_ZEROS and INVALID_CHECKSUM share the Persian message کد ملی نامعتبر است; the code tells them apart. See Error codes for every message.

Details

$id = NationalId::from('0013542419');
$id->value();        // '0013542419'
$id->cityCode();     // '001'
$id->city();         // 'تهران مرکزی'
$id->province();     // 'تهران'
$id->provinceEnum(); // Province::TEHRAN
(string) $id;        // '0013542419'
json_encode($id, JSON_UNESCAPED_UNICODE); // {"value":"0013542419","city_code":"001","city":"تهران مرکزی","province":"تهران"}

// Via ValidationResult:
$detail = NationalId::validate('0013542419')->detail();
$detail->cityCode;   // '001'

Extracting from text

extractAll() scans free text for 10-digit runs, in Persian or ASCII digits, and returns each valid ID, left to right:

NationalId::extractAll('کد ملی: ۰۰۱۳۵۴۲۴۱۹ و 1234567890'); // [NationalId('0013542419')]

A bare 10-digit run may equally be a postal code, so expect overlap when you scan mixed text with both.

Fixtures

NationalId::fake();      // random checksum-valid ID
NationalId::fake('001'); // pinned to the Tehran prefix

fake() throws ValidationException (FAKE.INVALID_ARGUMENT) when the prefix isn’t exactly three digits. The generated ID is valid by construction, but it may belong to a real person, so keep it out of production data.

Limitations and common mistakes

This checks local rules, not civil-registry identity or ownership. Store the ID as a string; casting it to an integer destroys leading zeros. The prefix names an issuing area, not a current residence.

Related: validation, error handling, error codes.

Search documentation

Search across all projects. Close this window to return to your guide.

Tab to navigate · Enter to openEsc to close