Last active
December 28, 2022 18:20
-
-
Save ArrayIterator/342064b1355adec857df21924cf35290 to your computer and use it in GitHub Desktop.
PHP Credit Card Generator / Validator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace ArrayIterator\Generator; | |
/** | |
* Credit card validator & generator | |
*/ | |
class CreditCard | |
{ | |
const AMERICAN_EXPRESS = 0; | |
const UNIONPAY = 1; | |
const DINERS_CLUB = 2; | |
const DINERS_CLUB_US = 3; | |
const DISCOVER = 4; | |
const JCB = 5; | |
const LASER = 6; | |
const MAESTRO = 7; | |
const MASTERCARD = 8; | |
const SOLO = 9; | |
const VISA = 10; | |
const MIR = 11; | |
const CARD_NAME = [ | |
self::AMERICAN_EXPRESS => 'American Express', | |
self::UNIONPAY => 'Unionpay', | |
self::DINERS_CLUB => 'Diners Club', | |
self::DINERS_CLUB_US => 'Diners Club US', | |
self::DISCOVER => 'Discover', | |
self::JCB => 'JCB', | |
self::LASER => 'Laser', | |
self::MAESTRO => 'Maestro', | |
self::MASTERCARD => 'Mastercard', | |
self::SOLO => 'Solo', | |
self::VISA => 'Visa', | |
self::MIR => 'Mir', | |
]; | |
const CARD_LENGTH = [ | |
self::AMERICAN_EXPRESS => [15], | |
self::DINERS_CLUB => [14], | |
self::DINERS_CLUB_US => [16], | |
self::DISCOVER => [16, 19], | |
self::JCB => [15, 16], | |
self::LASER => [16, 17, 18, 19], | |
self::MAESTRO => [12, 13, 14, 15, 16, 17, 18, 19], | |
self::MASTERCARD => [16], | |
self::SOLO => [16, 18, 19], | |
self::UNIONPAY => [16, 17, 18, 19], | |
self::VISA => [13, 16, 19], | |
self::MIR => [13, 16], | |
]; | |
const CARD_PREFIX = [ | |
self::AMERICAN_EXPRESS => ['34', '37'], | |
self::DINERS_CLUB => ['300', '301', '302', '303', '304', '305', '36'], | |
self::DINERS_CLUB_US => ['54', '55'], | |
self::DISCOVER => [ | |
'6011', | |
'622126', | |
'622127', | |
'622128', | |
'622129', | |
'62213', | |
'62214', | |
'62215', | |
'62216', | |
'62217', | |
'62218', | |
'62219', | |
'6222', | |
'6223', | |
'6224', | |
'6225', | |
'6226', | |
'6227', | |
'6228', | |
'62290', | |
'62291', | |
'622920', | |
'622921', | |
'622922', | |
'622923', | |
'622924', | |
'622925', | |
'644', | |
'645', | |
'646', | |
'647', | |
'648', | |
'649', | |
'65', | |
], | |
self::JCB => ['1800', '2131', '3528', '3529', '353', '354', '355', '356', '357', '358'], | |
self::LASER => ['6304', '6706', '6771', '6709'], | |
self::MAESTRO => [ | |
'5018', | |
'5020', | |
'5038', | |
'6304', | |
'6759', | |
'6761', | |
'6762', | |
'6763', | |
'6764', | |
'6765', | |
'6766', | |
'6772', | |
], | |
self::MASTERCARD => [ | |
'2221', | |
'2222', | |
'2223', | |
'2224', | |
'2225', | |
'2226', | |
'2227', | |
'2228', | |
'2229', | |
'223', | |
'224', | |
'225', | |
'226', | |
'227', | |
'228', | |
'229', | |
'23', | |
'24', | |
'25', | |
'26', | |
'271', | |
'2720', | |
'51', | |
'52', | |
'53', | |
'54', | |
'55', | |
], | |
self::SOLO => ['6334', '6767'], | |
self::UNIONPAY => [ | |
'622126', | |
'622127', | |
'622128', | |
'622129', | |
'62213', | |
'62214', | |
'62215', | |
'62216', | |
'62217', | |
'62218', | |
'62219', | |
'6222', | |
'6223', | |
'6224', | |
'6225', | |
'6226', | |
'6227', | |
'6228', | |
'62290', | |
'62291', | |
'622920', | |
'622921', | |
'622922', | |
'622923', | |
'622924', | |
'622925', | |
], | |
self::VISA => ['4'], | |
self::MIR => ['2200', '2201', '2202', '2203', '2204'], | |
]; | |
private static ?array $cardPrefixes = null; | |
private static ?array $cardValidLength = null; | |
private static array $cardPrefixesLength = []; | |
/** | |
* @return int[][] | |
*/ | |
public static function getCardPrefixes() : array | |
{ | |
if (self::$cardPrefixes !== null) { | |
return self::$cardPrefixes; | |
} | |
self::$cardPrefixes = []; | |
foreach (self::CARD_PREFIX as $cardType => $prefixes) { | |
foreach ($prefixes as $prefix) { | |
self::$cardPrefixes[$prefix][] = $cardType; | |
} | |
} | |
ksort(self::$cardPrefixes); | |
self::$cardPrefixes = array_reverse(self::$cardPrefixes, true); | |
return self::$cardPrefixes; | |
} | |
/** | |
* @param int $length | |
* | |
* @return bool | |
*/ | |
public static function isValidLength(int $length): bool | |
{ | |
return isset(self::getCardLengthList()[$length]); | |
} | |
/** | |
* @return array<int[]> | |
*/ | |
private static function getCardLengthList(): array | |
{ | |
if (self::$cardValidLength === null) { | |
self::$cardValidLength = []; | |
foreach (self::CARD_LENGTH as $cardType => $lengths) { | |
foreach ($lengths as $l) { | |
self::$cardValidLength[$l][] = $cardType; | |
} | |
} | |
} | |
return self::$cardValidLength; | |
} | |
/** | |
* @param int $length | |
* | |
* @return ?array<int[]> | |
*/ | |
private static function getCardPrefixesLength(int $length): ?array | |
{ | |
if (isset(self::$cardPrefixesLength[$length])) { | |
return self::$cardPrefixesLength[$length]; | |
} | |
$identities = self::getCardLengthList()[$length]??null; | |
if (!$identities) { | |
return null; | |
} | |
self::$cardPrefixesLength[$length] = []; | |
foreach ($identities as $identity) { | |
if (!isset(self::CARD_PREFIX[$identity])) { | |
continue; | |
} | |
array_map(static function ($prefix) use ($length, $identity) { | |
// keep prefix as string | |
self::$cardPrefixesLength[$length][":$prefix"][] = $identity; | |
}, self::CARD_PREFIX[$identity]); | |
} | |
ksort(self::$cardPrefixesLength[$length]); | |
self::$cardPrefixesLength[$length] = array_reverse(self::$cardPrefixesLength[$length], true); | |
return self::$cardPrefixesLength[$length]; | |
} | |
/** | |
* @param string|int $number | |
* | |
* @return ?string | |
*/ | |
public static function checkSum(string|int $number): ?string | |
{ | |
$number = is_string($number) | |
? str_replace(['-', '.', ' '], '', $number) | |
: (string) $number; | |
// 12 -> 19 | |
if (!preg_match('~^[1-9][0-9]{11,18}$~', $number)) { | |
return null; | |
} | |
return self::calculateChecksum($number) ? $number : null; | |
} | |
/** | |
* @param string $number | |
* | |
* @return bool | |
*/ | |
private static function calculateChecksum(string $number): bool | |
{ | |
$length = strlen($number); | |
$sum = 0; | |
$weight = 2; | |
for ($i = $length - 2; $i >= 0; $i--) { | |
$digit = $weight * (int) $number[$i]; | |
$sum += floor($digit / 10) + $digit % 10; | |
$weight = $weight % 2 + 1; | |
} | |
return ((10 - $sum % 10) % 10 == $number[$length - 1]); | |
} | |
/** | |
* @param string|int $number | |
* | |
* @return ?array returning null if credit card does not recognize, | |
* and contains 2 array when card is co-branding | |
*/ | |
public static function getType(string|int $number): ?array | |
{ | |
$number = self::checkSum($number); | |
if (!$number) { | |
return null; | |
} | |
$length = strlen($number); | |
$prefixes = self::isValidLength($length) | |
? self::getCardPrefixesLength($length) | |
: null; | |
if (!$prefixes) { | |
return null; | |
} | |
$number = ":$number"; | |
foreach ($prefixes as $prefix => $identities) { | |
if (str_starts_with($number, $prefix)) { | |
$result = []; | |
foreach ($identities as $identity) { | |
$result[$identity] = [ | |
'id' => $identity, | |
'name' => self::CARD_NAME[$identity], | |
]; | |
} | |
return $result; | |
} | |
} | |
return null; | |
} | |
/** | |
* @param string|int $number | |
* | |
* @return bool | |
*/ | |
public static function isValid(string|int $number): bool | |
{ | |
return self::getType($number) !== null; | |
} | |
/** | |
* Returning null if not valid type | |
* @param ?int $type | |
* | |
* @return ?array{id:integer,name:string,number:string,cvv:integer} | |
*/ | |
public static function generateFake(int $type = null): ?array | |
{ | |
if ($type === null) { | |
$types = array_keys(self::CARD_NAME); | |
shuffle($types); | |
$type = reset($types); | |
} | |
$lengths = self::CARD_LENGTH[$type]??null; | |
$prefixes = self::CARD_PREFIX[$type]??null; | |
if (!$lengths || !$prefixes) { | |
return null; | |
} | |
$min = min($lengths); | |
$counted = 16; | |
while ($counted > $min && !in_array($counted, $lengths, true)) { | |
$counted--; | |
} | |
if ($counted <= $min) { | |
$counted = reset($lengths); | |
} | |
shuffle($prefixes); | |
$card = reset($prefixes); | |
while (strlen($card) < ($counted-1)) { | |
$random = rand(0, 9); | |
$card .= $random; | |
} | |
$range = range(0, 9); | |
shuffle($range); | |
do { | |
$currentCard = $card . array_shift($range); | |
} while (count($range) > 0 && false === self::calculateChecksum($currentCard)); | |
return [ | |
'id' => $type, | |
'name' => self::CARD_NAME[$type], | |
'number' => implode('-', str_split($currentCard, 4)), | |
'cvv' => mt_rand(100, 999) | |
]; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment