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

IBAN::__toString()   A

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
c 0
b 0
f 0
rs 10
cc 1
eloc 2
nc 1
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
     *
159
     * @throws InvalidChecksum
160
     * @throws InvalidCountry
161
     * @throws InvalidFormat
162
     * @throws InvalidLength
163
     */
164
    public function check()
165
    {
166
        if (!$this->isCountryCodeValid()) {
167
            throw new InvalidCountry("IBAN ({$this->iban}) country code not valid or not supported");
168
        }
169
170
        if (!$this->isLengthValid()) {
171
            throw new InvalidLength("IBAN ({$this->iban}) length is invalid");
172
        }
173
174
        if (!$this->isFormatValid()) {
175
            throw new InvalidFormat("IBAN ({$this->iban}) format is invalid");
176
        }
177
178
        if (!$this->isChecksumValid()) {
179
            throw new InvalidChecksum("IBAN ({$this->iban}) checksum is invalid");
180
        }
181
    }
182
183
    /**
184
     * Validates the supplied IBAN and provides passthrough failure message when validation fails
185
     *
186
     * @param null $error passthrough variable
187
     *
188
     * @return bool
189
     */
190
    public function validate(&$error = null)
191
    {
192
        if (!$this->isCountryCodeValid()) {
193
            $error = 'IBAN country code not valid or not supported';
194
        } elseif (!$this->isLengthValid()) {
195
            $error = 'IBAN length is invalid';
196
        } elseif (!$this->isFormatValid()) {
197
            $error = 'IBAN format is invalid';
198
        } elseif (!$this->isChecksumValid()) {
199
            $error = 'IBAN checksum is invalid';
200
        } else {
201
            $error = '';
202
            return true;
203
        }
204
205
        return false;
206
    }
207
208
    /**
209
     * @return string
210
     */
211
    public function __toString()
212
    {
213
        return $this->format();
214
    }
215
216
    /**
217
     * Pretty print IBAN
218
     *
219
     * @return string
220
     */
221
    public function format()
222
    {
223
        return sprintf(
224
            '%s %s %s %s %s',
225
            $this->getCountryCode() . $this->getChecksum(),
226
            substr($this->getInstituteIdentification(), 0, 4),
227
            substr($this->getBankAccountNumber(), 0, 4),
228
            substr($this->getBankAccountNumber(), 4, 4),
229
            substr($this->getBankAccountNumber(), 8, 2)
230
        );
231
    }
232
233
    /**
234
     * Extract country code from IBAN
235
     *
236
     * @return string
237
     */
238
    public function getCountryCode()
239
    {
240
        return substr($this->iban, static::COUNTRY_CODE_OFFSET, static::COUNTRY_CODE_LENGTH);
241
    }
242
243
    /**
244
     * Extract checksum number from IBAN
245
     *
246
     * @return string
247
     */
248
    public function getChecksum()
249
    {
250
        return substr($this->iban, static::CHECKSUM_OFFSET, static::CHECKSUM_LENGTH);
251
    }
252
253
    /**
254
     * Extract Account Identification from IBAN
255
     *
256
     * @return string
257
     */
258
    public function getAccountIdentification()
259
    {
260
        return substr($this->iban, static::ACCOUNT_IDENTIFICATION_OFFSET);
261
    }
262
263
    /**
264
     * Extract Institute from IBAN
265
     *
266
     * @return string
267
     */
268
    public function getInstituteIdentification()
269
    {
270
        return substr($this->iban, static::INSTITUTE_IDENTIFICATION_OFFSET, static::INSTITUTE_IDENTIFICATION_LENGTH);
271
    }
272
273
    /**
274
     * Extract Bank Account number from IBAN
275
     *
276
     * @return string
277
     */
278
    public function getBankAccountNumber()
279
    {
280
        return substr($this->iban, static::BANK_ACCOUNT_NUMBER_OFFSET, static::BANK_ACCOUNT_NUMBER_LENGTH);
281
    }
282
283
284
    /**
285
     * Validate IBAN length boundaries
286
     *
287
     * @return bool
288
     */
289
    private function isLengthValid()
290
    {
291
        $countryCode = $this->getCountryCode();
292
        $validLength = static::COUNTRY_CODE_LENGTH + static::CHECKSUM_LENGTH + static::$ibanFormatMap[$countryCode][0];
293
294
        return strlen($this->iban) === $validLength;
295
    }
296
297
    /**
298
     * Validate IBAN country code
299
     *
300
     * @return bool
301
     */
302
    private function isCountryCodeValid()
303
    {
304
        $countryCode = $this->getCountryCode();
305
306
        return !(isset(static::$ibanFormatMap[$countryCode]) === false);
307
    }
308
309
    /**
310
     * Validate the IBAN format according to the country code
311
     *
312
     * @return bool
313
     */
314
    private function isFormatValid()
315
    {
316
        $countryCode = $this->getCountryCode();
317
        $accountIdentification = $this->getAccountIdentification();
318
319
        return !(preg_match('/' . static::$ibanFormatMap[$countryCode][1] . '/', $accountIdentification) !== 1);
320
    }
321
322
    /**
323
     * Validates if the checksum number is valid according to the IBAN
324
     *
325
     * @return bool
326
     */
327
    private function isChecksumValid()
328
    {
329
        $countryCode = $this->getCountryCode();
330
        $checksum = $this->getChecksum();
331
        $accountIdentification = $this->getAccountIdentification();
332
        $numericCountryCode = $this->getNumericCountryCode($countryCode);
333
        $numericAccountIdentification = $this->getNumericAccountIdentification($accountIdentification);
334
        $invertedIban = $numericAccountIdentification . $numericCountryCode . $checksum;
335
336
        return $this->bcmod($invertedIban, 97) === '1';
337
    }
338
339
    /**
340
     * Extract country code from the IBAN as numeric code
341
     *
342
     * @param $countryCode
343
     *
344
     * @return string
345
     */
346
    private function getNumericCountryCode($countryCode)
347
    {
348
        return $this->getNumericRepresentation($countryCode);
349
    }
350
351
    /**
352
     * Extract account identification from the IBAN as numeric value
353
     *
354
     * @param $accountIdentification
355
     *
356
     * @return string
357
     */
358
    private function getNumericAccountIdentification($accountIdentification)
359
    {
360
        return $this->getNumericRepresentation($accountIdentification);
361
    }
362
363
    /**
364
     * Retrieve numeric presentation of a letter part of the IBAN
365
     *
366
     * @param $letterRepresentation
367
     *
368
     * @return string
369
     */
370
    private function getNumericRepresentation($letterRepresentation)
371
    {
372
        $numericRepresentation = '';
373
374
        foreach (str_split($letterRepresentation) as $char) {
375
            $ord = ord($char);
376
            if ($ord >= 65 && $ord <= 90) {
377
                $numericRepresentation .= (string)($ord - 55);
378
            } elseif ($ord >= 48 && $ord <= 57) {
379
                $numericRepresentation .= (string)($ord - 48);
380
            }
381
        }
382
383
        return $numericRepresentation;
384
    }
385
386
    /**
387
     * Normailze IBAN by removing non-relevant characters and proper casing
388
     *
389
     * @param $iban
390
     *
391
     * @return mixed|string
392
     */
393
    private function normalize($iban)
394
    {
395
        return preg_replace('/[^a-z0-9]+/i', '', trim(strtoupper($iban)));
396
    }
397
398
    /**
399
     * Get modulus of an arbitrary precision number
400
     *
401
     * @param $x
402
     * @param $y
403
     *
404
     * @return string
405
     */
406
    private function bcmod($x, $y)
407
    {
408
        if (!function_exists('bcmod')) {
409
            $take = 5;
410
            $mod = '';
411
412
            do {
413
                $a = (int)$mod . substr($x, 0, $take);
414
                $x = substr($x, $take);
415
                $mod = $a % $y;
416
            } while (strlen($x));
417
418
            return (string)$mod;
419
        } else {
420
            return bcmod($x, $y);
421
        }
422
    }
423
}
424