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

Laravel Postal Code Validation Laravel Package

axlon/laravel-postal-code-validation

Laravel validation rules for postal codes, powered by Google's libaddressinput. Validate against one or more ISO 3166-1 alpha-2 countries, or conditionally with other fields using the postal_code and postal_code_with rules. Simple, fluent integration.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm it's wired up.

composer require axlon/laravel-postal-code-validation

Auto-discovery registers Axlon\PostalCodeValidation\ValidationServiceProvider. There is no config file to publish, no migration, no env var. If discovery is off, add the provider to config/app.php manually — that's the only setup knob.

Where to look first. Ignore the source tree; the entire public surface is two rule names and one fluent builder. Read the README's Available rules section, then jump straight to resources/lang/{locale}/validation.php. That lang file is where you'll spend your real configuration time, because the default messages are generic.

First use case: a checkout address form. Take an existing FormRequest and replace your regex/max:10 guess with a country-aware rule:

class StoreAddressRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'country'     => ['required', 'string', 'size:2'],
            'postal_code' => ['required', 'string', 'postal_code:NL,BE,DE'],
        ];
    }
}

Then add the message once, globally:

// resources/lang/en/validation.php
'postal_code'      => 'The :attribute is not a valid postal code for :countries (e.g. :examples).',
'postal_code_with' => 'The :attribute is not a valid postal code for :countries (e.g. :examples).',

:countries and :examples are filled automatically. Ship the lang keys in the same PR as the rule — reviewers will flag validation.postal_code as missing otherwise, and the fallback message is unhelpful to end users.

Version gate before you start. The current major (v4) requires PHP 8.2+ and Laravel 12+. On Laravel 10/11 you must pin ^3.10. Check this before planning the ticket — it's the most common reason this package can't be adopted on a legacy app.

Implementation Patterns

The realistic pattern is postal_code_with, not postal_code. Almost every product lets users enter a billing and a shipping address, and only one of them may be filled. Hard-coding a country list in the rule breaks the moment you add a market:

'billing.country'    => ['required', 'string', 'size:2'],
'billing.postal_code'=> ['required', 'string', 'postal_code_with:billing.country'],

'shipping.country'    => ['nullable', 'string', 'size:2'],
'shipping.postal_code'=> ['nullable', 'string', 'postal_code_with:billing.country,shipping.country'],

The rule validates against whichever of the referenced fields is present, so the postal code is checked against the country the user actually selected — not a static list you have to maintain.

Fluent API when the country logic is dynamic. String rules can't be built from a runtime array cleanly; the builder can:

use Axlon\PostalCodeValidation\Rules\PostalCode;

$allowed = $tenant->enabledCountries(); // ['NL', 'BE']

'postal_code' => [
    'required',
    'string',
    PostalCode::for($allowed[0])->or(...array_slice($allowed, 1)),
],

For a single-country case it reads better than concatenation, and it's testable as an object rather than a magic string.

Normalise before validating, not after. Postal codes arrive with padding, lowercase letters, and full-width characters from mobile keyboards. Put the cleanup in prepareForValidation so the rule sees a canonical value and your stored value matches what you validated:

protected function prepareForValidation(): void
{
    $this->merge([
        'postal_code' => strtoupper(trim((string) $this->input('postal_code'))),
    ]);
}

Country codes themselves are case-insensitive in the rule (since v1.1.0), but normalising them too keeps your DB consistent.

Wildcards work for repeatable address blocks. A tenant onboarding flow with N warehouses:

'addresses'             => ['required', 'array'],
'addresses.*.country'   => ['required', 'string', 'size:2'],
'addresses.*.postal_code' => ['required', 'postal_code_with:addresses.*.country'],

This is the single biggest reason to use the package instead of hand-rolled regex — nested/array path resolution in postal_code_with is non-trivial to get right yourself, and the changelog shows it took the maintainer multiple patch releases (v3.1.3, v3.2.0).

Test through the framework validator. The rule is registered on Laravel's validator, so write assertions the normal way — don't instantiate the rule class:

it('rejects a Dutch postcode with letters in the digits', function () {
    $v = Validator::make(
        ['country' => 'NL', 'postal_code' => '12AB 34'],
        ['postal_code' => 'postal_code_with:country']
    );

    expect($v->fails())->toBeTrue();
});

Table-driven tests over 4–5 countries (NL, US, GB, DE, and one no-postal-code market) give you a cheap regression net when you bump the package and patterns change upstream — which they do regularly (AR, EG, PY, SV, TT, NL, BH and others have all been updated in patch releases).

Pair it with a format hint in the UI. The package doesn't expose the pattern, only "valid/invalid". If your product shows placeholder text like 1234 AB, derive it from :examples in your own message or hard-code per-locale hints — don't expect the package to give you a mask.

Gotchas and Tips

It is format validation, not address verification. The regex data comes from Google's libaddressinput, but it's compiled into the package and evaluated offline. 9999 ZZ will pass for NL if it matches the pattern even though it doesn't exist. Never let a PM write "we validate addresses" in a spec — this checks shape, nothing about deliverability, AVS, or geocoding.

Empty country arguments fail, they don't pass. Passing nothing to the rule is an error condition, not a wildcard. Guard against a tenant with an empty market list before building the rule, or you'll get a hard failure in production that local testing (with a seeded country) never reproduces.

:countries and :examples can render empty. Per the README, if no valid countries are passed the placeholders resolve to nothing. A message like "Not a valid postal code for ." looks broken; phrase your message so it degrades gracefully, or validate that the country list is non-empty first.

postal_code_with semantics are "only if at least one referenced field is present". If every referenced field is absent/null, the rule passes. That's the intent, but it means a form where the user cleared their country will silently accept a garbage postal code. Make country required alongside it unless you genuinely want the skip.

Non-string values. v4.0.0 fixed a TypeError when a postal_code_with field holds a non-string, and earlier releases repeatedly patched null handling (v1.2.1, v3.1.1, v3.1.3). Combined with nullable on the postal code field itself, null values pass. Don't rely on the rule to reject null — use required when the field is mandatory.

The v4 breaking changes will bite if you copy old snippets from blogs or Stack Overflow. All of the following are gone: the postal_code_for rule (use postal_code_with), the PostalCodes facade, custom pattern overrides, and validating outside Laravel's validator. If a tutorial shows PostalCodes::patterns() or overriding a regex, it's for 3.x and won't work.

Extension points are deliberately closed in v4. With pattern overrides and the facade removed, there is no supported way to correct a pattern or add a country's rule at runtime. If you hit a wrong pattern for a market you care about:

  1. Confirm it's actually wrong with a quick php artisan tinker Validator::make check.
  2. Check the changelog for a newer patch — pattern updates ship as patch releases and are the maintainer's normal fix path.
  3. If you still need a custom shape, write your own Rule object and use both rules in an or chain, or open an issue upstream. Don't fork silently; the data set is large and regenerated.

Debugging a failing rule. The failure message doesn't tell you which country patterns were tried or why. Validator::make in tinker with a single country argument isolates it in seconds:

Validator::make(['pc' => '1234 AB'], ['pc' => 'postal_code:NL'])->errors()->all();

If it passes for NL but fails through postal_code_with:billing.country, the bug is in your field path or a missing/empty country value — not in the pattern.

Keep the package's Laravel constraint in the upgrade plan. Every major Laravel bump has needed a corresponding package release (11, 12, 13 each arrived as a separate PR). When you schedule a framework upgrade, add this package to the checklist and confirm a compatible tag exists; otherwise your composer update fails on a transitive constraint and blocks the whole upgrade branch.

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
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow