Skip to content

Instantly share code, notes, and snippets.

@ArrayIterator
Last active December 28, 2022 18:20
Show Gist options
  • Save ArrayIterator/342064b1355adec857df21924cf35290 to your computer and use it in GitHub Desktop.
Save ArrayIterator/342064b1355adec857df21924cf35290 to your computer and use it in GitHub Desktop.
PHP Credit Card Generator / Validator
<?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