Completed
Push — master ( 71a222...c936d6 )
by Darius
06:35
created

Card   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 233
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 35
lcom 1
cbo 7
dl 0
loc 233
ccs 65
cts 65
cp 1
rs 9
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A setCardNumber() 0 14 2
A isValidCardNumber() 0 20 4
A type() 0 4 1
A isValidCvc() 0 5 2
A isValidCvcLength() 0 6 2
C checkImplementation() 0 34 14
A validPattern() 0 4 1
A validLength() 0 4 1
A validChecksum() 0 4 2
A checksumTest() 0 19 4
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 21
            throw new CreditCardPatternException(
92 21
                sprintf('Wrong "%s" card pattern', $this->card_number)
93
            );
94
        }
95
96 46
        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 56
    public function isValidCardNumber()
106
    {
107 56
        if (! $this->card_number) {
108 11
            throw new CreditCardException('Card number is not set');
109
        }
110
111 45
        if (! $this->validLength()) {
112 12
            throw new CreditCardLengthException(
113 12
                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
     * @throws \LVR\CreditCard\Exceptions\CreditCardException
163
     */
164 95
    protected function checkImplementation()
165
    {
166 95
        if (! $this->type || ! is_string($this->type) || ! in_array($this->type, ['debit', 'credit'])) {
167 1
            throw new CreditCardTypeException('Credit card type is missing');
168
        }
169
170 95
        if (! $this->name || ! is_string($this->name)) {
171 2
            throw new CreditCardNameException('Credit card name is missing or is not a string');
172
        }
173
174 94
        if (! static::$pattern || ! is_string(static::$pattern)) {
175 2
            throw new CreditCardPatternException(
176 2
                'Credit card number recognition pattern is missing or is not a string'
177
            );
178
        }
179
180 93
        if (empty($this->number_length) || ! is_array($this->number_length)) {
181 2
            throw new CreditCardLengthException(
182 2
                'Credit card number length is missing or is not an array'
183
            );
184
        }
185
186 92
        if (empty($this->cvc_length) || ! is_array($this->cvc_length)) {
187 2
            throw new CreditCardCvcException(
188 2
                'Credit card cvc code length is missing or is not an array'
189
            );
190
        }
191
192 91
        if ($this->checksum_test === null || ! is_bool($this->checksum_test)) {
193 2
            throw new CreditCardChecksumException(
194 2
                'Credit card checksum test is missing or is not a boolean'
195
            );
196
        }
197 90
    }
198
199
    /**
200
     * @return bool
201
     */
202 67
    protected function validPattern()
203
    {
204 67
        return (bool) preg_match(static::$pattern, $this->card_number);
205
    }
206
207
    /**
208
     * @return bool
209
     */
210 45
    protected function validLength()
211
    {
212 45
        return in_array(strlen($this->card_number), $this->number_length, true);
213
    }
214
215
    /**
216
     * @return bool
217
     */
218 33
    protected function validChecksum()
219
    {
220 33
        return ! $this->checksum_test || $this->checksumTest();
221
    }
222
223
    /**
224
     * @return bool
225
     */
226 31
    protected function checksumTest()
227
    {
228 31
        $checksum = 0;
229 31
        $len = strlen($this->card_number);
230 31
        for ($i = 2 - ($len % 2); $i <= $len; $i += 2) {
231 31
            $checksum += $this->card_number[$i - 1];
232
        }
233
        // Analyze odd digits in even length strings or even digits in odd length strings.
234 31
        for ($i = $len % 2 + 1; $i < $len; $i += 2) {
235 31
            $digit = $this->card_number[$i - 1] * 2;
236 31
            if ($digit < 10) {
237 31
                $checksum += $digit;
238
            } else {
239 28
                $checksum += $digit - 9;
240
            }
241
        }
242
243 31
        return ($checksum % 10) === 0;
244
    }
245
}
246