18

As designing a new platform we tried to integrate the IBAN numbers. We have to make sure that the IBAN is validated and the IBAN stored to the database is always correct. So what would be a proper way to validate the number?

Peter Fox
  • 1,239
  • 2
  • 11
  • 31
  • Most do not have the country specific check, as recommended. – Peter Fox Jan 07 '14 at 22:39
  • 1
    The funny thing is: If you click on @MarcinOrlowski lmfgtfy link, this question pops up as the first search result ;) – Frederik Kammer Aug 30 '16 at 17:24
  • @FrederikKammer perhaps it's because of this link in the first place, so some sort of infinite loop in the wild :) – Marcin Orlowski Aug 30 '16 at 18:16
  • Google Code hosts a project of an open source php-iban validator: https://code.google.com/p/php-iban/ – user3733632 Jun 12 '14 at 10:12
  • Sorry, I can not yet comment due to low Rep, so the new link to user3733632s answer: https://github.com/globalcitizen/php-iban (the project is no longer hosted at google code as of March 2015, and the project will get a new version after some cleanup.) – Shuhari Apr 12 '15 at 21:12

7 Answers7

53

As the logic was explained in my other question, I've created a function myself. Based on the logic explained in the Wikipedia article find a proper function below. Country specific validation.

Algorithm and character lengths per country at https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN.

function checkIBAN($iban)
{
    if(strlen($iban) < 5) return false;
    $iban = strtolower(str_replace(' ','',$iban));
    $Countries = array('al'=>28,'ad'=>24,'at'=>20,'az'=>28,'bh'=>22,'be'=>16,'ba'=>20,'br'=>29,'bg'=>22,'cr'=>21,'hr'=>21,'cy'=>28,'cz'=>24,'dk'=>18,'do'=>28,'ee'=>20,'fo'=>18,'fi'=>18,'fr'=>27,'ge'=>22,'de'=>22,'gi'=>23,'gr'=>27,'gl'=>18,'gt'=>28,'hu'=>28,'is'=>26,'ie'=>22,'il'=>23,'it'=>27,'jo'=>30,'kz'=>20,'kw'=>30,'lv'=>21,'lb'=>28,'li'=>21,'lt'=>20,'lu'=>20,'mk'=>19,'mt'=>31,'mr'=>27,'mu'=>30,'mc'=>27,'md'=>24,'me'=>22,'nl'=>18,'no'=>15,'pk'=>24,'ps'=>29,'pl'=>28,'pt'=>25,'qa'=>29,'ro'=>24,'sm'=>27,'sa'=>24,'rs'=>22,'sk'=>24,'si'=>19,'es'=>24,'se'=>24,'ch'=>21,'tn'=>24,'tr'=>26,'ae'=>23,'gb'=>22,'vg'=>24);
    $Chars = 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);

    if(array_key_exists(substr($iban,0,2), $Countries) && strlen($iban) == $Countries[substr($iban,0,2)]){
                
        $MovedChar = substr($iban, 4).substr($iban,0,4);
        $MovedCharArray = str_split($MovedChar);
        $NewString = "";

        foreach($MovedCharArray AS $key => $value){
            if(!is_numeric($MovedCharArray[$key])){
                if(!isset($Chars[$MovedCharArray[$key]])) return false;
                $MovedCharArray[$key] = $Chars[$MovedCharArray[$key]];
            }
            $NewString .= $MovedCharArray[$key];
        }
        
        if(bcmod($NewString, '97') == 1)
        {
            return true;
        }
    }
    return false;
}
Scholtz
  • 2,878
  • 2
  • 23
  • 23
Peter Fox
  • 1,239
  • 2
  • 11
  • 31
  • Nice! The only thing you missed it to initialize the $NewString variable to an empty string, if not, it'll throw an error. – sucotronic Jan 24 '14 at 16:52
  • @PeterFox Where did you get the list of countries and their IBAN lengths, and does the list include every country? – BadHorsie Jul 16 '14 at 13:12
  • 1
    @BadHorsie in that case, Wikipedia was my friend. Try them – Peter Fox Jul 17 '14 at 13:36
  • 6
    Also, you should check if `substr($iban,0,2)` is actually a key in the `$Countries` array before you check `if(strlen($iban) == $Countries[substr($iban,0,2)])`. Prevents PHP errors if users enter nonsense input. – Wouter Florijn Nov 23 '14 at 15:02
  • 2
    You should use: `if(array_key_exists(substr($iban,0,2), $Countries) && strlen($iban) == $Countries[substr($iban,0,2)]){` – AndiPower Nov 30 '15 at 15:26
  • If you end up with configuration issues when using bcmod, try https://stackoverflow.com/questions/10626277/function-bcmod-is-not-available. Worked perfectly well for me. – Martin G Feb 12 '18 at 20:03
10

Slight modification of @PeterFox answer including support for bcmod() when bcmath is not available,

<?php

function isValidIBAN ($iban) {

  $iban = strtolower($iban);
  $Countries = array(
    'al'=>28,'ad'=>24,'at'=>20,'az'=>28,'bh'=>22,'be'=>16,'ba'=>20,'br'=>29,'bg'=>22,'cr'=>21,'hr'=>21,'cy'=>28,'cz'=>24,
    'dk'=>18,'do'=>28,'ee'=>20,'fo'=>18,'fi'=>18,'fr'=>27,'ge'=>22,'de'=>22,'gi'=>23,'gr'=>27,'gl'=>18,'gt'=>28,'hu'=>28,
    'is'=>26,'ie'=>22,'il'=>23,'it'=>27,'jo'=>30,'kz'=>20,'kw'=>30,'lv'=>21,'lb'=>28,'li'=>21,'lt'=>20,'lu'=>20,'mk'=>19,
    'mt'=>31,'mr'=>27,'mu'=>30,'mc'=>27,'md'=>24,'me'=>22,'nl'=>18,'no'=>15,'pk'=>24,'ps'=>29,'pl'=>28,'pt'=>25,'qa'=>29,
    'ro'=>24,'sm'=>27,'sa'=>24,'rs'=>22,'sk'=>24,'si'=>19,'es'=>24,'se'=>24,'ch'=>21,'tn'=>24,'tr'=>26,'ae'=>23,'gb'=>22,'vg'=>24
  );
  $Chars = 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
  );

  if (strlen($iban) != $Countries[ substr($iban,0,2) ]) { return false; }

  $MovedChar = substr($iban, 4) . substr($iban,0,4);
  $MovedCharArray = str_split($MovedChar);
  $NewString = "";

  foreach ($MovedCharArray as $k => $v) {

    if ( !is_numeric($MovedCharArray[$k]) ) {
      $MovedCharArray[$k] = $Chars[$MovedCharArray[$k]];
    }
    $NewString .= $MovedCharArray[$k];
  }
  if (function_exists("bcmod")) { return bcmod($NewString, '97') == 1; }

  // http://au2.php.net/manual/en/function.bcmod.php#38474
  $x = $NewString; $y = "97";
  $take = 5; $mod = "";

  do {
    $a = (int)$mod . substr($x, 0, $take);
    $x = substr($x, $take);
    $mod = $a % $y;
  }
  while (strlen($x));

  return (int)$mod == 1;
}
mpapec
  • 50,217
  • 8
  • 67
  • 127
  • perhaps you could also add an condition, if somebody put in code of not existing country if (!isset($Countries[ substr($iban,0,2) ]) || strlen($iban) != $Countries[ substr($iban,0,2) ]) { return false; }` – FantomX1 Jan 29 '19 at 14:22
  • Added some extra checks: https://gist.github.com/Sroose/47f8ce5cf5fe36f52eba01dabc474d8c – S. Roose Nov 24 '19 at 14:39
7

The accepted answer is not the preferred way of validation. The specification dictates the following:

  1. Check that the total IBAN length is correct as per the country. If not, the IBAN is invalid
  2. Replace the two check digits by 00 (e.g. GB00 for the UK)
  3. Move the four initial characters to the end of the string
  4. Replace the letters in the string with digits, expanding the string as necessary, such that A or a = 10, B or b = 11, and Z or z = 35. Each alphabetic character is therefore replaced by 2 digits
  5. Convert the string to an integer (i.e. ignore leading zeroes)
  6. Calculate mod-97 of the new number, which results in the remainder
  7. Subtract the remainder from 98, and use the result for the two check digits. If the result is a single digit number, pad it with a leading 0 to make a two-digit number

I've written a class that validates, formats and parses strings according to the spec. Hope this helps some save the time required to roll their own.

The code can be found on GitHub here.

robinCTS
  • 5,746
  • 14
  • 30
  • 37
esserj
  • 166
  • 2
  • 5
  • 1
    This is simply not true. What you are doing is calculating the check digits, thats totally fine but not necessary if you just want to validate the IBAN. If you do not reset the check digits to zero and do not subtract from 98 at the end, then you get 1 if the IBAN is valid. That is sufficient if you only want to verify. – inta Jul 27 '16 at 11:17
1

top rated function does NOT work.

Just try a string with '%' in it...

I'm using this one :

function checkIBAN($iban) {

// Normalize input (remove spaces and make upcase)
$iban = strtoupper(str_replace(' ', '', $iban));

if (preg_match('/^[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}$/', $iban)) {
    $country = substr($iban, 0, 2);
    $check = intval(substr($iban, 2, 2));
    $account = substr($iban, 4);

    // To numeric representation
    $search = range('A','Z');
    foreach (range(10,35) as $tmp)
        $replace[]=strval($tmp);
    $numstr=str_replace($search, $replace, $account.$country.'00');

    // Calculate checksum
    $checksum = intval(substr($numstr, 0, 1));
    for ($pos = 1; $pos < strlen($numstr); $pos++) {
        $checksum *= 10;
        $checksum += intval(substr($numstr, $pos,1));
        $checksum %= 97;
    }

    return ((98-$checksum) == $check);
} else
    return false;
}
jhabai
  • 11
  • 3
1

This function check the IBAN and need GMP activate http://php.net/manual/en/book.gmp.php.

function checkIban($string){
    $to_check = substr($string, 4).substr($string, 0,4);
    $converted = '';
    for ($i = 0; $i < strlen($to_check); $i++){
        $char = strtoupper($to_check[$i]);
        if(preg_match('/[0-9A-Z]/',$char)){
            if(!preg_match('/\d/',$char)){
                $char = ord($char)-55;
            }
            $converted .= $char;
        }
    }

    // prevent: "gmp_mod() $num1 is not an integer string" error
    $converted = ltrim($converted, '0');

    return strlen($converted) && gmp_strval(gmp_mod($converted, '97')) == 1;
}

enjoy !

Kerem
  • 11,377
  • 5
  • 59
  • 58
Fabrice G
  • 273
  • 2
  • 5
1

I found this solution in cakephp 3.7 validation class. Plain beautiful php realization.

/**
 * Check that the input value has a valid International Bank Account Number IBAN syntax
 * Requirements are uppercase, no whitespaces, max length 34, country code and checksum exist at right spots,
 * body matches against checksum via Mod97-10 algorithm
 *
 * @param string $check The value to check
 *
 * @return bool Success
 */
public static function iban($check)
{
    if (!preg_match('/^[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}$/', $check)) {
        return false;
    }

    $country = substr($check, 0, 2);
    $checkInt = intval(substr($check, 2, 2));
    $account = substr($check, 4);
    $search = range('A', 'Z');
    $replace = [];
    foreach (range(10, 35) as $tmp) {
        $replace[] = strval($tmp);
    }
    $numStr = str_replace($search, $replace, $account . $country . '00');
    $checksum = intval(substr($numStr, 0, 1));
    $numStrLength = strlen($numStr);
    for ($pos = 1; $pos < $numStrLength; $pos++) {
        $checksum *= 10;
        $checksum += intval(substr($numStr, $pos, 1));
        $checksum %= 97;
    }

    return ((98 - $checksum) === $checkInt);
}
Aivaras
  • 123
  • 1
  • 9
0

i'd suggest a bit of optimalization for checksum calc ...

function calculateIBANChecksum($CIBAN){
    $CX = str_split($CIBAN);
    $checksum = $CX[0];
    array_shift($CX);
    foreach ($CX as $pos => $DIGIT) {
        $checksum *= 10;
        $checksum += $DIGIT * 1;
        $checksum %= 97;
    }
    return  (98 - $checksum);
}
hugo
  • 76
  • 6