Completed
Pull Request — master (#1)
by
unknown
01:38
created

IBAN::check()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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