ayoub-gaouet / tn-validate
Tunisian data validation for Laravel — phone, CIN, RIB, IBAN, car plates, postal codes, tax ID, RNE, e-Dinar/CCP and passport, as native Rule objects and validator extensions.
Requires
- php: ^8.3
- illuminate/contracts: ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^11.0 || ^12.0 || ^13.0
- illuminate/validation: ^11.0 || ^12.0 || ^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^9.9 || ^10.0 || ^11.0
- phpunit/phpunit: ^11.0
README
Tunisian data validators for Laravel — phone numbers, CIN, RIB, IBAN, car plates, postal codes, tax ID (matricule fiscal), RNE, passport and e-Dinar/CCP — as native Rule objects and Validator string extensions, on top of a framework-agnostic core.
Inspired by degache-php, rebuilt Laravel-first with a wider set of validators and real checksum verification for RIB and IBAN.
Requirements
- PHP 8.3 or 8.4
- Laravel 11, 12 or 13
A note on Laravel 11. This package supports and tests against it, but every 11.x release is covered by a Composer security advisory (CVE-2026-48019) whose fix only landed in the 12.x line. If you are choosing today, use Laravel 12.60+ or 13. Installing on Laravel 11 may require relaxing Composer's advisory policy — that is a property of the framework version, not of this package.
Installation
composer require ayoub-gaouet/tn-validate
The service provider and the TnValidate facade auto-register through Laravel's package discovery. Publishing anything is optional:
php artisan vendor:publish --tag=tn-validate-config php artisan vendor:publish --tag=tn-validate-lang
At a glance
| Data | Rule object | String rule | Facade |
|---|---|---|---|
| Phone | TunisianPhone |
tn_phone |
validatePhoneNumber() |
| CIN | TunisianCin |
tn_cin |
validateCIN() |
| RIB | TunisianRib |
tn_rib |
validateRIB() |
| IBAN | TunisianIban |
tn_iban |
validateIBAN() |
| Car plate | TunisianCarPlate |
tn_car_plate |
validateCarPlate() |
| Postal code | TunisianPostalCode |
tn_postal_code |
validatePostalCode() |
| Tax ID | TunisianTaxId |
tn_tax_id |
validateTaxID() |
| RNE | TunisianRne |
tn_rne |
validateRNE() |
| Passport | TunisianPassport |
tn_passport |
validatePassport() |
| e-Dinar / CCP | TunisianEDinar |
tn_e_dinar |
validateEDinar() |
Usage
1. Rule objects
use AyoubGaouet\TnValidate\Rules\TunisianCin; use AyoubGaouet\TnValidate\Rules\TunisianPhone; use AyoubGaouet\TnValidate\Rules\TunisianRib; $request->validate([ 'phone' => ['required', new TunisianPhone], 'cin' => ['required', new TunisianCin], 'rib' => ['required', new TunisianRib(requireKnownBank: true)], ]);
Rules that have options take them as constructor arguments:
| Rule | Options |
|---|---|
TunisianPhone |
strict: bool |
TunisianRib |
requireKnownBank: bool |
TunisianIban |
requireKnownBank: bool |
TunisianCarPlate |
type: ?CarPlateType, strict: bool |
TunisianTaxId, TunisianRne, TunisianPassport |
strict: bool |
2. String rules
public function rules(): array { return [ 'phone' => ['required', 'tn_phone'], 'cin' => ['required', 'tn_cin'], 'rib' => ['required', 'tn_rib:known_bank'], 'iban' => ['required', 'tn_iban'], 'plate' => ['required', 'tn_car_plate:strict'], 'postal_code' => ['required', 'tn_postal_code'], 'tax_id' => ['required', 'tn_tax_id'], 'rne' => ['required', 'tn_rne'], 'passport' => ['required', 'tn_passport'], 'ccp' => ['required', 'tn_e_dinar'], ]; }
Parameters override the package defaults in both directions:
:strict/:looseontn_phone,tn_car_plate,tn_tax_id,tn_rne,tn_passport:known_bank/:any_bankontn_rib,tn_iban
Without a parameter, the defaults in config/tn-validate.php apply.
3. The facade
use AyoubGaouet\TnValidate\Facades\TnValidate; TnValidate::validatePhoneNumber('20123456'); // true TnValidate::formatPhoneNumber('+21620123456'); // "+216 20 123 456" TnValidate::nationalPhoneNumber('+216 20 123 456'); // "20123456" TnValidate::getPhoneCarrierInfo('90123456')->name; // "Tunisie Telecom" TnValidate::validateCIN('12345678'); // true TnValidate::validateRIB('08003000000000000026'); // true — key checked TnValidate::formatRIB('08003000000000000026'); // "08 003 0000000000000 26" TnValidate::parseRIB('08003000000000000026'); // ['bank' => '08', 'branch' => '003', ...] TnValidate::computeRIBKey('080030000000000000'); // "26" TnValidate::getBankInfoFromRIB('08003…')->name; // "Banque Internationale Arabe de Tunisie (BIAT)" TnValidate::validateIBAN('TN5908003000000000000026'); // true TnValidate::formatIBAN('TN5908003000000000000026'); // "TN59 0800 3000 0000 0000 0026" TnValidate::ibanFromRIB('08003000000000000026'); // "TN5908003000000000000026" TnValidate::ribFromIBAN('TN5908003000000000000026'); // "08003000000000000026" TnValidate::validateCarPlate('123 تونس 4567'); // true TnValidate::getCarPlateInfo('RS 123 تونس')->type; // CarPlateType::Rs TnValidate::validatePostalCode('2001'); // true TnValidate::getGovernorateFromPostalCode('3000'); // "Sfax" TnValidate::validateTaxID('1234567A/B/C/000'); // true TnValidate::validateRNE('1234567A'); // true TnValidate::validatePassport('K929139'); // true TnValidate::validateEDinar('123456'); // true TnValidate::ribFromEDinar('123456'); // "17000000000012345617" TnValidate::ibanFromEDinar('123456'); // "TN5917000000000012345617"
4. Without Laravel
Every validator also exists as a plain class under Support/ with no framework dependency, so the same logic works in a console script, a queue payload transformer, or a non-Laravel project:
use AyoubGaouet\TnValidate\Support\PhoneValidator; PhoneValidator::validate('20123456'); // true PhoneValidator::carrier('20123456'); // CarrierInfo
Support/ and Enums/ importing anything from Illuminate\ is asserted against in the test suite, so this property cannot quietly rot.
What each validator actually checks
| Validator | Rule | Confidence |
|---|---|---|
| Phone | 8 digits starting with 2, 4, 5 or 9; optional +216 / 216 / 00216 prefix. Mobile only — fixed lines (3x, 7x) and special ranges are rejected. |
verified against the published national numbering plan |
| CIN | 8 digits, first digit 0 or 1 | inferred from the format in circulation; there is no published check digit to verify |
| RIB | 20 digits — 2 bank + 3 branch + 13 account + 2-digit MOD 97-10 key, actually computed | verified: cross-checked against five independently published, checksum-valid Tunisian IBANs |
| IBAN | TN + 22 digits, the real ISO 7064 MOD 97-10 checksum, plus the domestic RIB key |
verified |
| Car plate | 123 تونس 4567 (standard) or RS 123 تونس (special series) |
inferred from the formats in circulation |
| Postal code | 4 digits with a known governorate prefix | not exhaustive — prefix-level only, see below |
| Tax ID | 7 digits + check letter, optional /CATEGORY/TYPE/ESTABLISHMENT suffix |
inferred; the check letter's algorithm is not public, so it is not verified |
| RNE | The same 7 digits + letter, without the suffix | inferred from the 2019 RNE / Matricule Fiscal merger |
| Passport | One uppercase letter + 6–7 digits, or the legacy 8-digit form | inferred; both shapes are in circulation |
| e-Dinar / CCP | 4–13 digits, expandable into a full RIB under bank code 17 | inferred — see below |
The RIB and IBAN checksums are real
The 2-digit RIB key is an ISO 7064 MOD 97-10 check on the preceding 18 digits:
key = (97 − ((first 18 digits × 100) mod 97)) mod 97
Equivalently, a valid 20-digit RIB is divisible by 97. This is also why every genuine Tunisian IBAN starts with TN59: once the domestic part is divisible by 97, the IBAN check digits are forced to 59. Both checks run, so a single mistyped or transposed digit is caught — the test suite asserts exactly that for every position of every sample.
What is not verified
- The CIN, tax ID, RNE and passport check characters. Their algorithms are not published, so those validators check shape only. A well-formed value is not proof the document exists.
- The postal-code list. Codes are validated by governorate prefix, not against the full ~680-code list.
3099passes because30is Sfax, even though no such office may exist. This avoids false negatives on legitimate but uncommon codes, at the cost of accepting unassigned numbers inside a real range. - The bank-code table.
RibValidator::BANKScovers the banks and payment institutions in common circulation, but new codes are assigned over time. An unrecognized code is reported as an unknown bank, not as an invalid RIB, unless you opt intorequireKnownBank. - CCP account-number length. La Poste does not publish one. The accepted 4–13 digit range is inferred from their own documented example (a 6-digit account) and from the 13-digit account field a RIB provides. Treat a pass as "this can be expanded into a RIB", not "this account exists".
- Carrier detection. The leading digit identifies the range holder. Number portability has existed since 2014, so a number can legitimately sit with a different operator.
Note on La Poste's documented example: their published IBAN for account 123456, TN59 1700 0000 0000 1234 5600, leaves the key as 00 and does not satisfy the checksum. ribFromEDinar() emits the real key instead, so its output round-trips through validateRIB() and validateIBAN().
Configuration
config/tn-validate.php has two options, both of which only set defaults for the tn_* string rules — Rule objects always take explicit constructor arguments:
return [ // Default 'strict' mode for tn_phone, tn_car_plate, tn_tax_id, tn_rne, tn_passport. 'strict_by_default' => false, // Default 'known bank' requirement for tn_rib and tn_iban. 'require_known_bank' => false, ];
Both are covered by tests that assert they change behaviour — an option that does nothing does not stay in this file.
Error messages
Messages ship in English, French and Arabic under resources/lang, and follow the application locale, including a setLocale() call made after boot. Override them the usual Laravel way:
$request->validate( ['cin' => ['required', 'tn_cin']], ['cin.tn_cin' => 'Numéro de CIN invalide.'], );
Testing
composer test
The full quality gate — the same one CI runs:
composer check
which covers composer validate --strict, Pint, PHPStan (level 8, via Larastan) and PHPUnit.
CI runs the suite on PHP 8.3 and 8.4 against Laravel 11, 12 and 13, on both lowest and highest resolvable dependencies.
Contributing
Issues and pull requests are welcome — especially additions to the bank-code table and to the postal-code coverage. See CONTRIBUTING.md.
New validation rules need a source. If the underlying rule is not publicly documented, say so and keep the behaviour conservative rather than inventing one.
License
MIT — see LICENSE.