Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
CreditCard
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
5 / 5
22
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 validate
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 getInverseError
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 validateLuhn
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 validateTypes
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
10
1<?php
2
3namespace Verja\Validator;
4
5use Verja\Error;
6use 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 */
19class 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}