Aller au contenu principal
Making DigitalMaking Digital
Note technique

Valider un IBAN et un numéro de TVA en PHP et Laravel

En bref

Une expression régulière ne suffit pas à valider un IBAN : il faut aussi vérifier sa clé, en déplaçant les quatre premiers caractères à la fin, en remplaçant les lettres par des nombres (A = 10 ... Z = 35) et en contrôlant que le reste de la division par 97 vaut 1. Un numéro de TVA français s'écrit FR, une clé de 2 caractères et le SIREN ; une clé numérique vaut (12 + 3 × (SIREN mod 97)) mod 97. Dans Laravel, ces contrôles se placent dans une classe ValidationRule ; la règle regex: exige des délimiteurs et une écriture en tableau dès que le motif contient un |.

Un formulaire d'ouverture de compte professionnel demande un IBAN pour le prélèvement et un numéro de TVA intracommunautaire pour la facture. Une expression régulière vérifie la forme, pas la clé de contrôle : un IBAN avec un chiffre faux passe sans problème. La bonne méthode tient en deux étapes : une regex pour le format, puis le calcul de la clé (modulo 97 dans les deux cas). Tout le code ci-dessous a été exécuté dans une application Laravel, avec des exemples valides et invalides.

Pourquoi l'ancienne règle ne suffisait pas ?

La version précédente de cette note proposait 'regex:^[a-zA-Z]{2}[0-9]{2}...$'. Nous l'avons testée, elle cumule trois défauts :

  • Pas de délimiteurs. La documentation de Laravel le rappelle : la règle utilise preg_match, le motif "should obey the same formatting required by preg_match and thus also include valid delimiters". Sans /.../, la validation lève une ErrorException : preg_match(): No ending delimiter '^' found.
  • s au lieu de \s dans la regex de TVA : [-_s0-9] accepte la lettre s, si bien que FRsss était accepté.
  • Aucun calcul de clé. FR1520041010050500013M02606, un IBAN d'exemple dont nous avons changé la clé, passait la regex.

Comment vérifier la clé d'un IBAN ?

L'IBAN (norme ISO 13616) commence par deux lettres de pays et deux chiffres de contrôle calculés selon la méthode "MOD 97-10" de la norme ISO 7064. Pour le vérifier, il faut déplacer les quatre premiers caractères à la fin, remplacer chaque lettre par un nombre (A = 10 ... Z = 35), et l'IBAN est valide si le reste de la division par 97 vaut 1 (méthode décrite notamment dans les textes officiels de la banque centrale de Turquie sur l'IBAN).

PHP
<?php
// app/Support/BankIds.php

namespace App\Support;

final class BankIds
{
    // Removes spaces and dashes, uppercases
    public static function normalize(string $value): string
    {
        return strtoupper(preg_replace('/[\s-]+/', '', $value));
    }

    public static function isIban(string $value): bool
    {
        $iban = self::normalize($value);

        // 2 letters, 2 check digits, then 11 to 30 letters or digits (34 max)
        if (! preg_match('/^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/', $iban)) {
            return false;
        }

        // Move the first 4 characters to the end, then A=10 ... Z=35
        $rearranged = substr($iban, 4).substr($iban, 0, 4);
        $digits = '';
        foreach (str_split($rearranged) as $char) {
            $digits .= ctype_alpha($char) ? (string) (ord($char) - 55) : $char;
        }

        // Modulo 97 computed by chunks to stay within PHP integers
        $remainder = 0;
        foreach (str_split($digits, 7) as $chunk) {
            $remainder = (int) ($remainder.$chunk) % 97;
        }

        return $remainder === 1;
    }

    public static function isFrenchVat(string $value): bool
    {
        $vat = self::normalize($value);

        // FR + 2-character key + 9-digit SIREN
        if (! preg_match('/^FR([0-9A-Z]{2})([0-9]{9})$/', $vat, $m)) {
            return false;
        }

        [, $key, $siren] = $m;

        // Numeric key: (12 + 3 x (SIREN mod 97)) mod 97
        if (ctype_digit($key)) {
            return (int) $key === (12 + 3 * ((int) $siren % 97)) % 97;
        }

        // Key with letters: format only
        return true;
    }
}

Le nombre obtenu dépasse largement la capacité d'un entier PHP : le calcul se fait donc par tranches de 7 chiffres, en reportant le reste. Résultats : FR1420041010050500013M02606, DE89370400440532013000, GB29NWBK60161331926819 et BE68539007547034 (IBAN d'exemple, pas de vrais comptes) sont acceptés, y compris saisis avec des espaces, des tirets ou en minuscules. Une clé modifiée, un dernier chiffre changé ou un caractère en moins sont refusés.

Pour aller plus loin, on peut contrôler la longueur propre à chaque pays (27 caractères pour un IBAN français), mais le calcul de la clé détecte déjà les fautes de frappe.

Comment contrôler un numéro de TVA français ?

Le numéro français s'écrit FR, une clé de 2 caractères, puis les 9 chiffres du SIREN. Quand la clé est numérique, elle se calcule ainsi : (12 + 3 × (SIREN modulo 97)) modulo 97. Pour le SIREN fictif 123456789, la fonction ci-dessus accepte FR32123456789 et refuse FR23123456789. Une clé contenant des lettres existe aussi : elle n'est contrôlée que sur la forme.

Format valide ne veut pas dire numéro attribué

Ce calcul détecte une faute de saisie, pas un numéro inventé. Nous n'avons pas trouvé la formule de la clé dans une publication de l'administration fiscale ; elle est reprise partout mais reste à prendre comme un contrôle de forme. Pour savoir si un numéro est réellement attribué à une entreprise, le service officiel est VIES, de la Commission européenne.

Comment brancher ces contrôles dans la validation Laravel ?

Une classe de règle (générée avec php artisan make:rule Iban) pour l'IBAN, réutilisable partout :

PHP
<?php
// app/Rules/Iban.php

namespace App\Rules;

use App\Support\BankIds;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class Iban implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (! is_string($value) || ! BankIds::isIban($value)) {
            $fail('Le champ :attribute doit contenir un IBAN valide.');
        }
    }
}

Et une fonction anonyme pour la TVA, si elle ne sert qu'à un endroit :

PHP
<?php
// app/Http/Controllers/AccountController.php (extrait)

use App\Rules\Iban;
use App\Support\BankIds;
use Closure;
use Illuminate\Support\Facades\Validator;

$validator = Validator::make($request->all(), [
    'iban' => ['required', 'string', new Iban],
    'tva' => ['nullable', 'string', function (string $attribute, mixed $value, Closure $fail) {
        if (! BankIds::isFrenchVat($value)) {
            $fail('Le numéro de TVA intracommunautaire n\'est pas valide.');
        }
    }],
]);

Résultats : FR14 2004 1010 0505 0001 3M02 606 et FR 32 123 456 789 passent ; une clé fausse donne les deux messages ; une TVA vide passe, car Laravel n'exécute pas une règle personnalisée sur un champ vide (sauf règle "implicit") ; un IBAN vide est arrêté par required. Pensez à enregistrer la valeur normalisée (BankIds::normalize()), pas la saisie brute.

Et si l'on garde une simple règle regex ?

Pour un contrôle de forme seul, mettez les délimiteurs et passez par un tableau ou par Rule::regex() :

PHP
<?php
// Format only, no check digits
use Illuminate\Validation\Rule;

'iban' => ['required', 'regex:/^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/'],
'iban_bis' => [Rule::regex('/^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/')],

N'utilisez pas la syntaxe avec des barres verticales dès que la regex contient un | : 'required|regex:/^(FR|BE)[0-9]{2}/' est coupé au milieu du motif et lève preg_match(): No ending delimiter '/' found. La même règle écrite en tableau fonctionne.

Ces contrôles sont typiques des formulaires d'une application sur mesure ou des données envoyées à un logiciel de gestion, où une saisie erronée se retrouve ensuite partout (connecter vos logiciels). Autre cas de validation de formulaire : pluck() pour remplir un select.

À retenir
  • Une regex vérifie la forme ; la clé d'un IBAN se vérifie par le modulo 97 (reste égal à 1).
  • TVA française : FR + clé + SIREN, clé numérique = (12 + 3 × (SIREN mod 97)) mod 97 ; VIES pour savoir si le numéro est attribué.
  • Dans Laravel, regex: exige des délimiteurs ; dès qu'un | est dans le motif, écrire les règles en tableau.
  • Une classe ValidationRule garde le contrôle réutilisable et testable.

Testé avec Laravel 13.37.0 et PHP 8.3.6.

Questions fréquentes

Faut-il un package pour valider un IBAN en PHP ?

Non pour la clé de contrôle : une vingtaine de lignes suffisent, sans extension particulière, en calculant le modulo 97 par tranches de chiffres. Un package devient utile si vous voulez contrôler la longueur et la structure propres à chaque pays.

Un IBAN qui passe le contrôle existe-t-il forcément ?

Non. La clé détecte les fautes de frappe, pas les comptes inexistants ou clôturés. Seule la banque peut confirmer le compte, par exemple lors du premier prélèvement.

Comment savoir si un numéro de TVA intracommunautaire est réellement attribué ?

En l'interrogeant sur VIES, le service de la Commission européenne qui indique si un numéro est valide. Le calcul de la clé ne vérifie que la cohérence entre la clé et le SIREN.

Pourquoi ma règle regex lève une ErrorException ?

Le plus souvent, le motif n'a pas de délimiteurs (/.../) ou contient un | dans une règle écrite avec des barres verticales, ce qui coupe le motif. Écrivez les règles en tableau, ou utilisez Rule::regex().

Making Digital

Nous connectons les logiciels des PME, automatisons leurs tâches répétitives et créons des assistants IA, depuis 2010. Nos articles partent de nos projets réels.

Notre parcours
Une question sur ce sujet ?

Parlons-en 30 minutes.

Vous saurez si votre problème se règle, comment, et en combien de temps. Sans engagement.

Un échange de 30 minutes, en visio ou par téléphone

Gratuit, sans engagement. On parle de votre problème, pas de technique.