Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
100.00% |
1 / 1 |
|
100.00% |
5 / 5 |
CRAP | |
100.00% |
43 / 43 |
CreditCard | |
100.00% |
1 / 1 |
|
100.00% |
5 / 5 |
22 | |
100.00% |
43 / 43 |
__construct | |
100.00% |
1 / 1 |
2 | |
100.00% |
2 / 2 |
|||
validate | |
100.00% |
1 / 1 |
6 | |
100.00% |
14 / 14 |
|||
getInverseError | |
100.00% |
1 / 1 |
1 | |
100.00% |
5 / 5 |
|||
validateLuhn | |
100.00% |
1 / 1 |
3 | |
100.00% |
6 / 6 |
|||
validateTypes | |
100.00% |
1 / 1 |
10 | |
100.00% |
16 / 16 |
<?php | |
namespace Verja\Validator; | |
use Verja\Error; | |
use Verja\Validator; | |
/** | |
* Class CreditCard | |
* | |
* NOTE: Diners Club enRoute can not be validated with this class. They where not issued since 1992 so no card should | |
* be valid anymore. | |
* | |
* You can extend these class and overwrite `protected static $cardTypes` to get more or different type validations. | |
* | |
* @package Verja\Validator | |
* @author Thomas Flori <thflori@gmail.com> | |
*/ | |
class CreditCard extends Validator | |
{ | |
const TYPE_VISA = 'visa'; | |
const TYPE_MASTER_CARD = 'mastercard'; | |
const TYPE_AMERICAN_EXPRESS = 'amex'; | |
const TYPE_MAESTRO = 'maestro'; | |
const TYPE_DINERSCLUB = 'dinersclub'; | |
/** @var array */ | |
protected $types = []; | |
// each card has an array of definitions | |
// each definition can be a regular expression or an array of length definition and range definition | |
// length definition can be an array or a fix length | |
/** @var array */ | |
protected static $cardTypes = [ | |
self::TYPE_VISA => ['/^4\d{12}(\d{3}){0,2}$/'], | |
self::TYPE_MASTER_CARD => [[16, [51, 55]], [16, [2221, 2720]]], | |
self::TYPE_AMERICAN_EXPRESS => ['/^3[47]\d{13}$/'], | |
self::TYPE_MAESTRO => ['/^6\d{11,18}$/', '/^50\d{10,17}$/', [[12, 19], [56, 58]]], | |
self::TYPE_DINERSCLUB => [ | |
'/^36\d{12,17}$/', '/^3095\d{12,15}$/', | |
[[16, 19], [300, 305]], [16, [54, 55]], [[16, 19], [38, 39]] | |
], | |
]; | |
/** | |
* CreditCard constructor. | |
* | |
* @param array|string $types | |
*/ | |
public function __construct($types = []) | |
{ | |
$this->types = is_string($types) ? func_get_args() : $types; | |
} | |
/** | |
* Validate $value | |
* | |
* @param mixed $value | |
* @param array $context | |
* @return bool | |
*/ | |
public function validate($value, array $context = []): bool | |
{ | |
if (!is_string($value)) { | |
return false; | |
} | |
// strip spaces | |
$number = str_replace(' ', '', $value); | |
if (!preg_match('/^\d{12,}$/', $number) || !$this->validateLuhn($number)) { | |
$this->error = new Error('NO_CREDIT_CARD', $value, 'value should be a valid credit card number'); | |
return false; | |
} | |
if (!empty($this->types) && !$this->validateTypes($number)) { | |
$this->error = new Error( | |
'WRONG_CREDIT_CARD', | |
$value, | |
sprintf('value should be a credit card of type %s', implode(' or ', $this->types)), | |
['types' => $this->types] | |
); | |
return false; | |
} | |
return true; | |
} | |
public function getInverseError($value) | |
{ | |
return new Error( | |
'CREDIT_CARD', | |
$value, | |
sprintf('value should not be a credit card of type %s', implode(' or ', $this->types)), | |
['types' => $this->types] | |
); | |
} | |
protected function validateLuhn(string $number): bool | |
{ | |
$sum = ''; | |
$revNumber = strrev($number); | |
$len = strlen($number); | |
for ($i = 0; $i < $len; $i++) { | |
$sum .= $i & 1 ? $revNumber[$i] * 2 : $revNumber[$i]; | |
} | |
return array_sum(str_split($sum)) % 10 === 0; | |
} | |
protected function validateTypes(string $number): bool | |
{ | |
foreach ($this->types as $type) { | |
// types that we can't validate are valid | |
if (!isset(static::$cardTypes[$type])) { | |
return true; | |
} | |
// validate each card definition | |
foreach (static::$cardTypes[$type] as $def) { | |
if (is_string($def) && preg_match($def, $number)) { | |
return true; | |
} elseif (is_array($def)) { | |
list($lenDef, $range) = $def; | |
// validate length | |
if (is_int($lenDef)) { | |
$lenDef = [$lenDef, $lenDef]; | |
} | |
if (!Validator::strLen($lenDef[0], $lenDef[1])->validate($number)) { | |
continue; | |
} | |
// validate prefix | |
$prefix = (int)substr($number, 0, strlen($range[0])); | |
if (Validator::between($range[0], $range[1])->validate($prefix)) { | |
return true; | |
} | |
} | |
} | |
} | |
return false; | |
} | |
} |