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.

Maintainers

Package info

github.com/ayoubgaouet/tn-validate

pkg:composer/ayoub-gaouet/tn-validate

Transparency log

Statistics

Installs: 8

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-08-29 03:06 UTC

This package is auto-updated.

Last update: 2026-08-29 03:12:43 UTC


README

tests Latest version PHP version License

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 / :loose on tn_phone, tn_car_plate, tn_tax_id, tn_rne, tn_passport
  • :known_bank / :any_bank on tn_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. 3099 passes because 30 is 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::BANKS covers 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 into requireKnownBank.
  • 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.