Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Php Credit Card Validator Laravel Package

inacho/php-credit-card-validator

A lightweight PHP package for validating credit card details. It checks card numbers with the Luhn algorithm, validates expiry dates and CVV codes, and supports common card types, making payment form validation easy in PHP or Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require inacho/php-credit-card-validator is still the whole install. No service provider, no facade alias, no config to publish — a single static class (Inacho\CreditCard\CreditCard, one file in src/) that you use at the top of whatever class needs it. That is also why it works unchanged in Laravel 5 through 11: nothing in it touches the framework.

Version 1.0.2 is the first meaningful tag in years and it changes behavior in two places that matter (Mastercard 2-series BINs, strict-numeric CVC), so pin the floor: "inacho/php-credit-card-validator": "^1.0.2". If you were already on an older 1.0.x, read CHANGELOG/the diff of src/CreditCard.php before bumping — see the CVC note in Gotchas.

First place to look is the source file itself, specifically the public card-pattern table (brand key → regex list, CVV length, and number lengths). That array is the package's only real surface area: everything else is a thin wrapper over preg_match, a Luhn checksum, and a couple of date() comparisons. Second place to look is the tests/ directory — 1.0.2 ships a unit suite, which is now the cheapest source of truth for edge cases (expiry month boundaries, CVC format, Luhn behavior on empty input) instead of guessing.

The first use case is pre-flight form validation before you hand anything to Stripe/Cashier/Adyen:

use Inacho\CreditCard;

$number = preg_replace('/\D+/', '', $request->input('card_number'));

$result = CreditCard::validCreditCard($number);
// ['valid' => true, 'type' => 'mastercard']

if ($result['valid']) {
    $brand = $result['type'];          // 'visa', 'mastercard', 'amex', ...
}

CreditCard::validCvc(trim($request->input('cvc')), $brand);            // bool, digits only
CreditCard::validDate((int) $request->input('exp_year'), (int) $request->input('exp_month')); // bool

A valid test card per brand gets you moving immediately: 4111111111111111 (visa), 5500005555555559 (mastercard), 2223003122003222 (mastercard 2-series — newly covered in 1.0.2), 378282246310005 (amex), 6011111111111117 (discover), 30569309025904 (dinersclub).

Implementation Patterns

Wrap it in a custom validation rule. Still the highest-value integration, because it keeps controllers clean and gives you one place to normalize input.

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Inacho\CreditCard;

class ValidCardNumber implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $number = preg_replace('/\D+/', '', (string) $value);

        if ($number === '' || !CreditCard::validCreditCard($number)['valid']) {
            $fail('The :attribute is not a valid card number.');
        }
    }
}

(On Laravel 8 and below, implement Illuminate\Contracts\Validation\Rule with a passes()/message() pair instead.)

Add a sibling CVC rule now that the library is strict about digits. validCvc() does the numeric check for you in 1.0.2, but trim first — you want the library rejecting 12a, not rejecting 123 for the wrong reason:

class ValidCvc implements ValidationRule
{
    public function __construct(private ?string $brand = null) {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (!CreditCard::validCvc(trim((string) $value), $this->brand)) {
            $fail('The :attribute is not a valid security code.');
        }
    }
}

Bind a thin service rather than calling static methods everywhere. The static API is untestable without hitting the real implementation, and you will eventually want to swap the validator (see the coverage note below). One interface, one implementation, one container binding:

namespace App\Support\Payments;

use Inacho\CreditCard;

class CardInspector
{
    /** @return array{brand: ?string, last4: string, valid: bool} */
    public function inspect(string $rawNumber): array
    {
        $number = preg_replace('/\D+/', '', $rawNumber);
        $result = CreditCard::validCreditCard($number);

        return [
            'valid'  => $result['valid'] === true,
            'brand'  => $result['type'] ?: null,
            'last4'  => substr($number, -4),
        ];
    }
}

Derive brand once, reuse it for CVC and UI. validCreditCard() already returns the detected type; do not call getType-style logic a second time. Pass the brand into validCvc() (Amex is 4 digits, everyone else 3) and store it next to last4 on your payment-method record so you can render the right logo without re-parsing the number.

Never persist the PAN. The natural model here is payment_methods(brand, last4, exp_month, exp_year, gateway_token). The package validates format only — it cannot tell you the card is live, funded, or not stolen. The gateway is still the source of truth; treat this as UX (instant "that doesn't look like a Visa" feedback) and request-shaping, not authorization.

Pass integers to the expiry check. 1.0.2 explicitly allows integer month/year values, so cast at the boundary rather than relying on string juggling:

$month = (int) $request->input('exp_month');
$year  = (int) $request->input('exp_year');   // normalise 2-digit -> 4-digit yourself

CreditCard::validDate($year, $month);  // year first, remember

If you want "expires this month" to behave exactly the way you expect, compare with Carbon instead:

$expiresAt = Carbon::createFromDate($year, $month, 1)->endOfMonth();
$dateOk = $expiresAt->isFuture() || $expiresAt->isToday();

Front-load normalization in the form request, not the controller. A StorePaymentMethodRequest that calls $this->merge(['card_number' => preg_replace('/\D+/', '', $this->card_number), 'cvc' => trim($this->cvc)]) before rules() runs means your rule, your brand lookup, and your last-4 extraction all see the same clean digits.

Test with the well-known fake numbers in feature tests, and add one negative case per failure mode you care about: bad Luhn (4111111111111112), wrong brand hint, 4-digit CVC on a Visa, non-numeric CVC, expired month. With 1.0.2 in place you can also copy the specific cases from the package's own tests/ into your suite, which pins the behavior you're relying on in case a future tag shifts it again.

Gotchas and Tips

validCreditCard() returns an array, not a bool. if (CreditCard::validCreditCard($n)) { ... } is always true, because a non-empty array is truthy — even ['valid' => false, 'type' => null]. Always compare $result['valid'] === true, or you will ship a validator that accepts everything. This is the single most common bug with this package, and 1.0.2 does not change it.

Normalize input yourself. The release did not add input cleaning. Do not assume the library strips spaces, dashes, or whitespace. Strip non-digits with preg_replace('/\D+/', '', $raw) before every validCreditCard() call, and be aware that Arabic-Indic or full-width digits will be silently reduced to garbage by that filter — reject non-ASCII early if your form allows it.

CVC validation is now strictly numeric — this is a behavior change. 1.0.2 rejects non-digit CVCs. If you were previously passing raw user input (untrimmed, " 123", or a value from a gateway widget that formats), those calls flip from true to false. trim() before calling and re-run your tests when bumping the minor. Also confirm the CVC is passed as a string of digits: validCvc(123, 'visa') may or may not be coerced depending on your installed revision, so cast: (string) $cvc.

Brand detection is order-dependent and regex-based. Some numbers match multiple patterns (Maestro/Mastercard overlap in particular, similarly Visa Electron/Visa). The returned type is whichever entry in the pattern table matched first, so do not build logic that assumes mutual exclusivity, and don't rely on the brand for anything security-sensitive.

The BIN table grew, but only for Mastercard. 1.0.2 adds the 2-series (2221–2720) ranges, so those cards now detect correctly instead of falling through to type === null. That does not mean full coverage: most newer UnionPay, and regional schemes like Elo, Hipercard, and Mir, are still missing or partial. Register them at runtime if they matter to you — the pattern table is exposed as a public static array — from a service provider's boot():

CreditCard::$cards['elo'] = [
    'patterns' => ['/^(4011|4312|4389|5067|5090|6277|6362|6363)/'],
    'length'   => [16],
    'cvcLength'=> [3],
    'luhn'     => true,
];

Verify the exact key names against the source in your installed version before doing this; the array shape is not a documented API and has changed between tags — check it again on every bump, since a new tag is exactly when that shape is most likely to move.

Expiry validation still has sharp edges, even with integers accepted. validDate() compares against date('Y')/date('n') in the server timezone and treats "this month" as still valid — correct for real cards, surprising in tests that run on the last day of a month. Two-digit years are a coin flip depending on your version; accept integers only after you have expanded 27 to 2027, and pass four digits.

Empty-string Luhn. Luhn implementations conventionally treat '' and '0' as passing (checksum 0). The new test suite may or may not cover it; guard $number === '' before calling validLuhn() or validCreditCard() regardless, or empty optional fields will sail through.

Maintenance risk is lower, not gone. 1.0.2 with unit tests is a real improvement over a package last tagged in 2015, but activity is still episodic and the API surface is an undocumented static array. Pin the exact constraint in composer.json, run your suite on every bump, and skim the diff. If the risk still bothers you, the alternatives remain a maintained card-validation package, Symfony's Luhn constraint, or ~40 lines of your own regex map plus a checksum loop; the total logic here is small enough that owning it is defensible.

No Laravel wiring means no facade. Don't add it to config/app.php aliases — it was never a service provider, and 1.0.2 doesn't add one. Import the FQCN, or wrap it as described above and inject your wrapper.

Debugging is still trivial, but do it in steps. When a card "should be valid" but isn't, log the brand result from validCreditCard($n)['type'] first: type === null means the regex table never matched (BIN coverage problem), while a brand with valid => false means the Luhn digit is wrong or the length doesn't match the brand's declared lengths. Those are two very different fixes. For CVC failures after upgrading, log strlen and ctype_digit of the trimmed value — nine times out of ten it's whitespace or a formatted widget value, not a genuine Amex/Visa length mismatch.

Stay out of PCI scope. Never log the raw number or CVC, never include them in exception messages, and never pass them to a queue job payload — the array return from validCreditCard() is safe to log, the input is not.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log