barbieswimcrew/zip-code-validator
Validate postal and zip codes with country-specific rules. This package adds Laravel validation support for dozens of countries, using accurate regex patterns plus an extensible interface for defining your own formats.
Install with Composer:
composer require barbieswimcrew/zip-code-validator
On modern Symfony with Flex the package/bundle is auto-registered. On older skeletons, register Barbieswimcrew\ZipCodeValidator\ZipCodeValidatorBundle in app/AppKernel.php or config/bundles.php.
Look first at:
src/Constraints/ZipCode.php — available options (countryCode, message), default messages, and the validatedBy() target class.src/Constraints/ZipCodeValidator.php — the country-to-regex table and the exact supported ISO codes. This is the source of truth for what will actually validate.Minimal first use case — a German/EU address form:
use Barbieswimcrew\ZipCodeValidator\Constraints\ZipCode;
use Symfony\Component\Validator\Constraints as Assert;
class Address
{
#[Assert\NotBlank]
#[ZipCode(countryCode: 'DE')]
private string $postalCode = '';
}
For older Symfony/PHP use the annotation equivalent:
/**
* @ZipCode(countryCode="DE")
*/
private $postalCode;
Then confirm the constraint is wired up:
bin/console debug:validator 'App\Entity\Address'
The first realistic use case is a checkout/registration form where the country is fixed or selected from a short list; add the constraint to the postal-code field and let Symfony's form layer surface violations automatically.
Static country per DTO. The simplest and most robust pattern: one address class per country context, or a single class with a fixed countryCode. This avoids trying to make the constraint read a sibling country property (it cannot).
Validate arbitrary values through the validator service. When the country is dynamic, do not fight the attribute system — validate at runtime:
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Barbieswimcrew\ZipCodeValidator\Constraints\ZipCode;
final class PostalCodeChecker
{
public function __construct(private ValidatorInterface $validator) {}
public function isValid(string $postalCode, string $countryCode): bool
{
$violations = $this->validator->validate(
$postalCode,
new ZipCode(['countryCode' => strtoupper($countryCode)])
);
return count($violations) === 0;
}
}
This is the pattern to use in importers, API payload validators, and anywhere the country arrives as data rather than as a class-level constant.
Dynamic country on an object. Wrap the constraint in a custom constraint/validator that reads $object->getCountry() and delegates to ZipCodeValidator, or use a Callback constraint that instantiates ZipCode with the sibling value. Prefer the callback for small cases; prefer a dedicated constraint when the rule is reused.
Normalize before validating. Add a form data transformer or a setter that trims, uppercases, and strips/normalizes separators before validation runs. Example: "1234 ab" → "1234 AB" for NL, "sw1a 1aa" → "SW1A 1AA" for GB. Some regexes expect a space, some expect none; check the pattern in ZipCodeValidator for the target country and normalize to exactly that shape.
Combine constraints deliberately.
#[Assert\NotBlank]
#[Assert\Type('string')]
#[ZipCode(countryCode: 'US')]
private string $zip;
ZipCode only checks format; it does not imply non-blank, and many Symfony validators treat null/'' as valid. Always pair with NotBlank for required fields.
Form integration. Attach the constraint directly to the form field ('constraints' => [new ZipCode(['countryCode' => 'DE'])]) and use error_mapping when the field name differs from the entity property. Keep the constraint on the entity/DTO as well if the same object is validated outside forms.
Bulk validation. For imports, validate row DTOs with validator->validate($row) and collect violations into a row-indexed error array. Do not validate the whole collection at once if you need per-row reporting.
Testing. Use Symfony's ConstraintValidatorTestCase if you write a wrapper validator, and integration-test through Validation::createValidatorBuilder()->enableAttributeMapping()->getValidator() rather than calling the constraint validator directly. Include data providers per country, including edge cases (leading zeros, spaces, letters).
countryCode is mandatory. Omitting it causes an exception during validation, not a validation violation. That means a misconfigured constraint can produce a 500 rather than a form error. Guard dynamic code paths and add a test that asserts the exception for missing configuration.Symfony\Component\Intl\Countries::exists($code) confirms ISO validity but not that this package has a pattern; inspect the validator's table or maintain an allow-list.UK is not the official code (GB), and EL is not (GR). Lowercase input will miss the lookup table. Normalize with strtoupper() before constructing the constraint.VARCHAR, never INT. 01067, 00100, and 01234 all break as integers on the way into validation.A1A 1A1, BR uses 00000-000, US uses 00000 or 00000-0000. Check whether the shipped regex accepts the hyphen/space variants your users type; otherwise normalize or reject with a clear message.null and empty strings may pass. The validator likely returns early on empty values. Add NotBlank explicitly.$this->country; use a callback, a class-level constraint, or service-level validation instead of duplicating constraints per country.message per locale or route through your translation domain if the default English message is not acceptable. Check the constraint class for the default message key before overriding.composer why-not symfony/validator), and note whether the package uses attributes, annotations, or both. Legacy bundle registration may be required on older skeletons.How can I help you explore Laravel packages today?