Completed
Push — master ( 02b308...987bda )
by Darius
01:28
created

Card::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
crap 2
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)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->number_length of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->cvc_length of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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