A lightweight PHP library for validating and formatting international postal codes (ZIP/PSC).
Provides country-specific formatters, unified exception handling, and clear error codes for integration in forms, APIs, and e-commerce applications.
- ✅ Validation and normalization of postal codes for multiple countries
- ✅ Country-specific formatters (
CZ,SK,DE,GB,FR,IT,…) - ✅ Unified API via
PostcodeFormatter - ✅ Strict typing (PHP 8.1+)
- ✅ Consistent error handling with
PostcodeExceptionandPostcodeErrorCodeenum - ✅ Ready for localization (
messageKey()for translation keys) - ✅ PSR-4 autoloading
- ✅ PHPUnit test coverage
- ✅ PHPStan Level 10 + Strict Rules
- ✅ Verified with 100% PHPUnit coverage
- ✅ Includes support for all 249 ISO 3166-1 alpha-2 country codes
composer require lemonade/component_postcodeuse Lemonade\Postcode\PostcodeFormatter;
use Lemonade\Postcode\CountryCode;
use Lemonade\Postcode\Exception\PostcodeException;
use Lemonade\Postcode\FormatterRegistry;
// initialize with default formatters
$registry = new FormatterRegistry(); // IMMUTABLE REGISTRY
$formatter = new PostcodeFormatter($registry); // READONLY REFERENCE
try {
$postcode = $formatter->format(CountryCode::CZ, '12000');
// "120 00"
} catch (PostcodeException $e) {
echo $e->getValue() . ' is invalid: ' . $e->getMessage();
}You can easily extend the library with your own country-specific formatters.
Simply implement CountryPostcodeFormatter and register it via FormatterRegistry.
Example with an anonymous class:
use Lemonade\Postcode\CountryCode;
use Lemonade\Postcode\CountryPostcodeFormatter;
use Lemonade\Postcode\Exception\InvalidPostcodeException;
use Lemonade\Postcode\FormatterRegistry;
use Lemonade\Postcode\PostcodeFormatter;
$registry = new FormatterRegistry();
// Add custom formatter for Antarctica (AQ)
$registry = $registry->register(CountryCode::AQ, new class implements CountryPostcodeFormatter {
public function format(string $postcode): string
{
if (preg_match('/^[0-9]{4}$/', $postcode) !== 1) {
throw new InvalidPostcodeException($postcode);
}
return strtoupper($postcode);
}
});
$formatter = new PostcodeFormatter($registry);
try {
echo $formatter->format(CountryCode::AQ, '1231');
// outputs "1231"
} catch (InvalidPostcodeException $e) {
echo $e->getValue() . ' is invalid: ' . $e->getMessage();
}Each country has its own Formatter class under Lemonade\Postcode\Formatter.
Examples:
- CZ_Formatter –
12000→120 00 - SK_Formatter –
81101→811 01 - PL_Formatter –
01001→01-001 - GB_Formatter –
SW1A1AA→SW1A 1AA - LT_Formatter – supports both
12345andLT12345
All errors are reported using dedicated exception classes.
This makes it easy to distinguish between invalid input and unsupported countries.
-
InvalidPostcodeException
Thrown when the postcode does not match the expected format
or contains an unsupported value.
Example:"ABCDE"forCZ. -
UnknownCountryException
Thrown when trying to format a postcode for an unsupported
or unrecognized ISO 3166-1 alpha-2 country code.
Example:"XX". -
UnsupportedCountryException
Thrown when the country code is valid, but no formatter is registered for it.Example:
"AQ"(valid ISO code, but not implemented inFormatterMapper)..
All exceptions implement the PostcodeException interface, which provides:
getValue()→ returns the original input (postcodeorcountry)getCode()→ stable numeric error code (PostcodeErrorCode)getMessage()→ translation key for localization
All notable changes are documented in the CHANGELOG.md.
This package is fully covered by PHPUnit tests and verified with PHPStan Level 10.
Run PHPUnit tests:
composer install
vendor/bin/phpunit
vendor/bin/phpunit -c vendor/lemonade/component_postcode/phpunit.xml --bootstrap vendor/autoload.phpRun static analysis (PHPStan Level 10 + Strict Rules):
vendor/bin/phpstan analyse vendor/lemonade/component_postcode/src \
--configuration=vendor/lemonade/component_postcode/phpstan.neon.dist \
--memory-limit=1G