La gestion d’un RIB ou d’un IBAN en PHP

On va se sortir un peu la tête du dossier Word et docx le temps d’un petit article pour discuter d’un grand classique, la gestion d’un RIB, d’un IBAN, bref d’un compte en banque, en PHP. On va tout de suite dire de quoi il ne s’agit pas : il ne s’agit pas d’un bête CRUD dans lequel on enregistre les différents champs et on n’en fait rien, pas besoin d’un article pour ça. Non, ce qu’on va faire, c’est enregistrer les informations RIB ou IBAN, en validant ces infos dans la foulée (grâce à la célèbre clé RIB) et en nous conformant aux spécifications liées à ce format très standard et très normalisé (donc très courant) d’information. Au final, on aura un petit package facile à emporter avec nous sur d’autres projets.

Qu’est-ce qu’un RIB ? Qu’est-ce qu’un IBAN ? (tiens, et un Swift, un BIC…)

Je sais, bande d’impatients, que vous souhaitez vous lancer directement dans la rédaction du code; parmi vous, certains n’auront même pas lu une ligne de texte et se jetteront directement sur la partie « code » de ce document. N’oubliez pas que l’important, pour un architecte, c’est son plan : il peut le modifier, le faire évoluer, mais son plan reste toujours son document de référence lorsqu’il construit. Il nous faut donc d’abord concevoir proprement notre modèle et nous préparer à ce qui nous attend.

Comprendre le RIB

Peut-être que vous ne connaissez pas l’IBAN, le Swift ou le BIC, mais si vous avez un compte en banque, vous savez ce qu’est un RIB (Relevé d’Identité Bancaire); c’est un ensemble d’informations permettant d’identifier votre compte en banque. On peut d’ores et déjà dire que le RIB est une sous-partie de l’IBAN, qu’on verra ensuite.

Une fois qu’on a dit ça, ce qui nous intéresse vraiment, c’est le format d’un RIB, qui est composé d’une partie permettant d’identifier la banque et d’une partie permettant d’identifier le compte bancaire, au sein de la banque. Chaque pays définit le format de son RIB (en cherchant sur le net, vous trouverez plus de résultats en le nommant BBAN — Basic Bank Account Number — plutôt que RIB, qui est la nomenclature française).

Cependant, un certain nombre de règles est commun à tous les pays, puisque tous les pays respectent la norme IBAN (International Bank Account Number); ces règles sont les suivantes :

  • le RIB (ou BBAN) ne fait pas plus de 30 caractères (et pas moins de 11);
  • la partie identifiant la banque fait entre 3 et 12 caractères;
  • la partie identifiant le compte fait entre 8 et 20 caractères.
Au niveau électronique, on écrit un BBAN sans aucun espace; le formatage « papier », pour une meilleure lecture, est standardisé pays par pays (en France, on sépare ainsi les code banque, code agence, numéro de compte et clé).
En France, la partie bancaire fait 10 caractères et la partie compte 11 + 2 (la clé).

Comprendre l’IBAN

De son nom chiant ISO 13616 (et en anglais International Bank Account Number), l’IBAN est donc une norme internationale; elle permet d’homogénéiser l’identification d’un compte bancaire partout dans le monde, notamment pour faciliter les transferts d’argent entre des comptes de pays différents.
La composition d’un IBAN est à peine plus complexe que celle d’un BBAN (ou RIB, pour la France); pour le coup, les gens qui ont mis le système en place ont bien bossé :
  • un code pays sur deux lettres (FR pour la France);
  • une clé de contrôle sur deux chiffres;
  • l’intégralité du BBAN.
Au niveau des règles, on peut déduire de ce qui précède qu’un IBAN fait toujours entre 15 et 34 caractères; au format électronique, on l’écrit sans aucun espace tandis qu’au format « papier », le standard veut qu’on l’écrive en séparant les caractères par groupe de 4 (lorsque le nombre de caractères n’est pas un multiple de 4, c’est le dernier groupe de caractères qui contient moins de 4 signes).
Vous trouverez sur cette page de nombreux exemples, par pays, de la composition d’un IBAN.

Comprendre le BIC

En Europe, les établissements bancaires demandent à ce qu’un code particulier, le BIC, soit joint à l’IBAN. Ce code BIC (Bank Identifier Code) permet, comme son nom l’indique, d’identifier une banque de façon unique (by the way, son nom chiant, c’est ISO 9362).
Immédiatement, vous allez me demander pourquoi avoir un BIC pour identifier une banque, puisqu’on identifie déjà une banque dans l’IBAN (puisqu’on le fait dans le BBAN); techniquement, vous avez parfaitement raison. Historiquement, le BIC a été conçu pour pouvoir faire des transferts d’argent entre pays différents sans que le coût, pour le titulaire du compte, ne dépasse celui d’un transfert national. Ça ne répond pas vraiment à la question d’un point de vue technique, mais j’imagine que, lors de la mise en place, les banques utilisaient des systèmes différents d’un pays à un autre, voire même d’une banque à une autre, et qu’ils ont dû considérer que c’était plus propre ainsi (et quand je dis « ils », je parle de l’Organisation Internationale de Normalisation (ISO) et du European Committee for Banking Standards (ECBS); en effet, avec la libre circulation des capitaux en Europe, il était nécessaire de pouvoir transférer de l’argent en Europe comme au sein d’un même pays, ce qui explique l’intérêt particulier de l’Union Européenne à ce standard).
À noter que l’Europe n’est pas la seule à utiliser le BIC; on le trouve aussi en Tunisie, par exemple.
La structure d’un BIC, comme pour n’importe quel standard, est parfaitement normalisé :
  • il fait entre 8 et 11 caractères;
  • d’abord le code banque, sur quatre lettres;
  • ensuite le code pays, sur deux lettres (les mêmes lettres que dans l’IBAN, c’est normal, c’est le code pays de la norme ISO 3166);
  • ensuite le code emplacement qui va permettre de localiser la banque au niveau d’une ville ou d’une province, sur deux caractères alpha (chiffres et/ou lettres);
  • enfin, et de manière optionnelle, le code branche, sur trois caractères alpha,  permettant de définir l’agence de la banque (‘XXX’ pour le sièges central, ‘LYO’ pour une agence à Lyon, ‘SXB’ pour Strasbourg, etc.).
Ainsi, lorsque le code ne contient que 8 caractères, il s’agit du siège national d’une banque.

Comprendre le Swift

Parfois on vous parle du code Swift plutôt que du code BIC, c’est en fait plus un écart de langage qu’autre chose; le Swift est l’organisation qui attribue les codes BIC (SWIFT pour Society for Worldwide Interbank Financial Telecommunication).

Préparation de notre package

Nous voulons pouvoir gérer des comptes bancaires, dans nos différents logiciels, non seulement en enregistrant toutes les informations dont on vient de parler, mais également en tenant compte des formats et en validant la saisie (comme nous l’allons voir par la suite, les RIB et IBAN contiennent chacun une clé de contrôle permettant de garantir une saisie correcte).

Ceci étant, alors que nous étions plutôt partis pour enregistrer, notamment, des RIB, on se rend compte que, pour pouvoir réutiliser nos classes, il est plus pertinent d’enregistrer des BBAN et de limiter la notion de RIB aux seuls BBAN français.

Modèle de données

A priori, de façon assez simple, notre modèle de données contiendra les informations suivantes :

  • IBAN, sur 34 caractères;
  • RIB, sur 30 caractères;
  • BIC, sur 11 caractères.
Je passe sur les champs annexes qu’on peut ajouter, tel un ID ou une date de création et une date de modification, cela ne nous intéresse pas dans le cadre de cet article.
Mais si on veut faire les choses proprement, il est préférable d’enregistrer les données distinctement, selon leur sens; ceci pour être capable de distinguer simplement les informations qui signifient quelque chose tout en gardant la possibilité de, très simplement, recomposer l’IBAN, ou le BBAN. On garantit ainsi également qu’on ne duplique pas les informations (le RIB étant dans l’IBAN). On arrive alors sur une table qui contiendrait les informations suivantes :
  • iban_code_pays, sur 2 caractères;
  • iban_cle_controle, sur 2 caractères;
  • bban_banque, sur 12 caractères;
  • bban_compte, sur 20 caractères;
  • bic, sur 11 caractères.
C’est mieux, c’est plus propre, mais ça pose un problème pour ce qui concerne, par exemple, le RIB : en effet, la partie « banque » d’un RIB contient deux jeux d’informations qu’on souhaite pouvoir distinguer; mais, par ailleurs, ces jeux d’information ne sont pas communs à tous les pays.
À ce stade, tout dépend de ce que vous attendez de votre package :
  • vous pouvez créer une table par type de BBAN que vous liez, par clé étrangère, à la table des IBAN;
  • vous pouvez créer une table par type de BBAN dans laquelle vous mettrez également les données d’IBAN;
  • vous pouvez même utiliser une arbre partant du pays de l’IBAN, puis la banque, l’agence, et enfin le numéro de compte.
Comme nous souhaitons que notre package soit le plus réutilisable possible mais que, par ailleurs, nous souhaitons conserver proprement les séparations du modèle MVC, nous allons nous reposer sur l’autre pendant de la partie Model du MVC qu’est la Classe de type CRUD (en Java, un Bean), pour opérer cette distinction (qui sera donc logique et non pas physique).
Je ne prétends pas que c’est la bonne solution, ni même la meilleure solution; je prétends simplement que c’est la meilleure solution pour notre cas générique à laquelle j’ai pensé. La beauté de l’architecture réside notamment dans le fait que deux personnes pourront résoudre le même problème de façons différentes, uniques, innovantes, sans pour autant commettre d’erreur.

Modèle objet

On va être amené à créer les classes suivantes :

  • Iban;
  • BBan;
  • Rib, qui étend la classe BBan.
Pour des raisons de simplicité, on intègrera le code BIC comme propriété de la classe Iban; on pourrait également créer une classe BIC, mais comme la réalité d’un BIC n’est jamais vraiment disjointe d’un IBAN, on ne le fera pas; ceci étant, si vous souhaitez que votre application puisse retrouver les informations d’une banque à partir du code BIC (avoir une liste de code BIC, par exemple, peut s’avérer tout à fait judicieux selon les besoins de votre application, tout comme lister les communes de France par code postal peut l’être), il conviendrait effectivement de séparer le BIC dans une table à part de votre base, et dans une classe à part de votre modèle.

Construction

Bon, on a, semble-t-il, une bonne base de travail, alors commençons la construction de notre package; on va, comme c’est la tradition sur ce blog, partir du principe qu’on travaille avec MySQL pour le SGBD et PHP (>5.2) pour le langage de programmation.

Création des tables

CREATE TABLE `iban` (
  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
  `titulaire` varchar(200) DEFAULT NULL,
  `code_pays` smallint(2) DEFAULT NULL,
  `code_controle` varchar(2) DEFAULT NULL,
  `bban_banque` varchar(12) DEFAULT NULL,
  `bban_compte` varchar(20) DEFAULT NULL,
  `bic` varchar(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
);

On a simplement ajouté un champ « titulaire » dans lequel on va inscrire le nom du titulaire du compte en banque; dans l’absolu, cette donnée n’est pas nécessaire puisque, dans une architecture complète, l’IBAN serait lié à des données d’une personne, physique ou morale, mais simplement dans le cadre de notre exercice, on l’ajoute.

Par ailleurs, vous noterez que j’ai mis le code_controle comme étant un smallint sur deux chiffres, et non pas un varchar sur deux caractères, ainsi que j’aurais pu le suggérer suite à cet article; en effet, on aurait pu le faire, mais comme on va, pour le coup, manipuler la valeur de cette clé comme un nombre (lorsqu’on va valider le numéro d’IBAN), je préfère l’avoir comme un nombre, quitte à devoir rajouter un 0 de formatage lorsqu’elle est inférieure à 10. On aurait pu, à l’inverse, utiliser un varchar (et donc enregistrer le 0 pour ce cas), mais cela signifiait traiter le varchar comme un int lorsqu’on valide le numéro d’IBAN; on peut le faire (surtout en PHP où il ‘n y a même pas besoin de caster) mais, à choisir, je préfère en cas de doute coller au mieux à la réalité.

Enfin, vous aurez constaté que, en dehors de l’id (évidemment), tous les champs sont potentiellement NULL; à ce stade, on ne sait pas vraiment ce qu’on souhaite garantir comme information minimum; on peut éventuellement partir du principe qu’un enregistrement contiendra au moins le BBAN et rendre ses deux champs NOT NULL, c’est un choix pertinent, et vous pouvez y aller de votre côté si vous le souhaitez. De la même manière, si on permet un enregistrement de BBAN seul, il est idiot d’appeler la table iban, il vaudrait mieux l’appeler par un nom plus concordant, comme information_compte ou bien compte_banque, par exemple. Comme vous le sentez.

Création des classes

On va commencer par créer nos classes simplement, uniquement la partie CRUD. On doit donc créer les classes suivantes :

  • Iban;
  • Bban;
  • Rib (qui hérite de Bban).
Voici le diagramme de classes correspondant (wah…hyper complexe et précis, comme article, il faut que je twitte à tous mes amis, ce blog est d’enfer…)
diagramme de classes du projet IBANPour faire ça bien, on va créer, dans notre projet, un répertoire classes/ dans lequel on va enregistrer nos classes :
Iban.class.php
<?
/**
 * Iban
 *
 * @package    leMondeDuDev
 * @subpackage iban
 * @author     Bruce Benamran © LeMondeDuDev.com — 2011
 */
class Iban {
	private $id;
	private $titulaire;
	private $codePays;
	private $codeControle;
	private $bban;
	private $bic;

	public function Iban($theId = null) {
		if ($theId > 0) {
			$this->setId($theId);
			$this->load();
		}
	}

	public function load() {
		// Initialisation
	}

	public function save() {
		if ($this->isValid()) {
			if ($this->getId())
				return $this->update();
			return $this->create();
		}
		return false;
	}

	protected function create() {
		// On insère en base de données
	}

	protected function update() {
		// On met à jour en base de données
	}

	protected function isValid() {
		// On valide ce qu'on doit valider
	}

	public function delete() {
		// Suppression de l'enregistrement et destruction d'instance
	}

	public function getId() { return $this->id; }
	public function setId($value) { $this->id = $value; }

	public function getTitulaire() { return $this->titulaire; }
	public function setTitulaire($value) { $this->titulaire = $value; }

	public function getCodePays() { return $this->codePays; }
	public function setCodePays($value) { $this->codePays = $value; }

	public function getCodeControle() { return $this->codeControle; }
	public function setCodeControle($value) { $this->codeControle = $value; }

	public function getBban() { return $this->bban; }
	public function setBban($value) { $this->bban = $value; }

	public function getBic() { return $this->Bic; }
	public function setBic($value) { $this->Bic = $value; }
}
?>

Pour le contenu des fonctions de chargement, d’enregistrement et de suppression, on va d’abord s’assurer les services d’une classe toute simple que j’ai écrite il y a quelques années déjà, DbUtil.class.php, qui permet la connexion à une DB et l’exécution de requêtes. Dans l’absolu, on pourrait penser à faire du PDO ou du Doctrine, mais c’est trop hors sujet, je préfère rester simple sur tout ce qui est annexe.

DbUtil.class.php
<?
/**
 * DbUtil - Utilitaire SGBD MySQL
 *
 * @package    leMondeDuDev
 * @subpackage iban
 * @author     Bruce Benamran © LeMondeDuDev.com — 2011
 */
class DbUtil {
    private $mysql;
    private $resultSet;
    private $host='localhost';
    private $dbName='test_iban';
    private $dbUser='leUser';
    private $dbPwd='lePwd';

    public function __construct() {
        $this->open();
    }

    public function open() {
        $this->mysql = new mysql($this->host,
                                 $this->dbUser,
                                 $this->dbPwd,
                                 $this->dbName);
    }

    public function close() {
        if ($this->isConnected) {
            if ($this->resultSet instanceof mysql_result)
                $this->resultSet->close();
            $this->mysql->close();
        }
    }

    /**
     * À utiliser pour les SELECT, renvoie un resultSet
     */
    public function execute($sql) {
        $this->resultSet = $this->mysql->query($sql);
        return $this->resultSet;
    }

    public function fetchRow() {
        if ($this->resultSet)
            return $this->resultSet->fetch_assoc();
    }

    /**
     * À utiliser pour les INSERT, UPDATE et DELETE, renvoie un booléen
     */
    public function execUpdate($sql) {
        return $this->mysql->query($sql);
    }

    /**
     * Dans le cas d'un INSERT, renvoie le dernier ID ajouté
     */
    public function getLastInsertId() {
        return $this->mysql->insert_id;
    }

    /**
     * Gère les problèmes d'apostrophes dans des valeurs
     */
    public static function escapeQuotes($text) {
        $text = ereg_replace("\\'","'",$text);
        return ereg_replace("'","''",$text);
    }

    public function getResultSet() { return $this->resultSet; }
    public function setResultSet($value) { $this->resultSet = $value; }
}
?>

Je vous informe immédiatement qu’il est inutile de commenter cette classe, elle a évidemment un certain nombre de défauts; ceci étant, elle remplira bien son office le temps de notre exercice.

Maintenant, on peut compléter notre classe Iban

Iban.class.php (version 2)
<?
/**
 * Iban
 *
 * @package    leMondeDuDev
 * @subpackage iban
 * @author     Bruce Benamran © LeMondeDuDev.com — 2011
 */
class Iban {
	private $id;
	private $titulaire;
	private $codePays;
	private $codeControle;
	private $bban;
	private $bic;

	public function Iban($theId = null) {
		if ($theId > 0) {
			$this->setId($theId);
			$this->load();
		}
	}

	public function load() {
	    if ($this->getId() > 0) {
	        $sql = "SELECT titulaire, code_pays, code_controle, bic "
	             . "FROM iban WHERE id = " . $this->getId();
	        $db = new DbUtil();
	        $db->execute($sql);
	        if ($row = $db->fetchRow()) {
	            $this->setTitulaire($row["titulaire"]);
	            $this->setCodePays($row["code_pays"]);
	            $this->setCodeControle($row["code_controle"]);
	            $this->setBic($row["bic"]);

	            // Reste à charger le BBan
	            $this->setBban(new Bban($this->getId()));
	        }
	    }
	}

	public function save() {
		if ($this->isValid()) {
			if ($this->getId())
				return $this->update();
			return $this->create();
		}
		return false;
	}

	protected function create() {
		// On insère en base de données, en commençant par le Bban (potentiellement, la seule donnée NOT NULL)
		if ($this->getBban()->save()) {
    		$sql = "UPDATE iban SET "
	             . "titulaire = '" . self::escapeQuotes($this->getTitulaire()) . "',"
	             . "code_pays = '" . $this->getCodePays() . "',"
	             . "code_controle = '" . $this->getCodeControle() . "',"
	             . "bic = '" . $this->getBic() . "' "
	             . "WHERE id = " . $this->getBban()->getId();
	        $db = new DbUtil();
	        return $db->execUpdate($sql);
		}
		return false;
	}

	protected function update() {
		// On met à jour en base de données, en commençant par le Bban (pour faire comme pour le create())
		if ($this->getBban()->save()) {
    		$sql = "UPDATE iban SET "
	             . "titulaire = '" . self::escapeQuotes($this->getTitulaire()) . "',"
	             . "code_pays = '" . $this->getCodePays() . "',"
	             . "code_controle = '" . $this->getCodeControle() . "',"
	             . "bic = '" . $this->getBic() . "' "
	             . "WHERE id = " . $this->getBban()->getId();
	        $db = new DbUtil();
	        return $db->execUpdate($sql);
		}
		return false;
	}

	protected function isValid() {
		// On valide ce qu'on doit valider : on part d'un a priori true qu'on va tester
	    $res = true;

		// On va utiliser des expressions régulières sur le contenu
		// le code pays doit contenir deux lettres ou rien
		$res = $res && (is_null($this->getCodePays()) || preg_match("/^[a-zA-Z]{2}$/",$this->getCodePays()));

		// le code contrôle doit contenir jusqu'à deux chiffres ou rien
		$res = $res && (is_null($this->getCodeControle()) || preg_match("/^[0-9]{1,2}$/",$this->getCodeControle()));

		// le code bic doit contenir entre 8 et 11 caractères alphanumériques composés de 6 lettres, puis de 2 ou 5 alphanumériques
		$res = $res && (is_null($this->getCodePays()) || preg_match("/^[a-zA-Z]{6}[0-9a-zA-Z]{2}([0-9a-zA-Z]{3})?$/",$this->getCodePays()));

		return $res;
	}

	public function delete() {
		// Suppression de l'enregistrement et destruction d'instance
		if ($this->getId() > 0) {
		    $sql = "DELETE FROM iban WHERE id = " . $this->getId();
		    $db = new DbUtil();
		    return $db->execUpdate($sql);
		}
		return false;
	}

	public function getId() { return $this->id; }
	public function setId($value) { $this->id = $value; }

	public function getTitulaire() { return $this->titulaire; }
	public function setTitulaire($value) { $this->titulaire = $value; }

	public function getCodePays() { return $this->codePays; }
	public function setCodePays($value) { $this->codePays = $value; }

	public function getCodeControle() { return $this->codeControle; }
	public function setCodeControle($value) { $this->codeControle = $value; }

	public function getBban() { return $this->bban; }
	public function setBban($value) { $this->bban = $value; }

	public function getBic() { return $this->Bic; }
	public function setBic($value) { $this->Bic = $value; }
}
?>

On n’y est pas encore complètement, mais on avance. On va simplement détailler deux/trois points.

Tout d’abord, pourquoi mettre un UPDATE dans la fonction create() ? Dès lors qu’on utilise la même table pour IBAN et BBAN, on crée le BBAN avant de créer l’IBAN; du coup, lorsqu’on crée l’IBAN, la ligne existe déjà, il s’agit donc d’une mise à jour. Dans ce cas, pourquoi avoir les deux fonction update() et create() alors qu’elles sont identiques ? Pour respecter la nomenclature CRUD (notamment pour permettre à un tiers d’appeler update() sans se poser de question), il est nécessaire de conserver les deux fonctions; en revanche, dès lors que leur contenu est identique, rien n’empêche de factoriser de la façon suivante :

	protected function create() {
	    return $this->update();
	}

Point suivant : on utilise l’id de l’Iban pour charger le Bban; cette classe n’existe peut-être pas encore, mais on sait, puisqu’il s’agit de la même table, qu’on aura le même id; par ailleurs, comme on est de toute façon en train de faire un SELECT dans la table en question, alors pourquoi ne pas récupérer directement les infos ? C’est vrai, on peut le faire, ce serait même plus efficace en terme de temps de réponse (une seule requête au lieu de deux quasi identiques), mais alors la classe Iban se chargerait de construire une instance de Bban, ce qui n’est pas terrible. Il vaut mieux bien séparer les choses distinctes, quitte à les mutualiser par la suite si besoin est. Quant à la classe Bban, on va la créer très bientôt.

Point suivant : what up les expressions régulières ? Ce n’est pas parce qu’on veut travailler simplement qu’on va se priver d’écrire les choses proprement…Juste pour les néophytes, sans redéfinir ce que sont les expressions régulières (Google est ton ami, jeune Padawan…mais dans un autre onglet, s’il te plaît), on va simplement dire (rappeler ?) que :

  • dans les fonction preg en PHP, il convient de délimiter les expressions régulières par un caractère quel qu’il soit (sauf backslash ou un espace); traditionnellement, j’utilise le slash (recommandé, en général, mais si vous voulez utiliser le Y ou le ?, vous pouvez y aller…);
  • pour définir un caractère possible, on le note entre crochets, on peut indiquer une liste de caractères possibles en les listant les uns à la suite des autres (par ex: [abcde] pour l’une des cinq premières lettres de l’alphabet) ou bien donner un range( [a-z] pour une lettre de a à z en minuscule); du coup :
    • pour un alpha sans distinction de casse : [a-zA-Z];
    • pour un chiffre : [0-9];
    • pour un alphanumérique : [a-zA-Z0-9] (identique à [0-9A-Za-z], pour info).
  • À noter que, dans le point précédent, on ne pose pas la question des caractères tels ç,ù,é,œ,etc.
  • pour indiquer une répétition n fois d’un caractère parmi une liste, on fait suivre immédiatement la liste par {n} (par exemple : [a-z]{3} pour trois lettres en minuscules);
  • de la même manière, pour indiquer qu’un caractère parmi une liste est répété entre m et n fois, on fait suivre la liste par {m,n} (par exemple : [a-z]{1,2} pour 1 à 2 lettres en minuscules);
  • pour indiquer une répétition au plus 1 fois, on peut utiliser la notation {0,1} ou aussi la notation équivalente ?;
  • on peut utiliser les parenthèses pour grouper des conditions (par ex: ([0-9]{2}){0,3} signifie entre 0 et 3 fois un groupe de 2 chiffres);
  • lorsqu’on veut indiquer que notre expression débute le mot, on met un ^ au début ( par ex : ^[abc] signifie « commence par a,b ou c »);
  • de la même manière, lorsqu’on veut indiquer la fin du mot, on met un $ à la fin ( par ex: [es]$ signifie « termine par e ou s »).
Du coup, nous voici paré pour nos tests sur le contenu :
  • le code pays contient deux lettres, et uniquement ces deux lettres : ^[a-zA-Z]{2}$  (on décide de ne pas distinguer majuscules et minuscules);
  • le code clé de contrôle contient deux chiffres (qu’on enregistre comme un nombre entre 0 et 99, donc potentiellement un seul chiffre) : ^[0-9]{1,2}$ ;
  • le BIC est plus complexe : 4 lettres, puis 2 lettres (soit 6 lettres, jusque là), puis 2 alphanumériques,puis peut-être 3 alphanumériques : Expression régulière pour le code BIC
Bban.class.php
<?
/**
 * Bban
 *
 * @package    leMondeDuDev
 * @subpackage iban
 * @author     Bruce Benamran © LeMondeDuDev.com — 2011
 */
class Bban {
	private $id;
	private $banque;
	private $compte;

	public function Bban($theId = null) {
		if ($theId > 0) {
			$this->setId($theId);
			$this->load();
		}
	}

	public function load() {
	    if ($this->getId() > 0) {
	        $sql = "SELECT bban_banque, bban_compte "
	             . "FROM iban WHERE id = " . $this->getId();
	        $db = new DbUtil();
	        $db->execute($sql);
	        if ($row = $db->fetchRow()) {
	            $this->setBanque($row["bban_banque"]);
	            $this->setCompte($row["bban_compte"]);
	        }
	    }
	}

	public function save() {
		if ($this->isValid()) {
			if ($this->getId())
				return $this->update();
			return $this->create();
		}
		return false;
	}

	protected function create() {
	    // On insère le Bban
	    $sql = "INSERT INTO iban(bban_banque,bban_compte) VALUES ("
	         . "'" . $this->getBanque() . "',"
	         . "'" . $this->getCompte() . "')";
	    $db = new DbUtil();
	    $res = $db->execUpdate($sql);
	    if ($res)
	        $this->setId($db->getLastInsertId());
	    return $res;
	}

	protected function update() {
		// On met à jour en base de données
		$sql = "UPDATE iban SET "
             . "bban_banque = '" . $this->getBanque() . "',"
             . "bban_compte = '" . $this->getCompte() . "' "
             . "WHERE id = " . $this->getId();
        $db = new DbUtil();
        return $db->execUpdate($sql);
	}

	protected function isValid() {
		// On valide ce qu'on doit valider : on part d'un a priori true qu'on va tester
	    $res = true;

		// On va utiliser des expressions régulières sur le contenu
		// le code banque doit être renseigné et contenir entre 3 et 12 caractères
		$res = $res && (!is_null($this->getBanque()) && preg_match("/^[0-9a-zA-Z]{3,12}$/",$this->getBanque()));

		// le numéro de compte doit être renseigné et contenir entre 8 et 20 caractères
		$res = $res && (!is_null($this->getCompte()) && preg_match("/^[0-9a-zA-Z]{8,20}$/",$this->getCompte()));

		return $res;
	}

	public function delete() {
		// Suppression de l'enregistrement et destruction d'instance
		if ($this->getId() > 0) {
		    $sql = "DELETE FROM iban WHERE id = " . $this->getId();
		    $db = new DbUtil();
		    return $db->execUpdate($sql);
		}
		return false;
	}

	public function getId() { return $this->id; }
	public function setId($value) { $this->id = $value; }

    public function getBanque() { return $this->banque; }
    public function setBanque($value) { $this->banque = $value; }

    public function getCompte() { return $this->compte; }
    public function setCompte($value) { $this->compte = $value; }
}
?>

Juste histoire de, on est parti du principe que les champs du BBAN (banque et compte) doivent être renseignés (d’où le !is_null($this->getBanque()) suivi du test &&); ce n’est pas conforme à ce qu’on a mis dans notre table SQL, mais bon, c’est pour l’exemple, et vous corrigerez de vous-même l’un ou l’autre élément pour qu’ils concordent selon vos besoins.

En-dehors de cela, rien de particulier dans cette classe.

Rib.class.php

Maintenant, concernant la classe Rib, il va falloir être un peu attentif sur tout ce qui concerne les intéractions avec la base de données; pour simplifier au max le travail de cette classe (qui, je le rappelle, étend la classe Bban), on va faire ce qu’il faut pour que les fonctions concernant la base de données (les fonctions de CRUD) soient absolument inchangées; on va en fait simplement considérer que le Rib est une présentation particulière d’un Bban. La seule chose qu’il va nous falloir, c’est un mapping permettant de lier les codes banque et compte du BBAN aux codes correspondant du RIB, ce qu’on va faire dans les fonctions getXX() et setXX().

Sans les nouveaux get et set, voici la classe :

<?
/**
 * Rib extends Bban - représentation d'un RIB
 *
 * @package    leMondeDuDev
 * @subpackage iban
 * @author     Bruce Benamran © LeMondeDuDev.com — 2011
 */
class Rib extends Bban {
    private $codeBanque;
    private $codeAgence;
    private $noCompte;
    private $cle;

	protected function isValid() {
	    if (!parent::isValid())
	        return false;
	    // ici, on va rajouter les tests spécifiques aux RIB
	}
}
?>

Dans le genre simple, je m’excuse mais c’est quand même un peu les vacances…

Maintenant pour les get et les set (je préviens immédiatement que les set sont filous…) :

    public function getCodeBanque() {
        if ($this->getBanque() && (strLen($this->getBanque()) == 10))
            return subStr($this->getBanque(),0,5);
    }

    public function getCodeGuichet() {
        if ($this->getBanque() && (strLen($this->getBanque()) == 10))
            return subStr($this->getBanque(),-5);
    }

    public function getNoCompte() {
        if ($this->getCompte() && (strLen($this->getCompte()) == 13))
            return subStr($this->getCompte(),0,11);
    }

    public function getCle() {
        if ($this->getCompte() && (strLen($this->getCompte()) == 13))
            return subStr($this->getCompte(),-2);
    }

    public function setCodeBanque($v) { $this->setBanque($v . $this->getCodeGuichet()); }
    public function setCodeGuichet($v) { $this->setBanque($this->getCodeBanque() . $v); }
    public function setNoCompte($v) { $this->setCompte($v . $this->getCle()); }
    public function setCle($v) { $this->setBanque($this->getNoCompte() . $v); }

Reste la validation du RIB qu’il faut d’abord qu’on explique un peu, car l’algorithme, sans être compliqué, n’est pas aussi simple qu’il en a l’air; on va parler de substitution, de redisposition et de modulos en tout genre…

 Le contrôle d’un RIB

L’idée de base est relativement simple, mais les particularités vont complexifier un peu la chose : l’idée de base, c’est de composer, à partir de tous les codes du RIB (sauf la clé) un « mot » de 21 caractères (5+5+11), de séparer ensuite ce mot en trois mots de 7 caractères, et d’effectuer des opérations mathématiques sur ces trois mots pour obtenir un nombre qui, ajouté à la clé, est un multiple de 97 (résultat modulo 97 == 0).

La première complexité réside dans le fait que le numéro de compte, sur 11 caractères, peut contenir des lettres qu’il faut auparavant remplacer par des chiffres selon un algorithme ultra-difficile à retenir : a = 1, b = 2, c=3…

Là, vous vous dites que je me fous de vous, que c’était sarcastique, qu’il n’y a pas plus bête comme algorithme; ce n’est pas le cas, il y a deux subtilités dans cette substitution :

  • on n’utilise jamais le 0 : lorsqu’on arrive à i = 9, on passe directement à j = 1;
  • ensuite, lorsqu’on franchit le 0, il faut passer à 1 la première fois (pour le j), mais à 2 la seconde fois (pour le s); le mnémotechnique est qu’on passe à la dizaine supérieure (10 pour le j, et 20 pour le s).
Au final, cela donne :

 

1 2 3 4 5 6 7 8 9
A,J B,K,S C,L,T D,M,U E,N,V F,O,W G,P,X H,Q,Y I,R,Z

L’opération suivante est purement mathématique, et peut être envisagée de deux façons :

  • soit vous souhaitez trouver la clé RIB pour un RIB donné (ce qui ne sera pas notre cas);
  • soit vous souhaitez valider la saisie du RIB en constatant que la clé RIB donnée est bien la bonne (ce qui est notre cas).
Si vous souhaitez déduire la clé RIB, vous devez opérer de la façon suivante :

 

Clé RIB = 97 – { [ (89 x Banque) + (15 x Guichet) + (3 x NoCompte) ] modulo 97 }

 

En revanche, si vous avez la clé RIB et que vous souhaitez valider la saisie, vous devez vérifier l’égalité suivante (qui découle évidemment de l’opération précédente) :

 

{ [ (89 x Banque) + (15 x Guichet) + (3 x NoCompte) + CléRib ] modulo 97 } == 0

 

Au niveau du PHP, on va donc modifier la fonction isValid() pour arriver à :

protected function isValid() {
    if (!parent::isValid())
        return false;
    // ici, on va rajouter les tests spécifiques aux RIB
    $noCompteModifie = $this->getFormattedNoCompte();
    return ((89*$this->getCodeBanque() + 15*$this->getCodeGuichet() + 3*$this->getFormattedNoCompte + $this->getCle)%97 == 0);
}

Avec évidemment à côté la fonction de transformation du numéro de compte :

public function getFormattedNoCompte() {
    $substitutionAbc = array("A"=>1,"B"=>2,"C"=>3,"D"=>4,"E"=>5,"F"=>6,"G"=>7,"H"=>8,"I"=>9,
                             "K"=>1,"K"=>2,"L"=>3,"M"=>4,"F"=>5,"O"=>6,"P"=>7,"Q"=>8,"R"=>9,
                                    "S"=>2,"T"=>3,"U"=>4,"V"=>5,"W"=>6,"X"=>7,"Y"=>8,"Z"=>9);
    // On s'assure que les éventuelles lettres sont en majuscules
    $res = strToUpper($this->getNoCompte());
    for ($i = 0 ; $i < strLen($res) ; $i++)
        if (array_key_exists($res{$i},$substitutionAbc))
            $res{$i} = $substitutionAbc($res{$i});
    return $res;
}
Le contrôle de l’IBAN

Avant de commencer à rentrer dans le détail du contrôle de la clé IBAN, je vais juste mettre un point en avant pour vous simplifier le problème, si toutefois vous vous limitez exclusivement à la France; en effet, le contrôle de l’IBAN étant basé sur un modulo 97, tout comme le RIB dont la clé est calculée dans ce but, si le RIB ne contient que des chiffres, les quatre premiers caractères seront toujours FR76 (FR pour la France, et 76 qui est une curiosité mathématique, mais qui s’explique très naturellement lorsqu’on pose les équations).

Ceci étant dit, voici la règle de calcul de la clé (ou de contrôle de la saisie) :

  • vous prenez les 4 premiers caractères de votre IBAN et vous les mettez à la fin (vous obtenez donc RIB + CodePays + CléContrôle);
  • vous remplacez les lettres présentes dans le résultat selon une règle de substitution différente de celle du RIB (je vous la présente plus loin);
  • si la division du nombre obtenu par 97 laisse un reste de 1, la saisie est correcte.
inversément, si vous souhaitez déterminer la clé, les deux premières étapes sont les mêmes, il suffit de mettre 00 en lieu et place de la clé :
  • calculez le reste de la division du nombre par 97;
  • votre clé de contrôle est égale à 98 – le reste   (ce qui fait que le reste + la clé vaut 98, et 98[modulo 97] = 1).
L’alphabet de substitution est différent de celui qu’on a vu pour le RIB; notamment, on remplace les lettres par un nombre à deux chiffres (en revanche, aucune subtilité au milieu) :
  • a = 10;
  • b = 11;
  • y = 34;
  • z = 35.
On va maintenant intégrer cet algorithme dans la fonction de validation isValid() de la classe Iban.
protected function isValid() {
	// On valide ce qu'on doit valider : on part d'un a priori true qu'on va tester
    $res = true;

	// On va utiliser des expressions régulières sur le contenu
	// le code pays doit contenir deux lettres ou rien
	$res = $res && (is_null($this->getCodePays()) || preg_match("/^[a-zA-Z]{2}$/",$this->getCodePays()));

	// le code contrôle doit contenir jusqu'à deux chiffres ou rien
	$res = $res && (is_null($this->getCodeControle()) || preg_match("/^[0-9]{1,2}$/",$this->getCodeControle()));

	// le code bic doit contenir entre 8 et 11 caractères alphanumériques composés de 6 lettres, puis de 2 ou 5 alphanumériques
	$res = $res && (is_null($this->getCodePays()) || preg_match("/^[a-zA-Z]{6}[0-9a-zA-Z]{2}([0-9a-zA-Z]{3})?$/",$this->getCodePays()));

    // pas la peine de contrôler l'IBAN si on n'a même pas passé ces tests
    if (!$res)
        return false;

    // contrôle de la clé IBAN
    $substitutionAbc = array("A"=>10,"B"=>11,"C"=>12,"D"=>13,"E"=>14,"F"=>15,"G"=>16,"H"=>17,"I"=>18, "J"=>19,
                             "K"=>20,"L"=>21,"M"=>22,"N"=>23,"O"=>24,"P"=>25,"Q"=>26,"R"=>27,"S"=>28, "T"=>29,
                             "U"=>30,"V"=>31,"W"=>32,"X"=>33,"Y"=>34,"Z"=>35);
    $noComplet = $this->getBban()->getBanque() . $this->getBban()->getCompte() . $this->getCodePays() . $this->getCodeControle();
    // attention, on ne change pas lettre à lettre cette fois, puisqu'on change chaque lettre en deux chiffres
    $noComplet = str_replace(array_keys($substitutionAbc),$substitutionAbc,$noComplet);

    return ($noComplet%97 == 1);
}

Bilan

Voilà, on a une bonne base de départ; évidemment, de nombreuses choses peuvent être modifiées au niveau même de l’architecture, ou simplement pour mieux coller à un projet donné. Au niveau de l’architecture, on pourrait séparer l’Iban du Bban au niveau du modèle, on pourrait également faire hériter le Bban de l’Iban, on pourrait mettre le titulaire au niveau du Rib uniquement.

Au niveau du code également, on peut se simplifier la vie (parfois aux dépends d’une certaine lisibilité, ceci étant) :

  • lors des set de codes qui contiennent des lettres, on peut passer en majuscules automatiquement :
    setValue($v) { $this->value = strToUpper($v); }
  • on peut remplacer l’utilisation d’alphabets de substitution par le remplacement des caractères en fonction de leur valeur ASCII; ainsi, pour l’IBAN par exemple, puisque la valeur ASCII du A majuscule est de 65 et que les caractères de l’alphabet se suivent en ASCII, il suffit de remplacer la lettre par (valeur en ASCII – 55); En revanche, pour le RIB, c’est un poil plus complexe (mais c’est faisable, cf. cette page — mais pour le coup, c’est vraiment moins lisible)

 

J’espère que ce tuto vous sera utile, que comme moi vous avez appris des choses sur la façon dont les banques mettent en place des systèmes, et qui en dit long sur leur simplicité de fonctionnement. Évidemment, je serais ravi d’entendre vos commentaires sur le sujet, et plus encore de bénéficier de vos apports en la matière.

 

Enfin, l’ensemble des sources de ce petit apéritif est disponible ici

 

2 réponses à to “La gestion d’un RIB ou d’un IBAN en PHP”

  • Normann BERNARD:

    Bonjour,
    attention il y a une erreur dans un des codes sources cités ici.
    Dans le dernier (function isValid), dans le tableau de correspondance ($substitutionAbc), il y a 2 fois la lettre U. La 2ème occurence est en fait un Y (celle qui est remplacée par 34)

    Cordialement,

  • Bien vu, je corrige immédiatement.