Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
44 / 44 |
|
100.00% |
5 / 5 |
CRAP | |
100.00% |
1 / 1 |
CreditCard | |
100.00% |
44 / 44 |
|
100.00% |
5 / 5 |
22 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
validate | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
6 | |||
getInverseError | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
validateLuhn | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
validateTypes | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
10 |
1 | <?php |
2 | |
3 | namespace Verja\Validator; |
4 | |
5 | use Verja\Error; |
6 | use Verja\Validator; |
7 | |
8 | /** |
9 | * Class CreditCard |
10 | * |
11 | * NOTE: Diners Club enRoute can not be validated with this class. They where not issued since 1992 so no card should |
12 | * be valid anymore. |
13 | * |
14 | * You can extend these class and overwrite `protected static $cardTypes` to get more or different type validations. |
15 | * |
16 | * @package Verja\Validator |
17 | * @author Thomas Flori <thflori@gmail.com> |
18 | */ |
19 | class CreditCard extends Validator |
20 | { |
21 | const TYPE_VISA = 'visa'; |
22 | const TYPE_MASTER_CARD = 'mastercard'; |
23 | const TYPE_AMERICAN_EXPRESS = 'amex'; |
24 | const TYPE_MAESTRO = 'maestro'; |
25 | const TYPE_DINERSCLUB = 'dinersclub'; |
26 | |
27 | /** @var array */ |
28 | protected $types = []; |
29 | |
30 | // each card has an array of definitions |
31 | // each definition can be a regular expression or an array of length definition and range definition |
32 | // length definition can be an array or a fix length |
33 | /** @var array */ |
34 | protected static $cardTypes = [ |
35 | self::TYPE_VISA => ['/^4\d{12}(\d{3}){0,2}$/'], |
36 | self::TYPE_MASTER_CARD => [[16, [51, 55]], [16, [2221, 2720]]], |
37 | self::TYPE_AMERICAN_EXPRESS => ['/^3[47]\d{13}$/'], |
38 | self::TYPE_MAESTRO => ['/^6\d{11,18}$/', '/^50\d{10,17}$/', [[12, 19], [56, 58]]], |
39 | self::TYPE_DINERSCLUB => [ |
40 | '/^36\d{12,17}$/', '/^3095\d{12,15}$/', |
41 | [[16, 19], [300, 305]], [16, [54, 55]], [[16, 19], [38, 39]] |
42 | ], |
43 | ]; |
44 | |
45 | /** |
46 | * CreditCard constructor. |
47 | * |
48 | * @param array|string $types |
49 | */ |
50 | public function __construct($types = []) |
51 | { |
52 | $this->types = is_string($types) ? func_get_args() : $types; |
53 | } |
54 | |
55 | |
56 | /** |
57 | * Validate $value |
58 | * |
59 | * @param mixed $value |
60 | * @param array $context |
61 | * @return bool |
62 | */ |
63 | public function validate($value, array $context = []): bool |
64 | { |
65 | if (!is_string($value)) { |
66 | return false; |
67 | } |
68 | |
69 | // strip spaces |
70 | $number = str_replace(' ', '', $value); |
71 | |
72 | if (!preg_match('/^\d{12,}$/', $number) || !$this->validateLuhn($number)) { |
73 | $this->error = new Error('NO_CREDIT_CARD', $value, 'value should be a valid credit card number'); |
74 | return false; |
75 | } |
76 | |
77 | if (!empty($this->types) && !$this->validateTypes($number)) { |
78 | $this->error = new Error( |
79 | 'WRONG_CREDIT_CARD', |
80 | $value, |
81 | sprintf('value should be a credit card of type %s', implode(' or ', $this->types)), |
82 | ['types' => $this->types] |
83 | ); |
84 | return false; |
85 | } |
86 | |
87 | return true; |
88 | } |
89 | |
90 | public function getInverseError($value) |
91 | { |
92 | return new Error( |
93 | 'CREDIT_CARD', |
94 | $value, |
95 | sprintf('value should not be a credit card of type %s', implode(' or ', $this->types)), |
96 | ['types' => $this->types] |
97 | ); |
98 | } |
99 | |
100 | protected function validateLuhn(string $number): bool |
101 | { |
102 | $sum = ''; |
103 | $revNumber = strrev($number); |
104 | $len = strlen($number); |
105 | |
106 | for ($i = 0; $i < $len; $i++) { |
107 | $sum .= $i & 1 ? $revNumber[$i] * 2 : $revNumber[$i]; |
108 | } |
109 | |
110 | return array_sum(str_split($sum)) % 10 === 0; |
111 | } |
112 | |
113 | protected function validateTypes(string $number): bool |
114 | { |
115 | foreach ($this->types as $type) { |
116 | // types that we can't validate are valid |
117 | if (!isset(static::$cardTypes[$type])) { |
118 | return true; |
119 | } |
120 | |
121 | // validate each card definition |
122 | foreach (static::$cardTypes[$type] as $def) { |
123 | if (is_string($def) && preg_match($def, $number)) { |
124 | return true; |
125 | } elseif (is_array($def)) { |
126 | list($lenDef, $range) = $def; |
127 | |
128 | // validate length |
129 | if (is_int($lenDef)) { |
130 | $lenDef = [$lenDef, $lenDef]; |
131 | } |
132 | if (!Validator::strLen($lenDef[0], $lenDef[1])->validate($number)) { |
133 | continue; |
134 | } |
135 | |
136 | // validate prefix |
137 | $prefix = (int)substr($number, 0, strlen($range[0])); |
138 | if (Validator::between($range[0], $range[1])->validate($prefix)) { |
139 | return true; |
140 | } |
141 | } |
142 | } |
143 | } |
144 | |
145 | return false; |
146 | } |
147 | } |