IBAN::getCountryCode()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace CMPayments;
4
5
/**
6
 * IBAN information and validation library
7
 *
8
 * based on https://github.com/jschaedl/Iban by Jan Schaedlich <[email protected]>
9
 *
10
 * @category Utility
11
 * @package  IBAN
12
 * @author   Bas Peters <[email protected]>
13
 *
14
 * @link     https://github.com/cmpayments/iban
15
 */
16
17
class IBAN
18
{
19
    /**
20
     * Semantic IBAN structure constants
21
     */
22
    const COUNTRY_CODE_OFFSET             = 0;
23
    const COUNTRY_CODE_LENGTH             = 2;
24
    const CHECKSUM_OFFSET                 = 2;
25
    const CHECKSUM_LENGTH                 = 2;
26
    const ACCOUNT_IDENTIFICATION_OFFSET   = 4;
27
    const INSTITUTE_IDENTIFICATION_OFFSET = 4;
28
    const INSTITUTE_IDENTIFICATION_LENGTH = 4;
29
    const BANK_ACCOUNT_NUMBER_OFFSET      = 8;
30
    const BANK_ACCOUNT_NUMBER_LENGTH      = 10;
31
32
    /**
33
     * @var array Country code to size, regex format for each country that supports IBAN
34
     */
35
    public static $ibanFormatMap = [
36
        'AA' => [12, '^[A-Z0-9]{12}$'],
37
        'AD' => [20, '^[0-9]{4}[0-9]{4}[A-Z0-9]{12}$'],
38
        'AE' => [19, '^[0-9]{3}[0-9]{16}$'],
39
        'AL' => [24, '^[0-9]{8}[A-Z0-9]{16}$'],
40
        'AO' => [21, '^[0-9]{21}$'],
41
        'AT' => [16, '^[0-9]{5}[0-9]{11}$'],
42
        'AX' => [14, '^[0-9]{6}[0-9]{7}[0-9]{1}$'],
43
        'AZ' => [24, '^[A-Z]{4}[A-Z0-9]{20}$'],
44
        'BA' => [16, '^[0-9]{3}[0-9]{3}[0-9]{8}[0-9]{2}$'],
45
        'BE' => [12, '^[0-9]{3}[0-9]{7}[0-9]{2}$'],
46
        'BF' => [23, '^[0-9]{23}$'],
47
        'BG' => [18, '^[A-Z]{4}[0-9]{4}[0-9]{2}[A-Z0-9]{8}$'],
48
        'BH' => [18, '^[A-Z]{4}[A-Z0-9]{14}$'],
49
        'BI' => [12, '^[0-9]{12}$'],
50
        'BJ' => [24, '^[A-Z]{1}[0-9]{23}$'],
51
        'BL' => [23, '^[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$'],
52
        'BR' => [25, '^[0-9]{8}[0-9]{5}[0-9]{10}[A-Z]{1}[A-Z0-9]{1}$'],
53
        'CH' => [17, '^[0-9]{5}[A-Z0-9]{12}$'],
54
        'CI' => [24, '^[A-Z]{1}[0-9]{23}$'],
55
        'CM' => [23, '^[0-9]{23}$'],
56
        'CR' => [17, '^[0-9]{4}[0-9]{13}$'],
57
        'CV' => [21, '^[0-9]{21}$'],
58
        'CY' => [24, '^[0-9]{3}[0-9]{5}[A-Z0-9]{16}$'],
59
        'CZ' => [20, '^[0-9]{4}[0-9]{6}[0-9]{10}$'],
60
        'DE' => [18, '^[0-9]{8}[0-9]{10}$'],
61
        'DK' => [14, '^[0-9]{4}[0-9]{9}[0-9]{1}$'],
62
        'DO' => [24, '^[A-Z0-9]{4}[0-9]{20}$'],
63
        'DZ' => [20, '^[0-9]{20}$'],
64
        'EE' => [16, '^[0-9]{2}[0-9]{2}[0-9]{11}[0-9]{1}$'],
65
        'ES' => [20, '^[0-9]{4}[0-9]{4}[0-9]{1}[0-9]{1}[0-9]{10}$'],
66
        'FI' => [14, '^[0-9]{6}[0-9]{7}[0-9]{1}$'],
67
        'FO' => [14, '^[0-9]{4}[0-9]{9}[0-9]{1}$'],
68
        'FR' => [23, '^[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$'],
69
        'GB' => [18, '^[A-Z]{4}[0-9]{6}[0-9]{8}$'],
70
        'GE' => [18, '^[A-Z]{2}[0-9]{16}$'],
71
        'GF' => [23, '^[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$'],
72
        'GI' => [19, '^[A-Z]{4}[A-Z0-9]{15}$'],
73
        'GL' => [14, '^[0-9]{4}[0-9]{9}[0-9]{1}$'],
74
        'GP' => [23, '^[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$'],
75
        'GR' => [23, '^[0-9]{3}[0-9]{4}[A-Z0-9]{16}$'],
76
        'GT' => [24, '^[A-Z0-9]{4}[A-Z0-9]{20}$'],
77
        'HR' => [17, '^[0-9]{7}[0-9]{10}$'],
78
        'HU' => [24, '^[0-9]{3}[0-9]{4}[0-9]{1}[0-9]{15}[0-9]{1}$'],
79
        'IE' => [18, '^[A-Z]{4}[0-9]{6}[0-9]{8}$'],
80
        'IL' => [19, '^[0-9]{3}[0-9]{3}[0-9]{13}$'],
81
        'IR' => [22, '^[0-9]{22}$'],
82
        'IS' => [22, '^[0-9]{4}[0-9]{2}[0-9]{6}[0-9]{10}$'],
83
        'IT' => [23, '^[A-Z]{1}[0-9]{5}[0-9]{5}[A-Z0-9]{12}$'],
84
        'JO' => [26, '^[A-Z]{4}[0-9]{4}[A-Z0-9]{18}$'],
85
        'KW' => [26, '^[A-Z]{4}[A-Z0-9]{22}$'],
86
        'KZ' => [16, '^[0-9]{3}[A-Z0-9]{13}$'],
87
        'LB' => [24, '^[0-9]{4}[A-Z0-9]{20}$'],
88
        'LC' => [28, '^[A-Z]{4}[A-Z0-9]{24}$'],
89
        'LI' => [17, '^[0-9]{5}[A-Z0-9]{12}$'],
90
        'LT' => [16, '^[0-9]{5}[0-9]{11}$'],
91
        'LU' => [16, '^[0-9]{3}[A-Z0-9]{13}$'],
92
        'LV' => [17, '^[A-Z]{4}[A-Z0-9]{13}$'],
93
        'MC' => [23, '^[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$'],
94
        'MD' => [20, '^[A-Z0-9]{2}[A-Z0-9]{18}$'],
95
        'ME' => [18, '^[0-9]{3}[0-9]{13}[0-9]{2}$'],
96
        'MF' => [23, '^[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$'],
97
        'MG' => [23, '^[0-9]{23}$'],
98
        'MK' => [15, '^[0-9]{3}[A-Z0-9]{10}[0-9]{2}$'],
99
        'ML' => [24, '^[A-Z]{1}[0-9]{23}$'],
100
        'MQ' => [23, '^[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$'],
101
        'MR' => [23, '^[0-9]{5}[0-9]{5}[0-9]{11}[0-9]{2}$'],
102
        'MT' => [27, '^[A-Z]{4}[0-9]{5}[A-Z0-9]{18}$'],
103
        'MU' => [26, '^[A-Z]{4}[0-9]{2}[0-9]{2}[0-9]{12}[0-9]{3}[A-Z]{3}$'],
104
        'MZ' => [21, '^[0-9]{21}$'],
105
        'NC' => [23, '^[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$'],
106
        'NL' => [14, '^[A-Z]{4}[0-9]{10}$'],
107
        'NO' => [11, '^[0-9]{4}[0-9]{6}[0-9]{1}$'],
108
        'PF' => [23, '^[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$'],
109
        'PK' => [20, '^[A-Z]{4}[A-Z0-9]{16}$'],
110
        'PL' => [24, '^[0-9]{8}[0-9]{16}$'],
111
        'PM' => [23, '^[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$'],
112
        'PS' => [25, '^[A-Z]{4}[A-Z0-9]{21}$'],
113
        'PT' => [21, '^[0-9]{4}[0-9]{4}[0-9]{11}[0-9]{2}$'],
114
        'QA' => [25, '^[A-Z]{4}[0-9]{4}[A-Z0-9]{17}$'],
115
        'RE' => [23, '^[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$'],
116
        'RO' => [20, '^[A-Z]{4}[A-Z0-9]{16}$'],
117
        'RS' => [18, '^[0-9]{3}[0-9]{13}[0-9]{2}$'],
118
        'SA' => [20, '^[0-9]{2}[A-Z0-9]{18}$'],
119
        'SC' => [27, '^[A-Z]{4}[0-9]{4}[0-9]{16}[A-Z]{3}$'],
120
        'SE' => [20, '^[0-9]{3}[0-9]{16}[0-9]{1}$'],
121
        'SI' => [15, '^[0-9]{5}[0-9]{8}[0-9]{2}$'],
122
        'SK' => [20, '^[0-9]{4}[0-9]{6}[0-9]{10}$'],
123
        'SM' => [23, '^[A-Z]{1}[0-9]{5}[0-9]{5}[A-Z0-9]{12}$'],
124
        'SN' => [24, '^[A-Z]{1}[0-9]{23}$'],
125
        'ST' => [21, '^[0-9]{8}[0-9]{11}[0-9]{2}$'],
126
        'TF' => [23, '^[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$'],
127
        'TL' => [19, '^[0-9]{3}[0-9]{14}[0-9]{2}$'],
128
        'TN' => [20, '^[0-9]{2}[0-9]{3}[0-9]{13}[0-9]{2}$'],
129
        'TR' => [22, '^[0-9]{5}[0-9]{1}[A-Z0-9]{16}$'],
130
        'UA' => [25, '^[0-9]{6}[A-Z0-9]{19}$'],
131
        'VG' => [20, '^[A-Z]{4}[0-9]{16}$'],
132
        'WF' => [23, '^[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$'],
133
        'XK' => [16, '^[0-9]{4}[0-9]{10}[0-9]{2}$'],
134
        'YT' => [23, '^[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}$']
135
    ];
136
137
    /**
138
     * @var string Internal IBAN number
139
     */
140
    private $iban;
141
142
    /**
143
     * IBAN constructor.
144
     *
145
     * @param $iban
146
     */
147
    public function __construct($iban)
148
    {
149
        $this->iban = $this->normalize($iban);
150
    }
151
152
    /**
153
     * Validates the supplied IBAN and provides passthrough failure message when validation fails
154
     *
155
     * @param null $error passthrough variable
156
     *
157
     * @return bool
158
     */
159
    public function validate(&$error = null)
160
    {
161
        if (!$this->isCountryCodeValid()) {
162
            $error = 'IBAN country code not valid or not supported';
163
        } elseif (!$this->isLengthValid()) {
164
            $error = 'IBAN length is invalid';
165
        } elseif (!$this->isFormatValid()) {
166
            $error = 'IBAN format is invalid';
167
        } elseif (!$this->isChecksumValid()) {
168
            $error = 'IBAN checksum is invalid';
169
        } else {
170
            $error = '';
171
            return true;
172
        }
173
174
        return false;
175
    }
176
177
    /**
178
     * Pretty print IBAN
179
     *
180
     * @return string
181
     */
182
    public function format()
183
    {
184
        return sprintf(
185
            '%s %s %s',
186
            $this->getCountryCode() . $this->getChecksum(),
187
            substr($this->getInstituteIdentification(), 0, 4),
188
            implode(' ', str_split($this->getBankAccountNumber(), 4))
189
        );
190
    }
191
192
    /**
193
     * Extract country code from IBAN
194
     *
195
     * @return string
196
     */
197
    public function getCountryCode()
198
    {
199
        return substr($this->iban, static::COUNTRY_CODE_OFFSET, static::COUNTRY_CODE_LENGTH);
200
    }
201
202
    /**
203
     * Extract checksum number from IBAN
204
     *
205
     * @return string
206
     */
207
    public function getChecksum()
208
    {
209
        return substr($this->iban, static::CHECKSUM_OFFSET, static::CHECKSUM_LENGTH);
210
    }
211
212
    /**
213
     * Extract Account Identification from IBAN
214
     *
215
     * @return string
216
     */
217
    public function getAccountIdentification()
218
    {
219
        return substr($this->iban, static::ACCOUNT_IDENTIFICATION_OFFSET);
220
    }
221
222
    /**
223
     * Extract Institute from IBAN
224
     *
225
     * @return string
226
     */
227
    public function getInstituteIdentification()
228
    {
229
        return substr($this->iban, static::INSTITUTE_IDENTIFICATION_OFFSET, static::INSTITUTE_IDENTIFICATION_LENGTH);
230
    }
231
232
    /**
233
     * Extract Bank Account number from IBAN
234
     *
235
     * @return string
236
     */
237
    public function getBankAccountNumber()
238
    {
239
        $countryCode = $this->getCountryCode();
240
        $length = static::$ibanFormatMap[$countryCode][0] - static::INSTITUTE_IDENTIFICATION_LENGTH;
241
        return substr($this->iban, static::BANK_ACCOUNT_NUMBER_OFFSET, $length);
242
    }
243
244
    /**
245
     * Validate IBAN length boundaries
246
     *
247
     * @return bool
248
     */
249
    private function isLengthValid()
250
    {
251
        $countryCode = $this->getCountryCode();
252
        $validLength = static::COUNTRY_CODE_LENGTH + static::CHECKSUM_LENGTH + static::$ibanFormatMap[$countryCode][0];
253
254
        return strlen($this->iban) === $validLength;
255
    }
256
257
    /**
258
     * Validate IBAN country code
259
     *
260
     * @return bool
261
     */
262
    private function isCountryCodeValid()
263
    {
264
        $countryCode = $this->getCountryCode();
265
266
        return !(isset(static::$ibanFormatMap[$countryCode]) === false);
267
    }
268
269
    /**
270
     * Validate the IBAN format according to the country code
271
     *
272
     * @return bool
273
     */
274
    private function isFormatValid()
275
    {
276
        $countryCode = $this->getCountryCode();
277
        $accountIdentification = $this->getAccountIdentification();
278
279
        return !(preg_match('/' . static::$ibanFormatMap[$countryCode][1] . '/', $accountIdentification) !== 1);
280
    }
281
282
    /**
283
     * Validates if the checksum number is valid according to the IBAN
284
     *
285
     * @return bool
286
     */
287
    private function isChecksumValid()
288
    {
289
        $countryCode = $this->getCountryCode();
290
        $checksum = $this->getChecksum();
291
        $accountIdentification = $this->getAccountIdentification();
292
        $numericCountryCode = $this->getNumericCountryCode($countryCode);
293
        $numericAccountIdentification = $this->getNumericAccountIdentification($accountIdentification);
294
        $invertedIban = $numericAccountIdentification . $numericCountryCode . $checksum;
295
296
        return $this->bcmod($invertedIban, 97) === '1';
297
    }
298
299
    /**
300
     * Extract country code from the IBAN as numeric code
301
     *
302
     * @param $countryCode
303
     *
304
     * @return string
305
     */
306
    private function getNumericCountryCode($countryCode)
307
    {
308
        return $this->getNumericRepresentation($countryCode);
309
    }
310
311
    /**
312
     * Extract account identification from the IBAN as numeric value
313
     *
314
     * @param $accountIdentification
315
     *
316
     * @return string
317
     */
318
    private function getNumericAccountIdentification($accountIdentification)
319
    {
320
        return $this->getNumericRepresentation($accountIdentification);
321
    }
322
323
    /**
324
     * Retrieve numeric presentation of a letter part of the IBAN
325
     *
326
     * @param $letterRepresentation
327
     *
328
     * @return string
329
     */
330
    private function getNumericRepresentation($letterRepresentation)
331
    {
332
        $numericRepresentation = '';
333
334
        foreach (str_split($letterRepresentation) as $char) {
335
            $ord = ord($char);
336
            if ($ord >= 65 && $ord <= 90) {
337
                $numericRepresentation .= (string) ($ord - 55);
338
            } elseif ($ord >= 48 && $ord <= 57) {
339
                $numericRepresentation .= (string) ($ord - 48);
340
            }
341
        }
342
343
        return $numericRepresentation;
344
    }
345
346
    /**
347
     * Normailze IBAN by removing non-relevant characters and proper casing
348
     *
349
     * @param $iban
350
     *
351
     * @return mixed|string
352
     */
353
    private function normalize($iban)
354
    {
355
        return preg_replace('/[^a-z0-9]+/i', '', trim(strtoupper($iban)));
356
    }
357
358
    /**
359
     * Get modulus of an arbitrary precision number
360
     *
361
     * @param $x
362
     * @param $y
363
     *
364
     * @return string
365
     */
366
    private function bcmod($x, $y)
367
    {
368
        if (!function_exists('bcmod')) {
369
            $take = 5;
370
            $mod = '';
371
372
            do {
373
                $a = (int)$mod . substr($x, 0, $take);
374
                $x = substr($x, $take);
375
                $mod = $a % $y;
376
            } while (strlen($x));
377
378
            return (string)$mod;
379
        } else {
380
            return bcmod($x, $y);
381
        }
382
    }
383
}
384