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

Zipcodevalidator Laravel Package

uvinum/zipcodevalidator

PHP package for validating ZIP and postal codes by country. It offers country-aware validation rules so Laravel and PHP applications can check address formats against supported regional formats, useful for checkout, registration, and other address forms.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin

composer require uvinum/zipcodevalidator

The last tagged release is from March 2017, so treat the version constraint as load-bearing: commit composer.lock, and check the require block in the package's composer.json before assuming it installs cleanly on PHP 8.x. If the constraint blocks you, prefer a fork/vendored copy over --ignore-platform-reqs so the lock file stays honest.

Where to look first

  1. src/ for the main validator class — this is your entire public surface.
  2. The bundled resource/data file (a PHP array or similar keyed by ISO 3166-1 alpha-2 country code). This is the real "feature set": if a country isn't in that map, the library has no opinion about it.
  3. The package's own tests — they double as the most reliable documentation of the call signature and expected inputs.

First use case: guard a checkout/shipping address

Wrap the library immediately rather than calling it from controllers. A thin adapter gives you one place to swap implementations later and one place to normalise input:

namespace App\Support;

use Uvinum\ZipCodeValidator\...; // confirm exact class from src/

final class PostalCodeValidator
{
    public function isValid(string $postalCode, string $countryCode): bool
    {
        $postalCode  = $this->normalise($postalCode);
        $countryCode = strtoupper(trim($countryCode));

        // Countries with no postal-code system at all.
        if (in_array($countryCode, ['IE', 'HK', 'AE', 'QA', 'LY'], true)) {
            return $postalCode === '';
        }

        try {
            return $this->validator->validate($postalCode, $countryCode);
        } catch (\Throwable $e) {
            report($e);
            return true; // fail open: never block a sale on a library error
        }
    }

    private function normalise(string $value): string
    {
        return strtoupper(preg_replace('/[\s\-]+/', '', trim($value)));
    }
}

Bind it as a singleton in a service provider if the constructor loads the full pattern table, and use it from a FormRequest or a custom rule from there.

Implementation Patterns

Custom validation rule (the pattern you'll actually use daily)

namespace App\Rules;

use App\Support\PostalCodeValidator;
use Illuminate\Contracts\Validation\Rule;

class PostalCode implements Rule
{
    public function __construct(private string $countryField) {}

    public function passes($attribute, $value): bool
    {
        $country = request()->input($this->countryField);

        if (! is_string($country) || $country === '') {
            return false; // let a required|country rule produce the real error
        }

        return app(PostalCodeValidator::class)->isValid((string) $value, $country);
    }

    public function message(): string
    {
        return 'The :attribute is not a valid postal code for the selected country.';
    }
}
// FormRequest
public function rules(): array
{
    return [
        'country'     => ['required', 'string', 'size:2', Rule::in(CountryCodes::all())],
        'postal_code' => ['required', 'string', 'max:20', new PostalCode('country')],
    ];
}

Note the ordering: put the country rule before the postal-code rule so passes() sees a valid country, and keep postal_code nullable/sometimes for the no-postal-code countries rather than inventing an empty-string special case in the rule.

Normalise on write, validate the canonical value

Pick one canonical form (uppercase, no spaces) and store that. Validation and equality both get easier, and you avoid "sw1a 1aa" vs "SW1A1AA" creating duplicate addresses. Do the normalisation in a mutator or an address value object, not in the validator, so the rule stays pure.

Value object for domain code

final class PostalAddress
{
    public function __construct(
        public readonly string $country,
        public readonly string $postalCode,
    ) {
        if (! app(PostalCodeValidator::class)->isValid($postalCode, $country)) {
            throw new InvalidPostalCode($postalCode, $country);
        }
    }
}

This keeps validation out of HTTP land and makes the check reusable in imports, seeders, and queue jobs.

Graceful degradation on the API

If you expose a "validate address" endpoint, return the library verdict as a hint, not a hard 422. Clients (mobile keyboards, autofill, carriers) produce legitimate formats the regex misses, and the data file is from 2017.

Test with a data provider, not per-country test methods

/** @dataProvider validCodes */
public function test_accepts_valid(string $zip, string $country): void
{
    $this->assertTrue(app(PostalCodeValidator::class)->isValid($zip, $country));
}

public static function validCodes(): array
{
    return [
        ['10001', 'US'], ['SW1A1AA', 'GB'], ['75008', 'FR'],
        ['10115', 'DE'], ['2000', 'AU'], ['01310-100', 'BR'],
    ];
}

Mirror it with an invalidCodes provider. These datasets are the cheapest insurance against a silent behaviour change when you fork or upgrade.

Gotchas and Tips

  • Format validation ≠ deliverability. This library answers "could this string be a postal code in country X", not "does this address exist". Don't use it for fraud scoring or carrier rating; use the carrier's own address API for that.
  • 2017 data ages. Postal-code systems change (Brazil's new ranges, UK BFPO/oversees territories, Portugal's 4-digit legacy codes). Budget for forking the pattern table and maintaining it, or fronting the library with a maintained dataset.
  • Countries without postal codes are the classic bug. Ireland, Hong Kong, UAE, Qatar, Libya, and others have no postal-code system. Depending on the data file, the library may return false, throw, or be silent — handle these explicitly before calling it, as in the adapter above.
  • UK is not GB. The library keys on ISO 3166-1 alpha-2. Validate and normalise the country code upstream (Rule::in a real ISO list, or symfony/intl); never let free-text country names reach the validator.
  • Whitespace and separators. SW1A 1AA, sw1a-1aa, and SW1A1AA should all behave the same. Strip spaces/hyphens and uppercase before validating — but only after you've confirmed your stored form matches what downstream systems (labels, carriers) expect.
  • Fail open, log loudly. Wrap the call in try/catch, report() the exception, and return a pass. A broken regex should never cost you an order; a monitoring alert should tell you it's broken.
  • Performance is fine, but don't be careless. The pattern table loads once; bind the validator as a singleton and let OPcache do its job. Avoid instantiating it inside a loop over imported rows.
  • Debugging a rejection: extract the pattern for that country from the resource file and test it directly with preg_match() in php artisan tinker. Nine times out of ten it's a normalisation problem, not a wrong pattern.
  • Error messages need care. "Invalid postal code" for a country that has no postal codes is a confusing, support-ticket-generating message. Branch your translation strings by country.
  • Extension points are limited but real. The clean seams are (a) your adapter class, and (b) the pattern data file if you fork. Wrap both behind an interface (PostalCodeValidatorInterface) so migrating to commerceguys/addressing or a carrier API later is a one-line container change.
  • No Laravel integration ships with it — no service provider, no facade, no config. That's a feature: you write ~30 lines of glue and own the behaviour, rather than inheriting someone else's abandoned config defaults.
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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core