13
13
14
14
use Symfony \Component \Validator \Constraint ;
15
15
use Symfony \Component \Validator \ConstraintValidator ;
16
+ use Symfony \Component \Validator \Exception \UnexpectedTypeException ;
16
17
17
18
/**
18
19
* @author Manuel Reinhard <manu@sprain.ch>
19
20
* @author Michael Schummel
21
+ * @author Bernhard Schussek <bschussek@gmail.com>
20
22
* @link http://www.michael-schummel.de/2007/10/05/iban-prufung-mit-php/
21
23
*/
22
24
class IbanValidator extends ConstraintValidator
@@ -30,41 +32,98 @@ public function validate($value, Constraint $constraint)
30
32
return ;
31
33
}
32
34
33
- // An IBAN without a country code is not an IBAN.
34
- if (0 === preg_match ('/[A-Z]/ ' , $ value )) {
35
- $ this ->context ->addViolation ($ constraint ->message , array ('{{ value }} ' => $ value ));
35
+ if (!is_scalar ($ value ) && !(is_object ($ value ) && method_exists ($ value , '__toString ' ))) {
36
+ throw new UnexpectedTypeException ($ value , 'string ' );
37
+ }
38
+
39
+ // Remove spaces
40
+ $ canonicalized = str_replace (' ' , '' , $ value );
41
+
42
+ if (strlen ($ canonicalized ) < 4 ) {
43
+ $ this ->context ->addViolation ($ constraint ->message , array (
44
+ '{{ value }} ' => $ value ,
45
+ ));
36
46
37
47
return ;
38
48
}
39
49
40
- $ teststring = preg_replace ('/\s+/ ' , '' , $ value );
41
-
42
- if (strlen ($ teststring ) < 4 ) {
43
- $ this ->context ->addViolation ($ constraint ->message , array ('{{ value }} ' => $ value ));
50
+ // The IBAN must have at least 4 characters, start with a country
51
+ // code and contain only digits and (uppercase) characters
52
+ if (strlen ($ canonicalized ) < 4 || !ctype_upper($ canonicalized {0 })
53
+ || !ctype_upper ($ canonicalized {1 }) || !ctype_alnum ($ canonicalized )) {
54
+ $ this ->context ->addViolation ($ constraint ->message , array (
55
+ '{{ value }} ' => $ value ,
56
+ ));
44
57
45
58
return ;
46
59
}
47
60
48
- $ teststring = substr ( $ teststring , 4 )
49
- .strval( ord ( $ teststring { 0 }) - 55 )
50
- .strval( ord ( $ teststring { 1 }) - 55 )
51
- .substr ($ teststring , 2 , 2 );
61
+ // Move the first four characters to the end
62
+ // e.g. CH93 0076 2011 6238 5295 7
63
+ // -> 0076 2011 6238 5295 7 CH93
64
+ $ canonicalized = substr ( $ canonicalized , 4 ) .substr ($ canonicalized , 0 , 4 );
52
65
53
- $ teststring = preg_replace_callback ('/[A-Z]/ ' , function ($ letter ) {
54
- return intval (ord (strtolower ($ letter [0 ])) - 87 );
55
- }, $ teststring );
66
+ // Convert all remaining letters to their ordinals
67
+ // The result is an integer, which is too large for PHP's int
68
+ // data type, so we store it in a string instead.
69
+ // e.g. 0076 2011 6238 5295 7 CH93
70
+ // -> 0076 2011 6238 5295 7 121893
71
+ $ checkSum = $ this ->toBigInt ($ canonicalized );
56
72
57
- $ rest = 0 ;
58
- $ strlen = strlen ($ teststring );
59
- for ($ pos = 0 ; $ pos < $ strlen ; $ pos += 7 ) {
60
- $ part = strval ($ rest ).substr ($ teststring , $ pos , 7 );
61
- $ rest = intval ($ part ) % 97 ;
73
+ if (false === $ checkSum ) {
74
+ $ this ->context ->addViolation ($ constraint ->message , array (
75
+ '{{ value }} ' => $ value ,
76
+ ));
77
+
78
+ return ;
62
79
}
63
80
64
- if ($ rest != 1 ) {
65
- $ this ->context ->addViolation ($ constraint ->message , array ('{{ value }} ' => $ value ));
81
+ // Do a modulo-97 operation on the large integer
82
+ // We cannot use PHP's modulo operator, so we calculate the
83
+ // modulo step-wisely instead
84
+ if (1 !== $ this ->bigModulo97 ($ checkSum )) {
85
+ $ this ->context ->addViolation ($ constraint ->message , array (
86
+ '{{ value }} ' => $ value ,
87
+ ));
66
88
67
89
return ;
68
90
}
69
91
}
92
+
93
+ private function toBigInt ($ string )
94
+ {
95
+ $ chars = str_split ($ string );
96
+ $ bigInt = '' ;
97
+
98
+ foreach ($ chars as $ char ) {
99
+ // Convert uppercase characters to ordinals, starting with 10 for "A"
100
+ if (ctype_upper ($ char )) {
101
+ $ bigInt .= (ord ($ char ) - 55 );
102
+
103
+ continue ;
104
+ }
105
+
106
+ // Disallow lowercase characters
107
+ if (ctype_lower ($ char )) {
108
+ return false;
109
+ }
110
+
111
+ // Simply append digits
112
+ $ bigInt .= $ char ;
113
+ }
114
+
115
+ return $ bigInt ;
116
+ }
117
+
118
+ private function bigModulo97 ($ bigInt )
119
+ {
120
+ $ parts = str_split ($ bigInt , 7 );
121
+ $ rest = 0 ;
122
+
123
+ foreach ($ parts as $ part ) {
124
+ $ rest = ($ rest .$ part ) % 97 ;
125
+ }
126
+
127
+ return $ rest ;
128
+ }
70
129
}
0 commit comments