├── tests ├── Fixtures │ └── IsoObject.php └── Constraints │ ├── ZipCodeTest.php │ ├── PaZipCodeValidatorTest.php │ ├── NlZipCodeValidatorTest.php │ ├── ChZipCodeValidatorTest.php │ ├── EsZipCodeValidatorTest.php │ ├── CuZipCodeValidatorTest.php │ ├── NaZipCodeValidatorTest.php │ ├── LtZipCodeValidatorTest.php │ ├── MdZipCodeValidatorTest.php │ ├── JmZipCodeValidatorTest.php │ ├── ZipCodeValidatorTest.php │ └── GbZipCodeValidatorTest.php ├── .gitignore ├── phpunit.xml ├── composer.json ├── phpunit.xml.dist ├── LICENSE ├── src └── ZipCodeValidator │ └── Constraints │ ├── ZipCode.php │ └── ZipCodeValidator.php ├── .github └── workflows │ └── ci.yaml ├── CODE_OF_CONDUCT.md └── README.md /tests/Fixtures/IsoObject.php: -------------------------------------------------------------------------------- 1 | iso; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #OS generated files 2 | .DS_STORE 3 | .DS_STORE? 4 | ._* 5 | -Spotlight-V11 6 | .Trashes 7 | Icon? 8 | ethumbs.db 9 | Thumbs.db 10 | 11 | #editor related 12 | .idea 13 | 14 | #composer related 15 | composer.phar 16 | composer.lock 17 | vendor/ 18 | 19 | .phpunit.result.cache 20 | .phpunit.cache 21 | code-coverage -------------------------------------------------------------------------------- /tests/Constraints/ZipCodeTest.php: -------------------------------------------------------------------------------- 1 | expectException(MissingOptionsException::class); 14 | $constraint = new ZipCode(null); 15 | } 16 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | ./tests 12 | 13 | 14 | 15 | 16 | 17 | ./src/ 18 | 19 | 20 | ./tests 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "barbieswimcrew/zip-code-validator", 3 | "description": "Constraint class for international zipcode validation", 4 | "keywords": ["symfony", "form", "constraints", "constraint", "validator", "zip code", "zipcode", "postal code", "postalcode", "validation"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Martin Schindler" 9 | } 10 | ], 11 | "minimum-stability": "dev", 12 | "prefer-stable": true, 13 | "require": { 14 | "php": ">=8.0", 15 | "symfony/validator": ">=4.4.40" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^9.6 || ^10.5 || ^11.0.3" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "ZipCodeValidator\\": "src/ZipCodeValidator" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "ZipCodeValidator\\Tests\\": "tests/" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Tests 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ./src/ 28 | 29 | 30 | ./tests 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Martin Schindler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/ZipCodeValidator/Constraints/ZipCode.php: -------------------------------------------------------------------------------- 1 | $options 28 | ); 29 | } 30 | 31 | parent::__construct($options, $groups, $payload); 32 | 33 | if (null === $this->iso && null === $this->getter) { 34 | throw new MissingOptionsException(sprintf('Either the option "iso" or "getter" must be given for constraint %s', __CLASS__), ['iso', 'getter']); 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # .github/workflows/ci.yaml 2 | name: Code_Checks 3 | 4 | on: ["push", "pull_request"] 5 | 6 | jobs: 7 | tests: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | php: ['8.0'] 13 | stability: [ prefer-stable ] 14 | include: 15 | - php: '8.0' 16 | stability: prefer-lowest 17 | symfony-version: 4.4 18 | - php: '8.0' 19 | symfony-version: 5.4 20 | - php: '8.1' 21 | symfony-version: 5.4 22 | - php: '8.2' 23 | symfony-version: 5.4 24 | - php: '8.2' 25 | symfony-version: 6.4 26 | - php: '8.3' 27 | symfony-version: 7.0 28 | 29 | name: PHP ${{ matrix.php }} - ${{ matrix.stability }} tests 30 | steps: 31 | # basically git clone 32 | - uses: actions/checkout@v4 33 | 34 | - name: Cache dependencies 35 | uses: actions/cache@v4 36 | with: 37 | path: ~/.composer/cache/files 38 | key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 39 | 40 | # use PHP of specific version 41 | - uses: shivammathur/setup-php@v2 42 | with: 43 | php-version: ${{ matrix.php }} 44 | extensions: pcov 45 | coverage: pcov 46 | 47 | - name: Install dependencies 48 | env: 49 | SYMFONY_REQUIRE: ${{ matrix.symfony-version }} 50 | run: | 51 | composer global config --no-plugins allow-plugins.symfony/flex true 52 | composer global require --no-progress --no-scripts --no-plugins symfony/flex 53 | composer update --no-interaction --prefer-dist --optimize-autoloader 54 | 55 | - name: Execute tests 56 | run: vendor/bin/phpunit -c ./phpunit.xml.dist ./tests 57 | -------------------------------------------------------------------------------- /tests/Constraints/PaZipCodeValidatorTest.php: -------------------------------------------------------------------------------- 1 | validator = new ZipCodeValidator; 22 | } 23 | 24 | /** 25 | * This test verifies that all known Panama codes are valid. 26 | * 27 | * @dataProvider getPanamaZipCodes 28 | */ 29 | public function testZipcodes(string $zipCode): void 30 | { 31 | $constraint = new ZipCode('PA'); 32 | 33 | /** @var ExecutionContext|MockObject $contextMock */ 34 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 35 | ->disableOriginalConstructor() 36 | ->getMock(); 37 | 38 | # be sure that buildViolation never gets called 39 | $contextMock->expects($this->never())->method('buildViolation'); 40 | $contextMock->setConstraint($constraint); 41 | 42 | $this->validator->initialize($contextMock); 43 | $this->validator->validate($zipCode, $constraint); 44 | } 45 | 46 | /** 47 | * used postal codes 48 | * from https://en.wikipedia.org/wiki/Postal_codes_in_Panama 49 | */ 50 | public static function getPanamaZipCodes(): array 51 | { 52 | return [ 53 | ['0101'], 54 | ['0401'], 55 | ['0201'], 56 | ['0301'], 57 | ['0501'], 58 | ['0601'], 59 | ['0701'], 60 | ['0801'], 61 | ['1001'], 62 | ['0901'], 63 | ]; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /tests/Constraints/NlZipCodeValidatorTest.php: -------------------------------------------------------------------------------- 1 | validator = new ZipCodeValidator; 19 | } 20 | 21 | /** 22 | * @doesNotPerformAssertions 23 | */ 24 | public function testValidationOfNlZipWithIsoCode(): void 25 | { 26 | $constraint = new ZipCode('NL'); 27 | 28 | // Test some variations 29 | $this->validator->validate('1000AA', $constraint); 30 | $this->validator->validate('1000 AA', $constraint); 31 | } 32 | 33 | /** 34 | * @doesNotPerformAssertions 35 | */ 36 | public function testValidationOfNlZipWithIsoCodeAnSmallCaps(): void 37 | { 38 | $constraint = new ZipCode([ 39 | 'iso' => 'NL', 40 | 'caseSensitiveCheck' => false 41 | ]); 42 | 43 | // Test some variations 44 | $this->validator->validate('1000aa', $constraint); 45 | $this->validator->validate('1000 aa', $constraint); 46 | } 47 | 48 | public function testValidationErrorWithInvalidNlZipCode(): void 49 | { 50 | $value = "1000"; 51 | $constraint = new ZipCode('NL'); 52 | 53 | $violationBuilderMock = $this->getMockBuilder(ConstraintViolationBuilderInterface::class) 54 | ->disableOriginalConstructor() 55 | ->getMock(); 56 | $violationBuilderMock->expects($this->once())->method('setParameter')->willReturnSelf(); 57 | 58 | /** @var ExecutionContext|MockObject $contextMock */ 59 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 60 | ->disableOriginalConstructor() 61 | ->getMock(); 62 | $contextMock->expects($this->once()) 63 | ->method('buildViolation') 64 | ->with($constraint->message) 65 | ->willReturn($violationBuilderMock); 66 | 67 | $contextMock->setConstraint($constraint); 68 | $this->validator->initialize($contextMock); 69 | $this->validator->validate($value, $constraint); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Constraints/ChZipCodeValidatorTest.php: -------------------------------------------------------------------------------- 1 | validator = new ZipCodeValidator; 19 | } 20 | 21 | /** 22 | * @dataProvider chValidZipCodes 23 | */ 24 | public function testValidationOfChZipCode(string $zipCode): void 25 | { 26 | $constraint = new ZipCode('CH'); 27 | 28 | /** @var ExecutionContext|MockObject $contextMock */ 29 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 30 | ->disableOriginalConstructor() 31 | ->getMock(); 32 | 33 | //be sure that buildViolation never gets called 34 | $contextMock->expects($this->never())->method('buildViolation'); 35 | 36 | $contextMock->setConstraint($constraint); 37 | $this->validator->initialize($contextMock); 38 | 39 | // Test some variations 40 | $this->validator->validate($zipCode, $constraint); 41 | } 42 | 43 | public static function chValidZipCodes(): array 44 | { 45 | return [ 46 | ['1000'], 47 | ['3000'], 48 | ['3250'], 49 | ['9658'], 50 | ]; 51 | } 52 | 53 | /** 54 | * @dataProvider chInvalidZipCodes 55 | */ 56 | public function testValidationErrorWithInvalidChZipCode(string $zipcode): void 57 | { 58 | $constraint = new ZipCode('CH'); 59 | 60 | $violationBuilderMock = $this->getMockBuilder(ConstraintViolationBuilderInterface::class) 61 | ->disableOriginalConstructor() 62 | ->getMock(); 63 | $violationBuilderMock->expects($this->once())->method('setParameter')->willReturnSelf(); 64 | 65 | /** @var ExecutionContext|MockObject $contextMock */ 66 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 67 | ->disableOriginalConstructor() 68 | ->getMock(); 69 | $contextMock->expects($this->once()) 70 | ->method('buildViolation') 71 | ->with($constraint->message) 72 | ->willReturn($violationBuilderMock); 73 | 74 | $contextMock->setConstraint($constraint); 75 | $this->validator->initialize($contextMock); 76 | $this->validator->validate($zipcode, $constraint); 77 | } 78 | 79 | public static function chInvalidZipCodes(): array 80 | { 81 | return [ 82 | ['0000'], 83 | ['0099'], 84 | ['024567'], 85 | ['ch128'], 86 | ['2-341'], 87 | ]; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /tests/Constraints/EsZipCodeValidatorTest.php: -------------------------------------------------------------------------------- 1 | validator = new ZipCodeValidator; 19 | } 20 | 21 | /** 22 | * @dataProvider esValidZipCodes 23 | */ 24 | public function testValidationOfEsZipCode(string $zipCode): void 25 | { 26 | $constraint = new ZipCode('ES'); 27 | 28 | /** @var ExecutionContext|MockObject $contextMock */ 29 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 30 | ->disableOriginalConstructor() 31 | ->getMock(); 32 | 33 | //be sure that buildViolation never gets called 34 | $contextMock->expects($this->never())->method('buildViolation'); 35 | 36 | $contextMock->setConstraint($constraint); 37 | $this->validator->initialize($contextMock); 38 | 39 | // Test some variations 40 | $this->validator->validate($zipCode, $constraint); 41 | } 42 | 43 | public static function esValidZipCodes(): array 44 | { 45 | return [ 46 | ['08020'], 47 | ['11231'], 48 | ['52006'], 49 | ['49739'], 50 | ]; 51 | } 52 | 53 | /** 54 | * @dataProvider esInvalidZipCodes 55 | */ 56 | public function testValidationErrorWithInvalidEsZipCode(string $zipcode): void 57 | { 58 | $constraint = new ZipCode('ES'); 59 | 60 | $violationBuilderMock = $this->getMockBuilder(ConstraintViolationBuilderInterface::class) 61 | ->disableOriginalConstructor() 62 | ->getMock(); 63 | $violationBuilderMock->expects($this->once())->method('setParameter')->willReturnSelf(); 64 | 65 | /** @var ExecutionContext|MockObject $contextMock */ 66 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 67 | ->disableOriginalConstructor() 68 | ->getMock(); 69 | $contextMock->expects($this->once()) 70 | ->method('buildViolation') 71 | ->with($constraint->message) 72 | ->willReturn($violationBuilderMock); 73 | 74 | $contextMock->setConstraint($constraint); 75 | $this->validator->initialize($contextMock); 76 | $this->validator->validate($zipcode, $constraint); 77 | } 78 | 79 | public static function esInvalidZipCodes(): array 80 | { 81 | return [ 82 | ['53456'], 83 | ['0413A'], 84 | ['024567'], 85 | ['es123'], 86 | ['2341'], 87 | ]; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Constraints/CuZipCodeValidatorTest.php: -------------------------------------------------------------------------------- 1 | validator = new ZipCodeValidator; 21 | } 22 | 23 | /** 24 | * @dataProvider cuValidZipCodes 25 | */ 26 | public function testValidationOfCuZipCode(string $zipCode): void 27 | { 28 | $constraint = new ZipCode('CU'); 29 | 30 | /** @var ExecutionContext|MockObject $contextMock */ 31 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 32 | ->disableOriginalConstructor() 33 | ->getMock(); 34 | 35 | //be sure that buildViolation never gets called 36 | $contextMock->expects($this->never())->method('buildViolation'); 37 | 38 | $contextMock->setConstraint($constraint); 39 | $this->validator->initialize($contextMock); 40 | 41 | // Test some variations 42 | $this->validator->validate($zipCode, $constraint); 43 | } 44 | 45 | public static function cuValidZipCodes(): array 46 | { 47 | return [ 48 | ['11300'], 49 | ['22700'], 50 | ['33400'], 51 | ['67600'], 52 | ]; 53 | } 54 | 55 | /** 56 | * @dataProvider cuInvalidZipCodes 57 | */ 58 | public function testValidationErrorWithInvalidCuZipCode(string $zipcode): void 59 | { 60 | $constraint = new ZipCode('CU'); 61 | 62 | $violationBuilderMock = $this->getMockBuilder(ConstraintViolationBuilderInterface::class) 63 | ->disableOriginalConstructor() 64 | ->getMock(); 65 | $violationBuilderMock->expects($this->once())->method('setParameter')->willReturnSelf(); 66 | 67 | /** @var ExecutionContext|MockObject $contextMock */ 68 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 69 | ->disableOriginalConstructor() 70 | ->getMock(); 71 | $contextMock->expects($this->once()) 72 | ->method('buildViolation') 73 | ->with($constraint->message) 74 | ->willReturn($violationBuilderMock); 75 | 76 | $contextMock->setConstraint($constraint); 77 | $this->validator->initialize($contextMock); 78 | $this->validator->validate($zipcode, $constraint); 79 | } 80 | 81 | public static function cuInvalidZipCodes(): array 82 | { 83 | return [ 84 | ['1420'], 85 | ['CA123'], 86 | ['CACAC'], 87 | ['ch1289'], 88 | ['14-241'], 89 | ]; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/Constraints/NaZipCodeValidatorTest.php: -------------------------------------------------------------------------------- 1 | validator = new ZipCodeValidator; 21 | } 22 | 23 | /** 24 | * @dataProvider naValidZipCodes 25 | */ 26 | public function testValidationOfNaZipCode(string $zipCode): void 27 | { 28 | $constraint = new ZipCode('NA'); 29 | 30 | /** @var ExecutionContext|MockObject $contextMock */ 31 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 32 | ->disableOriginalConstructor() 33 | ->getMock(); 34 | 35 | //be sure that buildViolation never gets called 36 | $contextMock->expects($this->never())->method('buildViolation'); 37 | 38 | $contextMock->setConstraint($constraint); 39 | $this->validator->initialize($contextMock); 40 | 41 | // Test some variations 42 | $this->validator->validate($zipCode, $constraint); 43 | } 44 | 45 | public static function naValidZipCodes(): array 46 | { 47 | return [ 48 | ['13006'], 49 | ['22002'], 50 | ['19001'], 51 | ['10033'], 52 | ]; 53 | } 54 | 55 | /** 56 | * @dataProvider naInvalidZipCodes 57 | */ 58 | public function testValidationErrorWithInvalidNaZipCode(string $zipcode): void 59 | { 60 | $constraint = new ZipCode('NA'); 61 | 62 | $violationBuilderMock = $this->getMockBuilder(ConstraintViolationBuilderInterface::class) 63 | ->disableOriginalConstructor() 64 | ->getMock(); 65 | $violationBuilderMock->expects($this->once())->method('setParameter')->willReturnSelf(); 66 | 67 | /** @var ExecutionContext|MockObject $contextMock */ 68 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 69 | ->disableOriginalConstructor() 70 | ->getMock(); 71 | $contextMock->expects($this->once()) 72 | ->method('buildViolation') 73 | ->with($constraint->message) 74 | ->willReturn($violationBuilderMock); 75 | 76 | $contextMock->setConstraint($constraint); 77 | $this->validator->initialize($contextMock); 78 | $this->validator->validate($zipcode, $constraint); 79 | } 80 | 81 | public static function naInvalidZipCodes(): array 82 | { 83 | return [ 84 | ['12345'], 85 | ['14-20'], 86 | ['110655'], 87 | ['024567'], 88 | ['PP023'], 89 | ['AC-70'], 90 | ]; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /tests/Constraints/LtZipCodeValidatorTest.php: -------------------------------------------------------------------------------- 1 | validator = new ZipCodeValidator; 19 | } 20 | 21 | /** 22 | * @dataProvider getValidLithuanianZipCodes 23 | */ 24 | public function testValidZipcodes(string $zipCode): void 25 | { 26 | $constraint = new ZipCode('LT'); 27 | 28 | /** @var ExecutionContext|MockObject $contextMock */ 29 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 30 | ->disableOriginalConstructor() 31 | ->getMock(); 32 | 33 | # be sure that buildViolation never gets called 34 | $contextMock->expects($this->never())->method('buildViolation'); 35 | $contextMock->setConstraint($constraint); 36 | 37 | $this->validator->initialize($contextMock); 38 | $this->validator->validate($zipCode, $constraint); 39 | } 40 | 41 | /** 42 | * Valid Lithuanian postal codes are five-digit numbers, optionally prefixed with "LT-". 43 | * @see https://en.wikipedia.org/wiki/Postal_codes_in_Lithuania 44 | */ 45 | public static function getValidLithuanianZipCodes(): array 46 | { 47 | return [ 48 | ['12345'], 49 | ['01234'], 50 | ['98765'], 51 | ['LT-12345'], 52 | ['LT-01234'], 53 | ['LT-98765'], 54 | ]; 55 | } 56 | 57 | /** 58 | * @dataProvider getInvalidLithuanianZipCodes 59 | */ 60 | public function testInvalidZipcodes(string $zipCode): void 61 | { 62 | $constraint = new ZipCode('LT'); 63 | 64 | $violation = $this->createMock(ConstraintViolationBuilderInterface::class); 65 | $violation->expects($this->once())->method('setParameter')->willReturnSelf(); 66 | 67 | /** @var ExecutionContext|MockObject $contextMock */ 68 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 69 | ->disableOriginalConstructor() 70 | ->getMock(); 71 | 72 | # be sure that buildViolation never gets called 73 | $contextMock->expects($this->once())->method('buildViolation')->willReturn($violation); 74 | $contextMock->setConstraint($constraint); 75 | 76 | $this->validator->initialize($contextMock); 77 | $this->validator->validate($zipCode, $constraint); 78 | } 79 | 80 | /** 81 | * Valid Lithuanian postal codes are five-digit numbers, optionally prefixed with "LT-". 82 | * @see https://en.wikipedia.org/wiki/Postal_codes_in_Lithuania 83 | */ 84 | public static function getInvalidLithuanianZipCodes(): array 85 | { 86 | return [ 87 | ['1234'], 88 | ['LT12345'], 89 | ['LY-12345'], 90 | ['LT-1234'], 91 | ]; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/Constraints/MdZipCodeValidatorTest.php: -------------------------------------------------------------------------------- 1 | validator = new ZipCodeValidator; 19 | } 20 | 21 | /** 22 | * @dataProvider getValidMoldovaZipCodes 23 | */ 24 | public function testValidZipcodes(string $zipCode): void 25 | { 26 | $constraint = new ZipCode('MD'); 27 | 28 | /** @var ExecutionContext|MockObject $contextMock */ 29 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 30 | ->disableOriginalConstructor() 31 | ->getMock(); 32 | 33 | # be sure that buildViolation never gets called 34 | $contextMock->expects($this->never())->method('buildViolation'); 35 | $contextMock->setConstraint($constraint); 36 | 37 | $this->validator->initialize($contextMock); 38 | $this->validator->validate($zipCode, $constraint); 39 | } 40 | 41 | /** 42 | * Valid Moldova postal codes are four-digit numbers, optionally prefixed with "MD-". 43 | * @see https://en.wikipedia.org/wiki/Postal_codes_in_Moldova 44 | */ 45 | public static function getValidMoldovaZipCodes(): array 46 | { 47 | return [ 48 | ['1234'], 49 | ['0123'], 50 | ['9876'], 51 | ['MD-1234'], 52 | ['MD-0123'], 53 | ['MD-9876'], 54 | ]; 55 | } 56 | 57 | /** 58 | * @dataProvider getInvalidMoldovaZipCodes 59 | */ 60 | public function testInvalidZipcodes(string $zipCode): void 61 | { 62 | $constraint = new ZipCode('MD'); 63 | 64 | $violation = $this->createMock(ConstraintViolationBuilderInterface::class); 65 | $violation->expects($this->once())->method('setParameter')->willReturnSelf(); 66 | 67 | /** @var ExecutionContext|MockObject $contextMock */ 68 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 69 | ->disableOriginalConstructor() 70 | ->getMock(); 71 | 72 | # be sure that buildViolation never gets called 73 | $contextMock->expects($this->once())->method('buildViolation')->willReturn($violation); 74 | $contextMock->setConstraint($constraint); 75 | 76 | $this->validator->initialize($contextMock); 77 | $this->validator->validate($zipCode, $constraint); 78 | } 79 | 80 | /** 81 | * Valid Moldova postal codes are four-digit numbers, optionally prefixed with "MD-". 82 | * @see https://en.wikipedia.org/wiki/Postal_codes_in_Moldova 83 | */ 84 | public static function getInvalidMoldovaZipCodes(): array 85 | { 86 | return [ 87 | ['123'], 88 | ['12345'], 89 | ['MD12345'], 90 | ['MO-12345'], 91 | ['MD-12345'], 92 | ]; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mrtnschndlr@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /tests/Constraints/JmZipCodeValidatorTest.php: -------------------------------------------------------------------------------- 1 | validator = new ZipCodeValidator; 22 | } 23 | 24 | /** 25 | * This test verifies that all known Jamaica codes are valid. 26 | * 27 | * @dataProvider getValidJamaicaZipCodes 28 | */ 29 | public function testValidZipcodes(string $zipCode): void 30 | { 31 | $constraint = new ZipCode('JM'); 32 | 33 | /** @var ExecutionContext|MockObject $contextMock */ 34 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 35 | ->disableOriginalConstructor() 36 | ->getMock(); 37 | 38 | # be sure that buildViolation never gets called 39 | $contextMock->expects($this->never())->method('buildViolation'); 40 | $contextMock->setConstraint($constraint); 41 | 42 | $this->validator->initialize($contextMock); 43 | $this->validator->validate($zipCode, $constraint); 44 | } 45 | 46 | /** 47 | * used postal codes 48 | * from https://en.wikipedia.org/wiki/Postal_codes_in_Jamaica 49 | */ 50 | public static function getValidJamaicaZipCodes(): array 51 | { 52 | return [ 53 | ['KN'], 54 | ['AW'], 55 | ['CE'], 56 | ['TS'], 57 | ['PD'], 58 | ['MY'], 59 | ]; 60 | } 61 | 62 | /** 63 | * This test verifies that all known Jamaica codes are valid. 64 | * 65 | * @dataProvider getInvalidJamaicaZipCodes 66 | */ 67 | public function testInvalidZipcodes(string $zipCode): void 68 | { 69 | $constraint = new ZipCode('JM'); 70 | 71 | $violation = $this->createMock(ConstraintViolationBuilderInterface::class); 72 | $violation->expects($this->once())->method('setParameter')->willReturnSelf(); 73 | 74 | /** @var ExecutionContext|MockObject $contextMock */ 75 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 76 | ->disableOriginalConstructor() 77 | ->getMock(); 78 | 79 | # be sure that buildViolation never gets called 80 | $contextMock->expects($this->once())->method('buildViolation')->willReturn($violation); 81 | $contextMock->setConstraint($constraint); 82 | 83 | $this->validator->initialize($contextMock); 84 | $this->validator->validate($zipCode, $constraint); 85 | } 86 | 87 | /** 88 | * used postal codes 89 | * from https://en.wikipedia.org/wiki/Postal_codes_in_Jamaica 90 | */ 91 | public static function getInvalidJamaicaZipCodes(): array 92 | { 93 | return [ 94 | ['12'], 95 | ['\W'], 96 | ['A1'], 97 | ['0z'], 98 | ]; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Constraint Class for international Zipcode Validation 2 | 3 | [![Build Status](https://github.com/barbieswimcrew/zip-code-validator/actions/workflows/ci.yaml/badge.svg)](https://github.com/barbieswimcrew/zip-code-validator/actions/workflows/ci.yaml) 4 | [![Downloads](https://img.shields.io/packagist/dt/barbieswimcrew/zip-code-validator.svg?style=flat-square)](https://packagist.org/packages/barbieswimcrew/zip-code-validator) 5 | [![Latest stable version](https://img.shields.io/packagist/v/barbieswimcrew/zip-code-validator.svg?style=flat-square)](https://packagist.org/packages/barbieswimcrew/zip-code-validator) 6 | [![PHP from Packagist](https://img.shields.io/packagist/php-v/barbieswimcrew/zip-code-validator.svg?style=flat-square)](./composer.json) 7 | [![GitHub stars](https://img.shields.io/github/stars/barbieswimcrew/zip-code-validator.svg?style=flat-square&label=Stars&style=flat-square)](https://github.com/barbieswimcrew/zip-code-validator/stargazers) 8 | [![MIT licensed](https://img.shields.io/github/license/barbieswimcrew/zip-code-validator.svg?style=flat-square)](https://github.com/barbieswimcrew/zip-code-validator/blob/master/LICENSE) 9 | 10 | ## Installation 11 | This package uses Composer, please checkout the [composer website](https://getcomposer.org) for more information. 12 | 13 | The following command will install `zip-code-validator` into your project. It will also add a new entry in your `composer.json` and update the `composer.lock` as well. 14 | 15 | ```bash 16 | $ composer require barbieswimcrew/zip-code-validator 17 | ``` 18 | 19 | > This package follows the PSR-4 convention names for its classes, which means you can easily integrate `zip-code-validator` classes loading in your own autoloader. 20 | 21 | ## What now? 22 | For validating a zip code you need to instantiate a new ZipCode class provided by this package to set it as a constraint to your form field, for example: 23 | 24 | ```php 25 | createFormBuilder($address) 28 | ->add('zipcode', TextType::class, [ 29 | 'constraints' => [ 30 | new ZipCodeValidator\Constraints\ZipCode([ 31 | 'iso' => 'DE' 32 | ]) 33 | ] 34 | ]) 35 | ->add('save', SubmitType::class, ['label' => 'Create Task']) 36 | ->getForm(); 37 | ``` 38 | 39 | Another way would be to use the constraint as an annotation of a class property, for example: 40 | ```php 41 | 'DE']) 63 | protected $zipCode; 64 | } 65 | ``` 66 | 67 | > Please consider to inject a valid ISO 3166 2-letter country code (e.g. DE, US, FR)! 68 | 69 | > NOTE: This library validates against known zip code regex patterns and does not validate the existence of a zipcode. 70 | 71 | ### Use a getter to inject the country code dynamically 72 | 73 | If you have a form, in which the user can select a country, you may want to validate the zip code dynamically. 74 | In this case you can use the `getter` option instead: 75 | 76 | ```php 77 | country; 93 | } 94 | } 95 | ``` 96 | 97 | To disable that the validator throws an exception, when the zip code pattern is not available for a country, 98 | you can set the `strict` option to `FALSE`. 99 | 100 | ```php 101 | /** 102 | * @ZipCode(getter="getCountry", strict=false) 103 | */ 104 | protected $zipCode; 105 | ``` 106 | 107 | The validator will not validate empty strings and null values. To disallow them use the Symfony stock `NotNull` or `NotBlank` constraint in addition to `ZipCode`. 108 | 109 | ```php 110 | /** 111 | * @ZipCode(getter="getCountry") 112 | * @NotBlank 113 | */ 114 | protected $zipCode; 115 | ``` 116 | 117 | ### Case insensitive zip code matching 118 | In case you want to match the zip code in a case insensitive way you have to pass a `caseSensitiveCheck` parameter with `false` value via the constructor: 119 | ```php 120 | $constraint = new ZipCode([ 121 | 'iso' => 'GB', 122 | 'caseSensitiveCheck' => false 123 | ]); 124 | 125 | ``` 126 | By the default the library is using case sensitive zip code matching. 127 | 128 | ## Copying / License 129 | This repository is distributed under the MIT License (MIT). You can find the whole license text in the [LICENSE](LICENSE) file. 130 | -------------------------------------------------------------------------------- /tests/Constraints/ZipCodeValidatorTest.php: -------------------------------------------------------------------------------- 1 | contextMock = $this->getMockBuilder(ExecutionContext::class) 25 | ->disableOriginalConstructor() 26 | ->getMock(); 27 | $this->validator = new ZipCodeValidator; 28 | } 29 | 30 | /** 31 | * @dataProvider validZipCodes 32 | * @doesNotPerformAssertions 33 | */ 34 | public function testZipCodeValidationWithIso(string|int $isoCode, string|int|null $value): void 35 | { 36 | $constraint = new ZipCode($isoCode); 37 | $this->validator->validate($value, $constraint); 38 | } 39 | 40 | /** 41 | * @dataProvider 42 | */ 43 | public static function validZipCodes(): array 44 | { 45 | return [ 46 | ['HK', 999077], 47 | ['KE', 12345], 48 | ['MU', '15325'], 49 | ['MU', '153BU123'], 50 | ['NL', '1234AB'], 51 | ['NL', '1234 AB'], 52 | ['PN', 'PCRN 1ZZ'] 53 | ]; 54 | } 55 | 56 | public function testValidZipCodeValidationWithGetter(): void 57 | { 58 | /** @var ZipCode|MockObject $constraintMock */ 59 | $constraintMock = $this->getMockBuilder(ZipCode::class) 60 | ->disableOriginalConstructor() 61 | ->getMock(); 62 | 63 | $constraintMock->iso = null; 64 | $constraintMock->getter = 'myValidationMethod'; 65 | 66 | $gbObject = new \ZipCodeValidator\Tests\Fixtures\IsoObject('VN'); 67 | 68 | $this->contextMock = $this->getMockBuilder(ExecutionContext::class) 69 | ->disableOriginalConstructor() 70 | ->getMock(); 71 | $this->contextMock->expects($this->once()) 72 | ->method('getObject') 73 | ->willReturn($gbObject); 74 | 75 | $this->contextMock->setConstraint($constraintMock); 76 | $this->validator->initialize($this->contextMock); 77 | $this->validator->validate(123456, $constraintMock); 78 | } 79 | 80 | public function testUnexpectedTypeException(): void 81 | { 82 | $constraint = $this->getMockBuilder(Constraint::class)->disableOriginalConstructor()->getMock(); 83 | $this->expectException(UnexpectedTypeException::class); 84 | $this->validator = new ZipCodeValidator(); 85 | $this->validator->validate('FOO', $constraint); 86 | } 87 | 88 | /** 89 | * @doesNotPerformAssertions 90 | */ 91 | public function testValidationIgnoresBlankValues(): void 92 | { 93 | $constraint = new ZipCode('HK'); 94 | 95 | $this->validator->validate('', $constraint); 96 | $this->validator->validate(null, $constraint); 97 | } 98 | 99 | public function testValidationReturnsNothingOnEmptyIso(): void 100 | { 101 | /** @var ZipCode|MockObject $constraintMock */ 102 | $constraintMock = $this->getMockBuilder(ZipCode::class) 103 | ->disableOriginalConstructor() 104 | ->getMock(); 105 | 106 | $constraintMock->iso = ''; 107 | $constraintMock->getter = 'myValidationMethod'; 108 | 109 | $isoObject = new \ZipCodeValidator\Tests\Fixtures\IsoObject(''); 110 | 111 | /** @var ExecutionContext|MockObject $this->contextMock */ 112 | $this->contextMock = $this->getMockBuilder(ExecutionContext::class) 113 | ->disableOriginalConstructor() 114 | ->getMock(); 115 | $this->contextMock->expects($this->once()) 116 | ->method('getObject') 117 | ->willReturn($isoObject); 118 | 119 | $this->contextMock->setConstraint($constraintMock); 120 | $this->validator->initialize($this->contextMock); 121 | 122 | $this->validator->validate('dummy', $constraintMock); 123 | } 124 | 125 | public function testConstraintDefinitionExceptionWhenInvalidIsoInStrictMode(): void 126 | { 127 | $constraint = new ZipCode('FOO'); 128 | $this->expectException(ConstraintDefinitionException::class); 129 | $this->validator->validate('dummy', $constraint); 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /tests/Constraints/GbZipCodeValidatorTest.php: -------------------------------------------------------------------------------- 1 | validator = new ZipCodeValidator; 20 | } 21 | 22 | /** 23 | * @dataProvider GbZipCodes 24 | */ 25 | public function testValidationOfGbZipCodeWithIso($zipCode): void 26 | { 27 | $constraint = new ZipCode('GB'); 28 | 29 | /** @var ExecutionContext|MockObject $contextMock */ 30 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 31 | ->disableOriginalConstructor() 32 | ->getMock(); 33 | 34 | //be sure that buildViolation never gets called 35 | $contextMock->expects($this->never())->method('buildViolation'); 36 | 37 | $contextMock->setConstraint($constraint); 38 | $this->validator->initialize($contextMock); 39 | 40 | // Test some variations 41 | $this->validator->validate($zipCode, $constraint); 42 | } 43 | 44 | /** 45 | * @dataProvider 46 | */ 47 | public static function gbZipCodes(): array 48 | { 49 | return [ 50 | ['EC1A 1BB'], 51 | ['W1A 0AX'], 52 | ['M1 1AE'], 53 | ['B33 8TH'], 54 | ['CR2 6XH'], 55 | ['DN55 1PT'], 56 | ['BFPO 801'], 57 | ['GIR 0AA'], 58 | ['PH1 5RB'], 59 | ['CO4 3SQ'], 60 | ['CO4 3SQ'], 61 | ]; 62 | } 63 | 64 | /** 65 | * @dataProvider gbZipCodesSmallCaps 66 | */ 67 | public function testValidationOfGbZipCodeWithIsoAndSmallCaps(string $zipCode): void 68 | { 69 | $constraint = new ZipCode([ 70 | 'iso' => 'GB', 71 | 'caseSensitiveCheck' => false 72 | ]); 73 | 74 | /** @var ExecutionContext|MockObject $contextMock */ 75 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 76 | ->disableOriginalConstructor() 77 | ->getMock(); 78 | 79 | //be sure that buildViolation never gets called 80 | $contextMock->expects($this->never())->method('buildViolation'); 81 | 82 | $contextMock->setConstraint($constraint); 83 | $this->validator->initialize($contextMock); 84 | 85 | $this->validator->validate($zipCode, $constraint); 86 | } 87 | 88 | /** 89 | * @dataProvider 90 | */ 91 | public static function gbZipCodesSmallCaps(): array 92 | { 93 | return [ 94 | ['ec1a 1bb'], 95 | ['w1a 0ax'], 96 | ['m1 1ae'], 97 | ['b33 8th'], 98 | ['cr2 6xh'], 99 | ['dn55 1pt'], 100 | ['bfpo 801'], 101 | ['gir 0aa'], 102 | ['ph1 5rb'], 103 | ['co4 3sq'] 104 | ]; 105 | } 106 | 107 | public function testValidationErrorWithInvalidGbZipCode(): void 108 | { 109 | $value = 'invalid'; 110 | $constraint = new ZipCode('GB'); 111 | 112 | $violationBuilderMock = $this->getMockBuilder(ConstraintViolationBuilderInterface::class) 113 | ->disableOriginalConstructor() 114 | ->getMock(); 115 | $violationBuilderMock->expects($this->once())->method('setParameter')->willReturnSelf(); 116 | 117 | /** @var ExecutionContext|MockObject $contextMock */ 118 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 119 | ->disableOriginalConstructor() 120 | ->getMock(); 121 | $contextMock->expects($this->once()) 122 | ->method('buildViolation') 123 | ->with($constraint->message) 124 | ->willReturn($violationBuilderMock); 125 | 126 | $contextMock->setConstraint($constraint); 127 | $this->validator->initialize($contextMock); 128 | $this->validator->validate($value, $constraint); 129 | } 130 | 131 | public function testValidationErrorWithValidGbZipCodeWithExtraLeadingChars(): void 132 | { 133 | $value = 'XEC1A 1BB'; 134 | $constraint = new ZipCode('GB'); 135 | 136 | $violationBuilderMock = $this->getMockBuilder(ConstraintViolationBuilderInterface::class) 137 | ->disableOriginalConstructor() 138 | ->getMock(); 139 | $violationBuilderMock->expects($this->once())->method('setParameter')->willReturnSelf(); 140 | 141 | /** @var ExecutionContext|MockObject $contextMock */ 142 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 143 | ->disableOriginalConstructor() 144 | ->getMock(); 145 | $contextMock->expects($this->once()) 146 | ->method('buildViolation') 147 | ->with($constraint->message) 148 | ->willReturn($violationBuilderMock); 149 | 150 | $contextMock->setConstraint($constraint); 151 | $this->validator->initialize($contextMock); 152 | $this->validator->validate($value, $constraint); 153 | } 154 | 155 | public function testValidationErrorWithValidGbZipCodeWithExtraTrailingChars(): void 156 | { 157 | $value = 'EC1A 1BBX'; 158 | $constraint = new ZipCode('GB'); 159 | 160 | $violationBuilderMock = $this->getMockBuilder(ConstraintViolationBuilderInterface::class) 161 | ->disableOriginalConstructor() 162 | ->getMock(); 163 | $violationBuilderMock->expects($this->once())->method('setParameter')->willReturnSelf(); 164 | 165 | /** @var ExecutionContext|MockObject $contextMock */ 166 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 167 | ->disableOriginalConstructor() 168 | ->getMock(); 169 | $contextMock->expects($this->once()) 170 | ->method('buildViolation') 171 | ->with($constraint->message) 172 | ->willReturn($violationBuilderMock); 173 | 174 | $contextMock->setConstraint($constraint); 175 | $this->validator->initialize($contextMock); 176 | $this->validator->validate($value, $constraint); 177 | } 178 | 179 | public function testEmptyIsoWithValidZipCodeWillCallGetterMethodOnObject(): void 180 | { 181 | $value = 'EC1A 1BB'; 182 | /** @var ZipCode|MockObject $constraintMock */ 183 | $constraintMock = $this->getMockBuilder(ZipCode::class) 184 | ->disableOriginalConstructor() 185 | ->getMock(); 186 | 187 | $constraintMock->iso = null; 188 | $constraintMock->getter = 'myValidationMethod'; 189 | 190 | $gbObject = new \ZipCodeValidator\Tests\Fixtures\IsoObject('GB'); 191 | 192 | /** @var ExecutionContext|MockObject $contextMock */ 193 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 194 | ->disableOriginalConstructor() 195 | ->getMock(); 196 | $contextMock->expects($this->once()) 197 | ->method('getObject') 198 | ->willReturn($gbObject); 199 | 200 | $contextMock->setConstraint($constraintMock); 201 | $this->validator->initialize($contextMock); 202 | $this->validator->validate($value, $constraintMock); 203 | } 204 | 205 | public function testConstraintDefinitionExceptionWhenCallableMethodNotExists(): void 206 | { 207 | $value = 'EC1A 1BB'; 208 | /** @var ZipCode|MockObject $constraintMock */ 209 | $constraintMock = $this->getMockBuilder(ZipCode::class) 210 | ->disableOriginalConstructor() 211 | ->getMock(); 212 | 213 | $constraintMock->iso = null; 214 | $constraintMock->getter = 'myFooMethod'; // not existing in GbObject 215 | 216 | $gbObject = new \ZipCodeValidator\Tests\Fixtures\IsoObject('GB'); 217 | 218 | /** @var ExecutionContext|MockObject $contextMock */ 219 | $contextMock = $this->getMockBuilder(ExecutionContext::class) 220 | ->disableOriginalConstructor() 221 | ->getMock(); 222 | $contextMock->expects($this->once()) 223 | ->method('getObject') 224 | ->willReturn($gbObject); 225 | 226 | $contextMock->setConstraint($constraintMock); 227 | $this->validator->initialize($contextMock); 228 | 229 | $exceptionMessage = 'Method "myFooMethod" used as iso code getter does not exist in class ' . 230 | get_class($gbObject); 231 | 232 | $this->expectException(ConstraintDefinitionException::class); 233 | $this->expectExceptionMessage($exceptionMessage); 234 | $this->validator->validate($value, $constraintMock); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/ZipCodeValidator/Constraints/ZipCodeValidator.php: -------------------------------------------------------------------------------- 1 | 18 | * $patterns = []; 19 | * $data = json_decode(file_get_contents('http://i18napis.appspot.com/address/data'), true); 20 | * $countries = explode('~', $data['countries']); 21 | * foreach ($countries as $country) { 22 | * $data = json_decode(file_get_contents('http://i18napis.appspot.com/address/data/'.$country), true); 23 | * if (isset($data['zip'])) { 24 | * $patterns[$country] = $data['zip']; 25 | * } 26 | * } 27 | * var_export($patterns); 28 | * 29 | */ 30 | private array $patterns = [ 31 | 'AC' => 'ASCN 1ZZ', 32 | 'AD' => 'AD[1-7]0\\d', 33 | 'AF' => '\\d{4}', 34 | 'AI' => '(?:AI-)?2640', 35 | 'AL' => '\\d{4}', 36 | 'AM' => '(?:37)?\\d{4}', 37 | 'AR' => '((?:[A-HJ-NP-Z])?\\d{4})([A-Z]{3})?', 38 | 'AS' => '(96799)(?:[ \\-](\\d{4}))?', 39 | 'AT' => '\\d{4}', 40 | 'AU' => '\\d{4}', 41 | 'AX' => '22\\d{3}', 42 | 'AZ' => '\\d{4}', 43 | 'BA' => '\\d{5}', 44 | 'BB' => 'BB\\d{5}', 45 | 'BD' => '\\d{4}', 46 | 'BE' => '\\d{4}', 47 | 'BG' => '\\d{4}', 48 | 'BH' => '(?:\\d|1[0-2])\\d{2}', 49 | 'BL' => '9[78][01]\\d{2}', 50 | 'BM' => '[A-Z]{2} ?[A-Z0-9]{2}', 51 | 'BN' => '[A-Z]{2} ?\\d{4}', 52 | 'BR' => '\\d{5}-?\\d{3}', 53 | 'BT' => '\\d{5}', 54 | 'BY' => '\\d{6}', 55 | 'CA' => '[ABCEGHJKLMNPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z] ?\\d[ABCEGHJ-NPRSTV-Z]\\d', 56 | 'CC' => '6799', 57 | 'CH' => '[1-9]\\d{3}', 58 | 'CL' => '\\d{7}', 59 | 'CN' => '\\d{6}', 60 | 'CO' => '\\d{6}', 61 | 'CR' => '\\d{4,5}|\\d{3}-\\d{4}', 62 | 'CU' => '\\d{5}', 63 | 'CV' => '\\d{4}', 64 | 'CX' => '6798', 65 | 'CY' => '\\d{4}', 66 | 'CZ' => '\\d{3} ?\\d{2}', 67 | 'DE' => '\\d{5}', 68 | 'DK' => '\\d{4}', 69 | 'DO' => '\\d{5}', 70 | 'DZ' => '\\d{5}', 71 | 'EC' => '\\d{6}', 72 | 'EE' => '\\d{5}', 73 | 'EG' => '\\d{5}', 74 | 'EH' => '\\d{5}', 75 | 'ES' => '^([0-4]\\d{4}|5[0-2]\\d{3})$', 76 | 'ET' => '\\d{4}', 77 | 'FI' => '\\d{5}', 78 | 'FK' => 'FIQQ 1ZZ', 79 | 'FM' => '(9694[1-4])(?:[ \\-](\\d{4}))?', 80 | 'FO' => '\\d{3}', 81 | 'FR' => '\\d{1}(?:A|B|\\d{1}) ?\\d{3}', 82 | 'GB' => '^GIR ?0AA$|^(?:(?:AB|AL|B|BA|BB|BD|BH|BL|BN|BR|BS|BT|BX|CA|CB|CF|CH|CM|CO|CR|CT|CV|CW|DA|DD|DE|DG|DH|DL|DN|DT|DY|E|EC|EH|EN|EX|FK|FY|G|GL|GY|GU|HA|HD|HG|HP|HR|HS|HU|HX|IG|IM|IP|IV|JE|KA|KT|KW|KY|L|LA|LD|LE|LL|LN|LS|LU|M|ME|MK|ML|N|NE|NG|NN|NP|NR|NW|OL|OX|PA|PE|PH|PL|PO|PR|RG|RH|RM|S|SA|SE|SG|SK|SL|SM|SN|SO|SP|SR|SS|ST|SW|SY|TA|TD|TF|TN|TQ|TR|TS|TW|UB|W|WA|WC|WD|WF|WN|WR|WS|WV|YO|ZE)(?:\\d[\\dA-Z]? ?\\d[ABD-HJLN-UW-Z]{2}))$|^BFPO ?\\d{1,4}$', 83 | 'GE' => '\\d{4}', 84 | 'GF' => '9[78]3\\d{2}', 85 | 'GG' => 'GY\\d[\\dA-Z]? ?\\d[ABD-HJLN-UW-Z]{2}', 86 | 'GI' => 'GX11 1AA', 87 | 'GL' => '39\\d{2}', 88 | 'GN' => '\\d{3}', 89 | 'GP' => '9[78][01]\\d{2}', 90 | 'GR' => '\\d{3} ?\\d{2}', 91 | 'GS' => 'SIQQ 1ZZ', 92 | 'GT' => '\\d{5}', 93 | 'GU' => '(969(?:[12]\\d|3[12]))(?:[ \\-](\\d{4}))?', 94 | 'GW' => '\\d{4}', 95 | 'HK' => '999077', //Hong Kong 96 | 'HM' => '\\d{4}', 97 | 'HN' => '\\d{5}', 98 | 'HR' => '\\d{5}', 99 | 'HT' => '\\d{4}', 100 | 'HU' => '\\d{4}', 101 | 'ID' => '\\d{5}', 102 | 'IE' => '[\\dA-Z]{3}( ?[\\dA-Z]{4})?', 103 | 'IL' => '\\d{5}(?:\\d{2})?', 104 | 'IM' => 'IM\\d[\\dA-Z]? ?\\d[ABD-HJLN-UW-Z]{2}', 105 | 'IN' => '\\d{6}', 106 | 'IO' => 'BBND 1ZZ', 107 | 'IQ' => '\\d{5}', 108 | 'IR' => '\\d{5}-?\\d{5}', 109 | 'IS' => '\\d{3}', 110 | 'IT' => '\\d{5}', 111 | 'JE' => 'JE\\d[\\dA-Z]? ?\\d[ABD-HJLN-UW-Z]{2}', 112 | 'JM' => '[a-zA-Z]{2}', 113 | 'JO' => '\\d{5}', 114 | 'JP' => '\\d{3}-?\\d{4}', 115 | 'KE' => '\\d{5}', 116 | 'KG' => '\\d{6}', 117 | 'KH' => '\\d{5}', 118 | 'KR' => '\\d{3}(?:\\d{2}|-\\d{3})', 119 | 'KW' => '\\d{5}', 120 | 'KY' => 'KY\\d-\\d{4}', 121 | 'KZ' => '\\d{6}', 122 | 'LA' => '\\d{5}', 123 | 'LB' => '(?:\\d{4})(?: ?(?:\\d{4}))?', 124 | 'LI' => '948[5-9]|949[0-7]', 125 | 'LK' => '\\d{5}', 126 | 'LR' => '\\d{4}', 127 | 'LS' => '\\d{3}', 128 | 'LT' => '(LT-)?\\d{5}', 129 | 'LU' => '\\d{4}', 130 | 'LV' => '(LV-)?\\d{4}', 131 | 'MA' => '\\d{5}', 132 | 'MC' => '980\\d{2}', 133 | 'MD' => '(MD-)?\\d{4}', 134 | 'ME' => '8\\d{4}', 135 | 'MF' => '9[78][01]\\d{2}', 136 | 'MG' => '\\d{3}', 137 | 'MH' => '(969[67]\\d)(?:[ \\-](\\d{4}))?', 138 | 'MK' => '\\d{4}', 139 | 'MM' => '\\d{5}', 140 | 'MN' => '\\d{5}', 141 | 'MO' => '999078', //Macau 142 | 'MP' => '(9695[012])(?:[ \\-](\\d{4}))?', 143 | 'MQ' => '9[78]2\\d{2}', 144 | 'MT' => '[A-Z]{3} ?\\d{2,4}', 145 | 'MU' => '\\d{3}(?:\\d{2}|[A-Z]{2}\\d{3})', 146 | 'MV' => '\\d{5}', 147 | 'MX' => '\\d{5}', 148 | 'MY' => '\\d{5}', 149 | 'MZ' => '\\d{4}', 150 | 'NA' => '\\d{2}0\d{2}', 151 | 'NC' => '988\\d{2}', 152 | 'NE' => '\\d{4}', 153 | 'NF' => '2899', 154 | 'NG' => '\\d{6}', 155 | 'NI' => '\\d{5}', 156 | 'NL' => '[1-9]{1}\\d{3}([ ]?[A-Z]{2})', // Changed: ZipCode cannot start with 0, fixed optional letters 157 | 'NO' => '\\d{4}', 158 | 'NP' => '\\d{5}', 159 | 'NZ' => '\\d{4}', 160 | 'OM' => '(?:PC )?\\d{3}', 161 | 'PA' => '\\d{4}', 162 | 'PE' => '(?:LIMA \\d{1,2}|CALLAO 0?\\d)|[0-2]\\d{4}', 163 | 'PF' => '987\\d{2}', 164 | 'PG' => '\\d{3}', 165 | 'PH' => '\\d{4}', 166 | 'PK' => '\\d{5}', 167 | 'PL' => '\\d{2}-\\d{3}', 168 | 'PM' => '9[78]5\\d{2}', 169 | 'PN' => 'PCRN 1ZZ', 170 | 'PR' => '(00[679]\\d{2})(?:[ \\-](\\d{4}))?', 171 | 'PT' => '\\d{4}-\\d{3}', 172 | 'PW' => '(969(?:39|40))(?:[ \\-](\\d{4}))?', 173 | 'PY' => '\\d{4}', 174 | 'RE' => '9[78]4\\d{2}', 175 | 'RO' => '\\d{6}', 176 | 'RS' => '\\d{5,6}', 177 | 'RU' => '\\d{6}', 178 | 'SA' => '\\d{5}', 179 | 'SE' => '\\d{3} ?\\d{2}', 180 | 'SG' => '\\d{6}', 181 | 'SH' => '(?:ASCN|STHL) 1ZZ', 182 | 'SI' => '\\d{4}', 183 | 'SJ' => '\\d{4}', 184 | 'SK' => '\\d{3} ?\\d{2}', 185 | 'SM' => '4789\\d', 186 | 'SN' => '\\d{5}', 187 | 'SO' => '[A-Z]{2} ?\\d{5}', 188 | 'SV' => 'CP [1-3][1-7][0-2]\\d', 189 | 'SZ' => '[HLMS]\\d{3}', 190 | 'TA' => 'TDCU 1ZZ', 191 | 'TC' => 'TKCA 1ZZ', 192 | 'TH' => '\\d{5}', 193 | 'TJ' => '\\d{6}', 194 | 'TM' => '\\d{6}', 195 | 'TN' => '\\d{4}', 196 | 'TR' => '\\d{5}', 197 | 'TW' => '\\d{3}(?:\\d{2})?', 198 | 'TZ' => '\\d{4,5}', 199 | 'UA' => '\\d{5}', 200 | 'UM' => '96898', 201 | 'US' => '(\\d{5})(?:[ \\-](\\d{4}))?', 202 | 'UY' => '\\d{5}', 203 | 'UZ' => '\\d{6}', 204 | 'VA' => '00120', 205 | 'VC' => 'VC\\d{4}', 206 | 'VE' => '\\d{4}', 207 | 'VG' => 'VG\\d{4}', 208 | 'VI' => '(008(?:(?:[0-4]\\d)|(?:5[01])))(?:[ \\-](\\d{4}))?', 209 | 'VN' => '\\d{6}', 210 | 'WF' => '986\\d{2}', 211 | 'XK' => '[1-7]\\d{4}', 212 | 'YT' => '976\\d{2}', 213 | 'ZA' => '\\d{4}', 214 | 'ZM' => '\\d{5}', 215 | ]; 216 | 217 | /** 218 | * {@inheritdoc} 219 | */ 220 | public function validate($value, Constraint $constraint): void 221 | { 222 | if (!$constraint instanceof ZipCode) { 223 | throw new UnexpectedTypeException($constraint, ZipCode::class); 224 | } 225 | 226 | if (null === $value || '' === $value) { 227 | return; 228 | } 229 | 230 | if (null === $constraint->iso || !($iso = strtoupper($constraint->iso))) { 231 | // if iso code is not specified, try to fetch it via getter from the object, which is currently validated 232 | $object = $this->context->getObject(); 233 | $getter = $constraint->getter; 234 | 235 | if (!is_callable(array($object, $getter))) { 236 | $message = 'Method "%s" used as iso code getter does not exist in class %s'; 237 | throw new ConstraintDefinitionException(sprintf($message, $getter, get_class($object))); 238 | } 239 | 240 | $iso = strtoupper($object->$getter() ?? ''); 241 | } 242 | 243 | if(empty($iso)){ 244 | return; 245 | } 246 | 247 | // check whether pattern for iso code is specified 248 | if (!isset($this->patterns[$iso])) { 249 | if ($constraint->strict) { 250 | throw new ConstraintDefinitionException(sprintf('Invalid iso code "%s" for validation', $iso)); 251 | } 252 | 253 | return; 254 | } 255 | 256 | $pattern = $this->patterns[$iso]; 257 | $caseSensitive = ($constraint->caseSensitiveCheck) ? '' : 'i'; 258 | if (!preg_match("/^{$pattern}$/" . $caseSensitive, $value, $matches)) { 259 | // If you're using the new 2.5 validation API (you probably are!) 260 | $this->context->buildViolation($constraint->message) 261 | ->setParameter('%string%', $value) 262 | ->addViolation(); 263 | } 264 | } 265 | } 266 | --------------------------------------------------------------------------------