Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Validator] Sync IBAN formats with Swift IBAN registry #48998

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/Validator/.gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/phpunit.xml.dist export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/Resources/bin/sync-iban-formats.php export-ignore
116 changes: 71 additions & 45 deletions src/Symfony/Component/Validator/Constraints/IbanValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
* @author Manuel Reinhard <[email protected]>
* @author Michael Schummel
* @author Bernhard Schussek <[email protected]>
*
* @see http://www.michael-schummel.de/2007/10/05/iban-prufung-mit-php/
*/
class IbanValidator extends ConstraintValidator
{
Expand All @@ -34,107 +32,135 @@ class IbanValidator extends ConstraintValidator
* a BBAN (Basic Bank Account Number) which has a fixed length per country and,
* included within it, a bank identifier with a fixed position and a fixed length per country
*
* @see https://www.swift.com/sites/default/files/resources/iban_registry.pdf
* @see Resources/bin/sync-iban-formats.php
* @see https://www.swift.com/swift-resource/11971/download?language=en
* @see https://en.wikipedia.org/wiki/International_Bank_Account_Number
*/
private const FORMATS = [
// auto-generated
'AD' => 'AD\d{2}\d{4}\d{4}[\dA-Z]{12}', // Andorra
'AE' => 'AE\d{2}\d{3}\d{16}', // United Arab Emirates
'AE' => 'AE\d{2}\d{3}\d{16}', // United Arab Emirates (The)
'AL' => 'AL\d{2}\d{8}[\dA-Z]{16}', // Albania
'AO' => 'AO\d{2}\d{21}', // Angola
'AT' => 'AT\d{2}\d{5}\d{11}', // Austria
'AX' => 'FI\d{2}\d{6}\d{7}\d{1}', // Aland Islands
'AX' => 'FI\d{2}\d{3}\d{11}', // Finland
'AZ' => 'AZ\d{2}[A-Z]{4}[\dA-Z]{20}', // Azerbaijan
'BA' => 'BA\d{2}\d{3}\d{3}\d{8}\d{2}', // Bosnia and Herzegovina
'BE' => 'BE\d{2}\d{3}\d{7}\d{2}', // Belgium
'BF' => 'BF\d{2}\d{23}', // Burkina Faso
'BF' => 'BF\d{2}[\dA-Z]{2}\d{22}', // Burkina Faso
'BG' => 'BG\d{2}[A-Z]{4}\d{4}\d{2}[\dA-Z]{8}', // Bulgaria
'BH' => 'BH\d{2}[A-Z]{4}[\dA-Z]{14}', // Bahrain
'BI' => 'BI\d{2}\d{12}', // Burundi
'BJ' => 'BJ\d{2}[A-Z]{1}\d{23}', // Benin
'BY' => 'BY\d{2}[\dA-Z]{4}\d{4}[\dA-Z]{16}', // Belarus - https://bank.codes/iban/structure/belarus/
'BL' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Saint Barthelemy
'BR' => 'BR\d{2}\d{8}\d{5}\d{10}[A-Z][\dA-Z]', // Brazil
'CG' => 'CG\d{2}\d{23}', // Congo
'BI' => 'BI\d{2}\d{5}\d{5}\d{11}\d{2}', // Burundi
'BJ' => 'BJ\d{2}[\dA-Z]{2}\d{22}', // Benin
'BL' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
'BR' => 'BR\d{2}\d{8}\d{5}\d{10}[A-Z]{1}[\dA-Z]{1}', // Brazil
'BY' => 'BY\d{2}[\dA-Z]{4}\d{4}[\dA-Z]{16}', // Republic of Belarus
'CF' => 'CF\d{2}\d{23}', // Central African Republic
'CG' => 'CG\d{2}\d{23}', // Congo, Republic of the
'CH' => 'CH\d{2}\d{5}[\dA-Z]{12}', // Switzerland
'CI' => 'CI\d{2}[A-Z]{1}\d{23}', // Ivory Coast
'CM' => 'CM\d{2}\d{23}', // Cameron
'CR' => 'CR\d{2}0\d{3}\d{14}', // Costa Rica
'CV' => 'CV\d{2}\d{21}', // Cape Verde
'CI' => 'CI\d{2}[A-Z]{1}\d{23}', // Côte d'Ivoire
'CM' => 'CM\d{2}\d{23}', // Cameroon
'CR' => 'CR\d{2}\d{4}\d{14}', // Costa Rica
'CV' => 'CV\d{2}\d{21}', // Cabo Verde
'CY' => 'CY\d{2}\d{3}\d{5}[\dA-Z]{16}', // Cyprus
'CZ' => 'CZ\d{2}\d{20}', // Czech Republic
'CZ' => 'CZ\d{2}\d{4}\d{6}\d{10}', // Czechia
'DE' => 'DE\d{2}\d{8}\d{10}', // Germany
'DJ' => 'DJ\d{2}\d{5}\d{5}\d{11}\d{2}', // Djibouti
'DK' => 'DK\d{2}\d{4}\d{9}\d{1}', // Denmark
'DO' => 'DO\d{2}[\dA-Z]{4}\d{20}', // Dominican Republic
'DK' => 'DK\d{2}\d{4}\d{10}', // Denmark
'DZ' => 'DZ\d{2}\d{20}', // Algeria
'DZ' => 'DZ\d{2}\d{22}', // Algeria
'EE' => 'EE\d{2}\d{2}\d{2}\d{11}\d{1}', // Estonia
'ES' => 'ES\d{2}\d{4}\d{4}\d{1}\d{1}\d{10}', // Spain (also includes Canary Islands, Ceuta and Melilla)
'FI' => 'FI\d{2}\d{6}\d{7}\d{1}', // Finland
'EG' => 'EG\d{2}\d{4}\d{4}\d{17}', // Egypt
'ES' => 'ES\d{2}\d{4}\d{4}\d{1}\d{1}\d{10}', // Spain
'FI' => 'FI\d{2}\d{3}\d{11}', // Finland
'FO' => 'FO\d{2}\d{4}\d{9}\d{1}', // Faroe Islands
'FR' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
'GF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // French Guyana
'GB' => 'GB\d{2}[A-Z]{4}\d{6}\d{8}', // United Kingdom of Great Britain and Northern Ireland
'GA' => 'GA\d{2}\d{23}', // Gabon
'GB' => 'GB\d{2}[A-Z]{4}\d{6}\d{8}', // United Kingdom
'GE' => 'GE\d{2}[A-Z]{2}\d{16}', // Georgia
'GF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
'GG' => 'GB\d{2}[A-Z]{4}\d{6}\d{8}', // United Kingdom
'GI' => 'GI\d{2}[A-Z]{4}[\dA-Z]{15}', // Gibraltar
'GL' => 'GL\d{2}\d{4}\d{9}\d{1}', // Greenland
'GP' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Guadeloupe
'GP' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
'GQ' => 'GQ\d{2}\d{23}', // Equatorial Guinea
'GR' => 'GR\d{2}\d{3}\d{4}[\dA-Z]{16}', // Greece
'GT' => 'GT\d{2}[\dA-Z]{4}[\dA-Z]{20}', // Guatemala
'GW' => 'GW\d{2}[\dA-Z]{2}\d{19}', // Guinea-Bissau
'HN' => 'HN\d{2}[A-Z]{4}\d{20}', // Honduras
'HR' => 'HR\d{2}\d{7}\d{10}', // Croatia
'HU' => 'HU\d{2}\d{3}\d{4}\d{1}\d{15}\d{1}', // Hungary
'IE' => 'IE\d{2}[A-Z]{4}\d{6}\d{8}', // Ireland
'IL' => 'IL\d{2}\d{3}\d{3}\d{13}', // Israel
'IM' => 'GB\d{2}[A-Z]{4}\d{6}\d{8}', // United Kingdom
'IQ' => 'IQ\d{2}[A-Z]{4}\d{3}\d{12}', // Iraq
'IR' => 'IR\d{2}\d{22}', // Iran
'IS' => 'IS\d{2}\d{4}\d{2}\d{6}\d{10}', // Iceland
'IT' => 'IT\d{2}[A-Z]{1}\d{5}\d{5}[\dA-Z]{12}', // Italy
'JE' => 'GB\d{2}[A-Z]{4}\d{6}\d{8}', // United Kingdom
'JO' => 'JO\d{2}[A-Z]{4}\d{4}[\dA-Z]{18}', // Jordan
'KW' => 'KW\d{2}[A-Z]{4}\d{22}', // KUWAIT
'KM' => 'KM\d{2}\d{23}', // Comoros
'KW' => 'KW\d{2}[A-Z]{4}[\dA-Z]{22}', // Kuwait
'KZ' => 'KZ\d{2}\d{3}[\dA-Z]{13}', // Kazakhstan
'LB' => 'LB\d{2}\d{4}[\dA-Z]{20}', // LEBANON
'LI' => 'LI\d{2}\d{5}[\dA-Z]{12}', // Liechtenstein (Principality of)
'LB' => 'LB\d{2}\d{4}[\dA-Z]{20}', // Lebanon
'LC' => 'LC\d{2}[A-Z]{4}[\dA-Z]{24}', // Saint Lucia
'LI' => 'LI\d{2}\d{5}[\dA-Z]{12}', // Liechtenstein
'LT' => 'LT\d{2}\d{5}\d{11}', // Lithuania
'LU' => 'LU\d{2}\d{3}[\dA-Z]{13}', // Luxembourg
'LV' => 'LV\d{2}[A-Z]{4}[\dA-Z]{13}', // Latvia
'LY' => 'LY\d{2}\d{3}\d{3}\d{15}', // Libya
'MA' => 'MA\d{2}\d{24}', // Morocco
'MC' => 'MC\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Monaco
'MD' => 'MD\d{2}[\dA-Z]{2}[\dA-Z]{18}', // Moldova
'ME' => 'ME\d{2}\d{3}\d{13}\d{2}', // Montenegro
'MF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Saint Martin (French part)
'MF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
'MG' => 'MG\d{2}\d{23}', // Madagascar
'MK' => 'MK\d{2}\d{3}[\dA-Z]{10}\d{2}', // Macedonia, Former Yugoslav Republic of
'ML' => 'ML\d{2}[A-Z]{1}\d{23}', // Mali
'MQ' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Martinique
'MK' => 'MK\d{2}\d{3}[\dA-Z]{10}\d{2}', // Macedonia
'ML' => 'ML\d{2}[\dA-Z]{2}\d{22}', // Mali
'MQ' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
'MR' => 'MR\d{2}\d{5}\d{5}\d{11}\d{2}', // Mauritania
'MT' => 'MT\d{2}[A-Z]{4}\d{5}[\dA-Z]{18}', // Malta
'MU' => 'MU\d{2}[A-Z]{4}\d{2}\d{2}\d{12}\d{3}[A-Z]{3}', // Mauritius
'MZ' => 'MZ\d{2}\d{21}', // Mozambique
'NC' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // New Caledonia
'NL' => 'NL\d{2}[A-Z]{4}\d{10}', // The Netherlands
'NC' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
'NE' => 'NE\d{2}[A-Z]{2}\d{22}', // Niger
'NI' => 'NI\d{2}[A-Z]{4}\d{24}', // Nicaragua
'NL' => 'NL\d{2}[A-Z]{4}\d{10}', // Netherlands (The)
'NO' => 'NO\d{2}\d{4}\d{6}\d{1}', // Norway
'PF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // French Polynesia
'PF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
'PK' => 'PK\d{2}[A-Z]{4}[\dA-Z]{16}', // Pakistan
'PL' => 'PL\d{2}\d{8}\d{16}', // Poland
'PM' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Saint Pierre et Miquelon
'PM' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
'PS' => 'PS\d{2}[A-Z]{4}[\dA-Z]{21}', // Palestine, State of
'PT' => 'PT\d{2}\d{4}\d{4}\d{11}\d{2}', // Portugal (plus Azores and Madeira)
'PT' => 'PT\d{2}\d{4}\d{4}\d{11}\d{2}', // Portugal
'QA' => 'QA\d{2}[A-Z]{4}[\dA-Z]{21}', // Qatar
'RE' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Reunion
'RE' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
'RO' => 'RO\d{2}[A-Z]{4}[\dA-Z]{16}', // Romania
'RS' => 'RS\d{2}\d{3}\d{13}\d{2}', // Serbia
'RU' => 'RU\d{2}\d{9}\d{5}[\dA-Z]{15}', // Russia
'SA' => 'SA\d{2}\d{2}[\dA-Z]{18}', // Saudi Arabia
'SC' => 'SC\d{2}[A-Z]{4}\d{2}\d{2}\d{16}[A-Z]{3}', // Seychelles
'SD' => 'SD\d{2}\d{2}\d{12}', // Sudan
'SE' => 'SE\d{2}\d{3}\d{16}\d{1}', // Sweden
'SI' => 'SI\d{2}\d{5}\d{8}\d{2}', // Slovenia
'SK' => 'SK\d{2}\d{4}\d{6}\d{10}', // Slovak Republic
'SK' => 'SK\d{2}\d{4}\d{6}\d{10}', // Slovakia
'SM' => 'SM\d{2}[A-Z]{1}\d{5}\d{5}[\dA-Z]{12}', // San Marino
'SN' => 'SN\d{2}[A-Z]{1}\d{23}', // Senegal
'TF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // French Southern Territories
'SN' => 'SN\d{2}[A-Z]{2}\d{22}', // Senegal
'SO' => 'SO\d{2}\d{4}\d{3}\d{12}', // Somalia
'ST' => 'ST\d{2}\d{4}\d{4}\d{11}\d{2}', // Sao Tome and Principe
'SV' => 'SV\d{2}[A-Z]{4}\d{20}', // El Salvador
'TD' => 'TD\d{2}\d{23}', // Chad
'TF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
'TG' => 'TG\d{2}[A-Z]{2}\d{22}', // Togo
'TL' => 'TL\d{2}\d{3}\d{14}\d{2}', // Timor-Leste
'TN' => 'TN\d{2}\d{2}\d{3}\d{13}\d{2}', // Tunisia
'TR' => 'TR\d{2}\d{5}[\dA-Z]{1}[\dA-Z]{16}', // Turkey
'TR' => 'TR\d{2}\d{5}\d{1}[\dA-Z]{16}', // Turkey
'UA' => 'UA\d{2}\d{6}[\dA-Z]{19}', // Ukraine
'VA' => 'VA\d{2}\d{3}\d{15}', // Vatican City State
'VG' => 'VG\d{2}[A-Z]{4}\d{16}', // Virgin Islands, British
'WF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Wallis and Futuna Islands
'XK' => 'XK\d{2}\d{4}\d{10}\d{2}', // Republic of Kosovo
'YT' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Mayotte
'VG' => 'VG\d{2}[A-Z]{4}\d{16}', // Virgin Islands
'WF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
'XK' => 'XK\d{2}\d{4}\d{10}\d{2}', // Kosovo
'YT' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
];

/**
Expand Down
204 changes: 204 additions & 0 deletions src/Symfony/Component/Validator/Resources/bin/sync-iban-formats.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
#!/usr/bin/env php
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

if ('cli' !== \PHP_SAPI) {
throw new \Exception('This script must be run from the command line.');
}

/*
* This script syncs IBAN formats from the upstream and updates them into IbanValidator.
*
* Usage:
* php Resources/bin/sync-iban-formats.php
*/

error_reporting(\E_ALL);

set_error_handler(static function (int $type, string $msg, string $file, int $line): void {
throw new \ErrorException($msg, 0, $type, $file, $line);
});

echo "Collecting IBAN formats...\n";

$formats = array_merge(
(new WikipediaIbanProvider())->getIbanFormats(),
(new SwiftRegistryIbanProvider())->getIbanFormats()
);

printf("Collected %d IBAN formats\n", count($formats));

echo "Updating validator...\n";

updateValidatorFormats(__DIR__.'/../../Constraints/IbanValidator.php', $formats);

echo "Done.\n";

exit(0);

function updateValidatorFormats(string $validatorPath, array $formats): void
{
ksort($formats);

$formatsContent = "[\n";
$formatsContent .= " // auto-generated\n";

foreach ($formats as $countryCode => [$format, $country]) {
$formatsContent .= " '{$countryCode}' => '{$format}', // {$country}\n";
}

$formatsContent .= ' ]';

$validatorContent = file_get_contents($validatorPath);

$validatorContent = preg_replace(
'/FORMATS = \[.*?\];/s',
"FORMATS = {$formatsContent};",
$validatorContent
);

file_put_contents($validatorPath, $validatorContent);
}

final class SwiftRegistryIbanProvider
{
/**
* @return array<string, array{string, string}>
*/
public function getIbanFormats(): array
{
$items = $this->readPropertiesFromRegistry([
'Name of country' => 'country',
'IBAN prefix country code (ISO 3166)' => 'country_code',
'IBAN structure' => 'iban_structure',
'Country code includes other countries/territories' => 'included_country_codes',
]);

$formats = [];

foreach ($items as $item) {
$formats[$item['country_code']] = [$this->buildIbanRegexp($item['iban_structure']), $item['country']];

foreach ($this->parseCountryCodesList($item['included_country_codes']) as $includedCountryCode) {
$formats[$includedCountryCode] = $formats[$item['country_code']];
}
}

return $formats;
}

/**
* @return list<string>
*/
private function parseCountryCodesList(string $countryCodesList): array
{
if ('N/A' === $countryCodesList) {
return [];
}

$countryCodes = [];

foreach (explode(',', $countryCodesList) as $countryCode) {
$countryCodes[] = preg_replace('/^([A-Z]{2})(\s+\(.+?\))?$/', '$1', trim($countryCode));
}

return $countryCodes;
}

/**
* @param array<string, string> $properties
*
* @return list<array<string, string>>
*/
private function readPropertiesFromRegistry(array $properties): array
{
$items = [];

$registryContent = file_get_contents('https://www.swift.com/swift-resource/11971/download');
$lines = explode("\n", $registryContent);

// skip header line
array_shift($lines);

foreach ($lines as $line) {
$columns = str_getcsv($line, "\t");
$propertyLabel = array_shift($columns);

if (!isset($properties[$propertyLabel])) {
continue;
}

$propertyField = $properties[$propertyLabel];

foreach ($columns as $index => $value) {
$items[$index][$propertyField] = $value;
}
}

return array_values($items);
}

private function buildIbanRegexp(string $ibanStructure): string
{
$pattern = $ibanStructure;

$pattern = preg_replace('/(\d+)!n/', '\\d{$1}', $pattern);
$pattern = preg_replace('/(\d+)!a/', '[A-Z]{$1}', $pattern);
$pattern = preg_replace('/(\d+)!c/', '[\\dA-Z]{$1}', $pattern);

return $pattern;
}
}

final class WikipediaIbanProvider
{
/**
* @return array<string, array{string, string}>
*/
public function getIbanFormats(): array
{
$formats = [];

foreach ($this->readIbanFormatsTable() as $item) {
if (!preg_match('/^([A-Z]{2})/', $item['Example'], $matches)) {
continue;
}

$countryCode = $matches[1];

$formats[$countryCode] = [$this->buildIbanRegexp($countryCode, $item['BBAN Format']), $item['Country']];
}

return $formats;
}

/**
* @return list<array<string, string|int>>
*/
private function readIbanFormatsTable(): array
{
$tablesResponse = file_get_contents('https://www.wikitable2json.com/api/International_Bank_Account_Number?table=3&keyRows=1&clearRef=true');

return json_decode($tablesResponse, true, 512, JSON_THROW_ON_ERROR)[0];
}

private function buildIbanRegexp(string $countryCode, string $bbanFormat): string
{
$pattern = $bbanFormat;

$pattern = preg_replace('/\s*,\s*/', '', $pattern);
$pattern = preg_replace('/(\d+)n/', '\\d{$1}', $pattern);
$pattern = preg_replace('/(\d+)a/', '[A-Z]{$1}', $pattern);
$pattern = preg_replace('/(\d+)c/', '[\\dA-Z]{$1}', $pattern);

return $countryCode.'\\d{2}'.$pattern;
}
}
Loading