Card::isValidCardNumber()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 13
cts 13
cp 1
rs 9.1928
c 0
b 0
f 0
cc 5
nc 5
nop 0
crap 5
1
<?php
2
3
namespace LVR\CreditCard\Cards;
4
5
use LVR\CreditCard\Exceptions\CreditCardCharactersException;
6
use LVR\CreditCard\Exceptions\CreditCardChecksumException;
7
use LVR\CreditCard\Exceptions\CreditCardCvcException;
8
use LVR\CreditCard\Exceptions\CreditCardException;
9
use LVR\CreditCard\Exceptions\CreditCardLengthException;
10
use LVR\CreditCard\Exceptions\CreditCardNameException;
11
use LVR\CreditCard\Exceptions\CreditCardPatternException;
12
use LVR\CreditCard\Exceptions\CreditCardTypeException;
13
14
abstract class Card
15
{
16
    /**
17
     * Regular expression for card number recognition.
18
     *
19
     * @var string
20
     */
21
    public static $pattern;
22
23
    /**
24
     * Credit card type: "debit", "credit".
25
     *
26
     * @var string
27
     */
28
    protected $type;
29
30
    /**
31
     * Credit card name.
32
     *
33
     * @var string
34
     */
35
    protected $name;
36
37
    /**
38
     * Brand name.
39
     *
40
     * @var string
41
     */
42
    protected $brand;
43
44
    /**
45
     * Card number length's.
46
     *
47
     * @var array
48
     */
49
    protected $number_length;
50
51
    /**
52
     * CVC code length's.
53
     *
54
     * @var array
55
     */
56
    protected $cvc_length;
57
58
    /**
59
     * Test cvc code checksum against Luhn algorithm.
60
     *
61
     * @var bool
62
     */
63
    protected $checksum_test;
64
65
    /**
66
     * @var string|null
67
     */
68
    private $card_number;
69
70
    /**
71
     * Card constructor.
72
     *
73
     * @param string $card_number
74
     *
75
     * @throws \LVR\CreditCard\Exceptions\CreditCardException
76
     */
77 124
    public function __construct(string $card_number = '')
78
    {
79 124
        $this->checkImplementation();
80
81 119
        if ($card_number) {
82 48
            $this->setCardNumber($card_number);
83
        }
84 89
    }
85
86
    /**
87
     * @param string $card_number
88
     *
89
     * @return $this
90
     * @throws \LVR\CreditCard\Exceptions\CreditCardPatternException
91
     */
92 90
    public function setCardNumber(string $card_number)
93
    {
94 90
        $this->card_number = preg_replace('/\s+/', '', $card_number);
95
96 90
        $this->isValidCardNumber();
97
98 45
        if (! $this->validPattern()) {
99 13
            throw new CreditCardPatternException(
100 13
                sprintf('Wrong "%s" card pattern', $this->card_number)
101
            );
102
        }
103
104 32
        return $this;
105
    }
106
107
    /**
108
     * @return bool
109
     * @throws \LVR\CreditCard\Exceptions\CreditCardChecksumException
110
     * @throws \LVR\CreditCard\Exceptions\CreditCardCharactersException
111
     * @throws \LVR\CreditCard\Exceptions\CreditCardException
112
     * @throws \LVR\CreditCard\Exceptions\CreditCardLengthException
113
     */
114 104
    public function isValidCardNumber()
115
    {
116 104
        if (! $this->card_number) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->card_number of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
117 14
            throw new CreditCardException('Card number is not set');
118
        }
119
120 90
        if (! is_numeric(preg_replace('/\s+/', '', $this->card_number))) {
121 16
            throw new CreditCardCharactersException(
122 16
                sprintf('Card number "%s" contains invalid characters', $this->card_number)
123
            );
124
        }
125
126 74
        if (! $this->validLength()) {
127 27
            throw new CreditCardLengthException(
128 27
                sprintf('Incorrect "%s" card length', $this->card_number)
129
            );
130
        }
131
132 59
        if (! $this->validChecksum()) {
133 14
            throw new CreditCardChecksumException(
134 14
                sprintf('Invalid card number: "%s". Checksum is wrong', $this->card_number)
135
            );
136
        }
137
138 45
        return true;
139
    }
140
141
    /**
142
     * @return string
143
     */
144 14
    public function type()
145
    {
146 14
        return $this->type;
147
    }
148
149
    /**
150
     * @return string
151
     */
152 1
    public function name()
153
    {
154 1
        return $this->name;
155
    }
156
157
    /**
158
     * @return string
159
     */
160 1
    public function brand()
161
    {
162 1
        return $this->brand;
163
    }
164
165
    /**
166
     * @param $cvc
167
     *
168
     * @return bool
169
     */
170 1
    public function isValidCvc($cvc)
171
    {
172 1
        return is_numeric($cvc)
173 1
            && self::isValidCvcLength($cvc, $this->cvc_length);
174
    }
175
176
    /**
177
     * Check CVS length against possible lengths.
178
     *
179
     * @param string|int $cvc
180
     *
181
     * @param array $available_lengths
182
     *
183
     * @return bool
184
     */
185 2
    public static function isValidCvcLength($cvc, array $available_lengths = [3, 4])
186
    {
187
        return
188 2
            is_numeric($cvc)
189 2
            && in_array(strlen($cvc), $available_lengths, true);
190
    }
191
192
    /**
193
     * @throws \LVR\CreditCard\Exceptions\CreditCardException
194
     */
195 124
    protected function checkImplementation()
196
    {
197 124
        if (! $this->type || ! is_string($this->type) || ! in_array($this->type, ['debit', 'credit'])) {
198 1
            throw new CreditCardTypeException('Credit card type is missing');
199
        }
200
201 124
        if (! $this->name || ! is_string($this->name)) {
202 2
            throw new CreditCardNameException('Credit card name is missing or is not a string');
203
        }
204
205 123
        if (! static::$pattern || ! is_string(static::$pattern)) {
206 2
            throw new CreditCardPatternException(
207 2
                'Credit card number recognition pattern is missing or is not a string'
208
            );
209
        }
210
211 122
        if (empty($this->number_length) || ! is_array($this->number_length)) {
212 2
            throw new CreditCardLengthException(
213 2
                'Credit card number length is missing or is not an array'
214
            );
215
        }
216
217 121
        if (empty($this->cvc_length) || ! is_array($this->cvc_length)) {
218 2
            throw new CreditCardCvcException(
219 2
                'Credit card cvc code length is missing or is not an array'
220
            );
221
        }
222
223 120
        if ($this->checksum_test === null || ! is_bool($this->checksum_test)) {
224 2
            throw new CreditCardChecksumException(
225 2
                'Credit card checksum test is missing or is not a boolean'
226
            );
227
        }
228 119
    }
229
230
    /**
231
     * @return bool
232
     */
233 45
    protected function validPattern()
234
    {
235 45
        return (bool) preg_match(static::$pattern, $this->card_number);
236
    }
237
238
    /**
239
     * @return bool
240
     */
241 74
    protected function validLength()
242
    {
243 74
        return in_array(strlen($this->card_number), $this->number_length, true);
244
    }
245
246
    /**
247
     * @return bool
248
     */
249 59
    protected function validChecksum()
250
    {
251 59
        return ! $this->checksum_test || $this->checksumTest();
252
    }
253
254
    /**
255
     * @return bool
256
     */
257 56
    protected function checksumTest()
258
    {
259 56
        $checksum = 0;
260 56
        $len = strlen($this->card_number);
261 56
        for ($i = 2 - ($len % 2); $i <= $len; $i += 2) {
262 56
            $checksum += $this->card_number[$i - 1];
263
        }
264
        // Analyze odd digits in even length strings or even digits in odd length strings.
265 56
        for ($i = $len % 2 + 1; $i < $len; $i += 2) {
266 56
            $digit = $this->card_number[$i - 1] * 2;
267 56
            if ($digit < 10) {
268 56
                $checksum += $digit;
269
            } else {
270 50
                $checksum += $digit - 9;
271
            }
272
        }
273
274 56
        return ($checksum % 10) === 0;
275
    }
276
}
277