1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace LVR\CreditCard\Cards; |
4
|
|
|
|
5
|
|
|
use LVR\CreditCard\Exceptions\CreditCardException; |
6
|
|
|
use LVR\CreditCard\Exceptions\CreditCardCvcException; |
7
|
|
|
use LVR\CreditCard\Exceptions\CreditCardNameException; |
8
|
|
|
use LVR\CreditCard\Exceptions\CreditCardTypeException; |
9
|
|
|
use LVR\CreditCard\Exceptions\CreditCardLengthException; |
10
|
|
|
use LVR\CreditCard\Exceptions\CreditCardPatternException; |
11
|
|
|
use LVR\CreditCard\Exceptions\CreditCardChecksumException; |
12
|
|
|
|
13
|
|
|
abstract class Card |
14
|
|
|
{ |
15
|
|
|
/** |
16
|
|
|
* Regular expression for card number recognition. |
17
|
|
|
* |
18
|
|
|
* @var string |
19
|
|
|
*/ |
20
|
|
|
public static $pattern; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Credit card type: "debit", "credit". |
24
|
|
|
* |
25
|
|
|
* @var string |
26
|
|
|
*/ |
27
|
|
|
protected $type; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Credit card name. |
31
|
|
|
* |
32
|
|
|
* @var string |
33
|
|
|
*/ |
34
|
|
|
protected $name; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* Card number length's. |
38
|
|
|
* |
39
|
|
|
* @var array |
40
|
|
|
*/ |
41
|
|
|
protected $number_length; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* CVC code length's. |
45
|
|
|
* |
46
|
|
|
* @var array |
47
|
|
|
*/ |
48
|
|
|
protected $cvc_length; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Test cvc code checksum against Luhn algorithm. |
52
|
|
|
* |
53
|
|
|
* @var bool |
54
|
|
|
*/ |
55
|
|
|
protected $checksum_test; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* @var string |
59
|
|
|
*/ |
60
|
|
|
private $card_number; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* Card constructor. |
64
|
|
|
* |
65
|
|
|
* @param string $card_number |
66
|
|
|
* |
67
|
|
|
* @throws \LVR\CreditCard\Exceptions\CreditCardException |
68
|
|
|
*/ |
69
|
95 |
|
public function __construct(string $card_number = '') |
70
|
|
|
{ |
71
|
95 |
|
$this->checkImplementation(); |
72
|
|
|
|
73
|
90 |
|
if ($card_number) { |
74
|
34 |
|
$this->setCardNumber($card_number); |
75
|
|
|
} |
76
|
90 |
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* @param string $card_number |
80
|
|
|
* |
81
|
|
|
* @return $this |
82
|
|
|
* @throws \LVR\CreditCard\Exceptions\CreditCardPatternException |
83
|
|
|
*/ |
84
|
67 |
|
public function setCardNumber(string $card_number) |
85
|
|
|
{ |
86
|
67 |
|
$card_number = preg_replace('/[^0-9]/', '', $card_number); |
87
|
|
|
|
88
|
67 |
|
$this->card_number = $card_number; |
89
|
|
|
|
90
|
67 |
|
if (! $this->validPattern()) { |
91
|
22 |
|
throw new CreditCardPatternException( |
92
|
22 |
|
sprintf('Wrong "%s" card pattern', $this->card_number) |
93
|
|
|
); |
94
|
|
|
} |
95
|
|
|
|
96
|
45 |
|
return $this; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* @return bool |
101
|
|
|
* @throws \LVR\CreditCard\Exceptions\CreditCardChecksumException |
102
|
|
|
* @throws \LVR\CreditCard\Exceptions\CreditCardException |
103
|
|
|
* @throws \LVR\CreditCard\Exceptions\CreditCardLengthException |
104
|
|
|
*/ |
105
|
55 |
|
public function isValidCardNumber() |
106
|
|
|
{ |
107
|
55 |
|
if (! $this->card_number) { |
108
|
11 |
|
throw new CreditCardException('Card number is not set'); |
109
|
|
|
} |
110
|
|
|
|
111
|
44 |
|
if (! $this->validLength()) { |
112
|
11 |
|
throw new CreditCardLengthException( |
113
|
11 |
|
sprintf('Incorrect "%s" card length', $this->card_number) |
114
|
|
|
); |
115
|
|
|
} |
116
|
|
|
|
117
|
33 |
|
if (! $this->validChecksum()) { |
118
|
11 |
|
throw new CreditCardChecksumException( |
119
|
11 |
|
sprintf('Invalid card number: "%s". Checksum is wrong', $this->card_number) |
120
|
|
|
); |
121
|
|
|
} |
122
|
|
|
|
123
|
22 |
|
return true; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* @return string |
128
|
|
|
*/ |
129
|
11 |
|
public function type() |
130
|
|
|
{ |
131
|
11 |
|
return $this->type; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* @param $cvc |
136
|
|
|
* |
137
|
|
|
* @return bool |
138
|
|
|
*/ |
139
|
1 |
|
public function isValidCvc($cvc) |
140
|
|
|
{ |
141
|
1 |
|
return is_numeric($cvc) |
142
|
1 |
|
&& self::isValidCvcLength($cvc, $this->cvc_length); |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* Check CVS length against possible lengths. |
147
|
|
|
* |
148
|
|
|
* @param string|int $cvc |
149
|
|
|
* |
150
|
|
|
* @param array $available_lengths |
151
|
|
|
* |
152
|
|
|
* @return bool |
153
|
|
|
*/ |
154
|
2 |
|
public static function isValidCvcLength($cvc, array $available_lengths = [3, 4]) |
155
|
|
|
{ |
156
|
|
|
return |
157
|
2 |
|
is_numeric($cvc) |
158
|
2 |
|
&& in_array(strlen($cvc), $available_lengths, true); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* @param string $year |
163
|
|
|
* @param string $month |
164
|
|
|
* |
165
|
|
|
* @return bool |
166
|
|
|
*/ |
167
|
4 |
|
public static function isValidExpirationDate(string $year, string $month) |
168
|
|
|
{ |
169
|
4 |
|
if ($year == '' || $month == '') { |
170
|
1 |
|
return false; |
171
|
|
|
} |
172
|
|
|
|
173
|
4 |
|
$month = str_pad($month, 2, '0', STR_PAD_LEFT); |
174
|
|
|
|
175
|
4 |
|
if (! preg_match('/^20\d\d$/', $year)) { |
176
|
2 |
|
return false; |
177
|
|
|
} |
178
|
|
|
|
179
|
4 |
|
if (! preg_match('/^(0[1-9]|1[0-2])$/', $month)) { |
180
|
1 |
|
return false; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
// past date |
184
|
4 |
|
if ($year < date('Y') || $year == date('Y') && $month < date('m')) { |
185
|
3 |
|
return false; |
186
|
|
|
} |
187
|
|
|
|
188
|
4 |
|
return true; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* @throws \LVR\CreditCard\Exceptions\CreditCardException |
193
|
|
|
*/ |
194
|
95 |
|
protected function checkImplementation() |
195
|
|
|
{ |
196
|
95 |
|
if (! $this->type || ! is_string($this->type) || ! in_array($this->type, ['debit', 'credit'])) { |
197
|
1 |
|
throw new CreditCardTypeException('Credit card type is missing'); |
198
|
|
|
} |
199
|
|
|
|
200
|
95 |
|
if (! $this->name || ! is_string($this->name)) { |
201
|
2 |
|
throw new CreditCardNameException('Credit card name is missing or is not a string'); |
202
|
|
|
} |
203
|
|
|
|
204
|
94 |
|
if (! static::$pattern || ! is_string(static::$pattern)) { |
205
|
2 |
|
throw new CreditCardPatternException( |
206
|
2 |
|
'Credit card number recognition pattern is missing or is not a string' |
207
|
|
|
); |
208
|
|
|
} |
209
|
|
|
|
210
|
93 |
|
if (! $this->number_length || ! is_array($this->number_length)) { |
|
|
|
|
211
|
2 |
|
throw new CreditCardLengthException( |
212
|
2 |
|
'Credit card number length is missing or is not an array' |
213
|
|
|
); |
214
|
|
|
} |
215
|
|
|
|
216
|
92 |
|
if (! $this->cvc_length || ! is_array($this->cvc_length)) { |
|
|
|
|
217
|
2 |
|
throw new CreditCardCvcException( |
218
|
2 |
|
'Credit card cvc code length is missing or is not an array' |
219
|
|
|
); |
220
|
|
|
} |
221
|
|
|
|
222
|
91 |
|
if ($this->checksum_test === null || ! is_bool($this->checksum_test)) { |
223
|
2 |
|
throw new CreditCardChecksumException( |
224
|
2 |
|
'Credit card checksum test is missing or is not a boolean' |
225
|
|
|
); |
226
|
|
|
} |
227
|
90 |
|
} |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* @return bool |
231
|
|
|
*/ |
232
|
67 |
|
protected function validPattern() |
233
|
|
|
{ |
234
|
67 |
|
return ! ! preg_match(static::$pattern, $this->card_number); |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* @return bool |
239
|
|
|
*/ |
240
|
44 |
|
protected function validLength() |
241
|
|
|
{ |
242
|
44 |
|
return in_array(strlen($this->card_number), $this->number_length, true); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* @return bool |
247
|
|
|
*/ |
248
|
33 |
|
protected function validChecksum() |
249
|
|
|
{ |
250
|
33 |
|
return ! $this->checksum_test || $this->checksumTest(); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* @return bool |
255
|
|
|
*/ |
256
|
31 |
|
protected function checksumTest() |
257
|
|
|
{ |
258
|
31 |
|
$checksum = 0; |
259
|
31 |
|
$len = strlen($this->card_number); |
260
|
31 |
|
for ($i = 2 - ($len % 2); $i <= $len; $i += 2) { |
261
|
31 |
|
$checksum += $this->card_number[$i - 1]; |
262
|
|
|
} |
263
|
|
|
// Analyze odd digits in even length strings or even digits in odd length strings. |
264
|
31 |
|
for ($i = $len % 2 + 1; $i < $len; $i += 2) { |
265
|
31 |
|
$digit = $this->card_number[$i - 1] * 2; |
266
|
31 |
|
if ($digit < 10) { |
267
|
31 |
|
$checksum += $digit; |
268
|
|
|
} else { |
269
|
28 |
|
$checksum += $digit - 9; |
270
|
|
|
} |
271
|
|
|
} |
272
|
|
|
|
273
|
31 |
|
return ($checksum % 10) === 0; |
274
|
|
|
} |
275
|
|
|
} |
276
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.