Skip to content

Instantly share code, notes, and snippets.

@ArrayIterator
Last active December 28, 2022 18:20
Show Gist options
  • Select an option

  • Save ArrayIterator/342064b1355adec857df21924cf35290 to your computer and use it in GitHub Desktop.

Select an option

Save ArrayIterator/342064b1355adec857df21924cf35290 to your computer and use it in GitHub Desktop.

Revisions

  1. ArrayIterator revised this gist Dec 28, 2022. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion CreditCard.php
    Original file line number Diff line number Diff line change
    @@ -364,8 +364,8 @@ public static function generateFake(int $type = null): ?array
    $card .= $random;
    }
    $range = range(0, 9);
    shuffle($range);
    do {
    shuffle($range);
    $currentCard = $card . array_shift($range);
    } while (count($range) > 0 && false === self::calculateChecksum($currentCard));

  2. ArrayIterator revised this gist Dec 28, 2022. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion CreditCard.php
    Original file line number Diff line number Diff line change
    @@ -367,7 +367,7 @@ public static function generateFake(int $type = null): ?array
    do {
    shuffle($range);
    $currentCard = $card . array_shift($range);
    } while (false === self::calculateChecksum($currentCard));
    } while (count($range) > 0 && false === self::calculateChecksum($currentCard));

    return [
    'id' => $type,
  3. ArrayIterator revised this gist Dec 28, 2022. 1 changed file with 0 additions and 5 deletions.
    5 changes: 0 additions & 5 deletions CreditCard.php
    Original file line number Diff line number Diff line change
    @@ -8,11 +8,6 @@
    */
    class CreditCard
    {
    /**
    * Detected CCI list
    *
    * @var string
    */
    const AMERICAN_EXPRESS = 0;
    const UNIONPAY = 1;
    const DINERS_CLUB = 2;
  4. ArrayIterator created this gist Dec 28, 2022.
    384 changes: 384 additions & 0 deletions CreditCard.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,384 @@
    <?php
    declare(strict_types=1);

    namespace ArrayIterator\Generator;

    /**
    * Credit card validator & generator
    */
    class CreditCard
    {
    /**
    * Detected CCI list
    *
    * @var string
    */
    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);
    do {
    shuffle($range);
    $currentCard = $card . array_shift($range);
    } while (false === self::calculateChecksum($currentCard));

    return [
    'id' => $type,
    'name' => self::CARD_NAME[$type],
    'number' => implode('-', str_split($currentCard, 4)),
    'cvv' => mt_rand(100, 999)
    ];
    }
    }