Credit Card Validator — PHP source
Validate credit card numbers with the Luhn checksum and detect the issuing brand (Visa, Mastercard, Amex, Discover, Diners, JCB, UnionPay, Maestro). 100% client-side - nothing is transmitted.
This is the PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.
<?php
/**
* credit-card-validator — PHP.
* CosmoDev polyglot showcase port of the credit-card-validator tool,
* ported from src/lib/creditCard.ts.
*
* Display source — part of CosmoDev's polyglot tool pages.
*
* Pure credit-card validation logic with no I/O: sanitize to digits, run the
* Luhn checksum, detect the issuing brand by prefix + length, and group the
* number for display. Throws nothing; mirrors the TypeScript contract.
*
* PCRE note: without the /u modifier, \d and \D are ASCII [0-9] / [^0-9],
* matching the JavaScript source exactly.
*/
declare(strict_types=1);
/**
* Luhn checksum (mod-10). Empty or non-digit input returns false.
*/
function luhn_check(string $digits): bool
{
if ($digits === '' || preg_match('/^\d*$/', $digits) !== 1) {
return false;
}
$sum = 0;
$shouldDouble = false;
// Walk right-to-left; double every second digit, collapsing >9 to its digit-sum
// (e.g. 7*2=14 -> 14-9=5, identical to summing 1+4).
for ($i = strlen($digits) - 1; $i >= 0; $i--) {
$d = ord($digits[$i]) - 48; // ord('0') === 48
if ($shouldDouble) {
$d *= 2;
if ($d > 9) {
$d -= 9;
}
}
$sum += $d;
$shouldDouble = !$shouldDouble;
}
return $sum % 10 === 0;
}
/**
* Brand detection table. ORDER MATTERS — the first prefix+length match wins.
* Maestro is last because its broad "6" / 5x ranges overlap narrower issuers.
*/
function credit_card_brand_rules(): array
{
return [
['name' => 'Visa', 're' => '/^4/', 'lengths' => [13, 16, 19]],
['name' => 'Mastercard', 're' => '/^(5[1-5]|2(2[2-9]|[3-6]\d|7[01]|720))/', 'lengths' => [16]],
['name' => 'American Express','re' => '/^3[47]/', 'lengths' => [15]],
['name' => 'Discover', 're' => '/^(6011|65|64[4-9]|622(12[6-9]|1[3-9]\d|[2-8]\d\d|9([01]\d|2[0-5])))/', 'lengths' => [16, 19]],
['name' => 'Diners Club', 're' => '/^(30[0-5]|3095|36|38|39|54|55)/', 'lengths' => [14, 16]],
['name' => 'JCB', 're' => '/^35(2[89]|[3-8]\d)/', 'lengths' => [16, 17, 18, 19]],
['name' => 'UnionPay', 're' => '/^62/', 'lengths' => [16, 17, 18, 19]],
['name' => 'Maestro', 're' => '/^(50|56|57|58|6)/', 'lengths' => [12, 13, 14, 15, 16, 17, 18, 19]],
];
}
/**
* Detect the card brand by prefix + length. Returns null when unknown.
*/
function detect_brand(string $digits): ?string
{
if ($digits === '') {
return null;
}
$len = strlen($digits);
foreach (credit_card_brand_rules() as $rule) {
if (preg_match($rule['re'], $digits) === 1 && in_array($len, $rule['lengths'], true)) {
return $rule['name'];
}
}
return null;
}
/**
* Slice digits into runs of the given widths, joining with spaces.
*/
function credit_card_group(string $digits, array $widths): string
{
$parts = [];
$i = 0;
$len = strlen($digits);
foreach ($widths as $w) {
if ($i >= $len) {
break;
}
$parts[] = substr($digits, $i, $w);
$i += $w;
}
// Any trailing remainder (e.g. a partially typed number) is appended as-is.
if ($i < $len) {
$parts[] = substr($digits, $i);
}
return implode(' ', $parts);
}
/**
* Group the number per brand spacing rules.
*/
function format_card(string $digits, ?string $brand): string
{
if ($digits === '') {
return '';
}
// Amex uses a 4-6-5 grouping; Diners Club's 14-digit variant uses 4-6-4.
if ($brand === 'American Express') {
return credit_card_group($digits, [4, 6, 5]);
}
if ($brand === 'Diners Club' && strlen($digits) === 14) {
return credit_card_group($digits, [4, 6, 4]);
}
// Default: groups of four separated by spaces; the lookahead (?=.) drops the trailing space.
return trim(preg_replace('/(.{4})(?=.)/', '$1 ', $digits) ?? '');
}
/**
* Validate a card number. Always returns an array shaped like CardInfo; never throws.
*/
function validate_card(string $input): array
{
// preg_replace returns null only on engine error; treat that as empty.
$sanitized = preg_replace('/\D+/', '', $input) ?? '';
$brand = detect_brand($sanitized);
// Luhn is only meaningful once we have a plausible card length (>= 12 digits).
$luhnValid = strlen($sanitized) >= 12 && luhn_check($sanitized);
return [
'input' => $input,
'sanitized' => $sanitized,
'valid' => $luhnValid,
'luhnValid' => $luhnValid,
'brand' => $brand,
'formatted' => format_card($sanitized, $brand),
];
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →