diff --git a/README.md b/README.md index c5a9fe7..7fc044b 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,7 @@ For validating a zip code you need to instantiate a new ZipCode class provided b $form = $this->createFormBuilder($address) ->add('zipcode', TextType::class, [ 'constraints' => [ - new ZipCodeValidator\Constraints\ZipCode([ - 'iso' => 'DE' - ]) + new ZipCodeValidator\Constraints\ZipCode(iso: 'DE') ] ]) ->add('save', SubmitType::class, ['label' => 'Create Task']) @@ -51,7 +49,7 @@ class Address } ``` -You can also use it as a PHP8 Attribute, with parameters passed as an array of options, for example: +You can also use it as a PHP8 Attribute with named parameters: ```php 'DE']) + #[ZipCode(iso: 'DE')] protected $zipCode; } ``` +Legacy array options are still supported for backward compatibility: +```php +#[ZipCode(['iso' => 'DE'])] +``` + > Please consider to inject a valid ISO 3166 2-letter country code (e.g. DE, US, FR)! > NOTE: This library validates against known zip code regex patterns and does not validate the existence of a zipcode. @@ -117,10 +120,7 @@ protected $zipCode; ### Case insensitive zip code matching 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: ```php -$constraint = new ZipCode([ - 'iso' => 'GB', - 'caseSensitiveCheck' => false -]); +$constraint = new ZipCode(iso: 'GB', caseSensitiveCheck: false); ``` By the default the library is using case sensitive zip code matching. diff --git a/composer.json b/composer.json index 17f6d4a..4dc8078 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "prefer-stable": true, "require": { "php": ">=8.0", - "symfony/validator": ">=4.4.40" + "symfony/validator": "^5.4.43 || ^6.4.11 || ^7.1.4 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.6 || ^10.5 || ^11.0.3" diff --git a/src/ZipCodeValidator/Constraints/ZipCode.php b/src/ZipCodeValidator/Constraints/ZipCode.php index 258a418..f73f793 100644 --- a/src/ZipCodeValidator/Constraints/ZipCode.php +++ b/src/ZipCodeValidator/Constraints/ZipCode.php @@ -4,6 +4,7 @@ use Attribute; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\InvalidOptionsException; use Symfony\Component\Validator\Exception\MissingOptionsException; /** @@ -20,19 +21,72 @@ class ZipCode extends Constraint public bool $strict = true; public bool $caseSensitiveCheck = true; - public function __construct(mixed $options = null, ?array $groups = null, mixed $payload = null) + public function __construct( + mixed $options = null, + ?array $groups = null, + mixed $payload = null, + ?string $iso = null, + ?string $getter = null, + ?bool $strict = null, + ?bool $caseSensitiveCheck = null, + ?string $message = null + ) { if (is_string($options)) { - $options = array( - 'iso' => $options + if (null !== $iso) { + throw new InvalidOptionsException( + 'Cannot pass both positional string $options and named "iso". Use one style.', + ['options', 'iso'] + ); + } + + $options = ['iso' => $options]; + } elseif (null === $options) { + $options = []; + } elseif (!is_array($options)) { + throw new InvalidOptionsException(sprintf('The options "%s" do not exist in constraint "%s".', 'options', __CLASS__), ['options']); + } + + $resolvedOptions = [ + 'iso' => $iso, + 'getter' => $getter, + 'strict' => $strict, + 'caseSensitiveCheck' => $caseSensitiveCheck, + 'message' => $message, + 'groups' => $groups, + 'payload' => $payload, + ]; + + $invalidOptions = array_values(array_filter(array_keys($options), fn ($option) => !in_array($option, array_keys($resolvedOptions), true))); + if ([] !== $invalidOptions) { + throw new InvalidOptionsException( + sprintf('The options "%s" do not exist in constraint "%s".', implode('", "', $invalidOptions), __CLASS__), + $invalidOptions ); } - parent::__construct($options, $groups, $payload); + foreach ($resolvedOptions as $option => $resolvedValue) { + if (null !== $resolvedValue || !array_key_exists($option, $options)) { + continue; + } + + $resolvedOptions[$option] = 'groups' === $option ? (array) $options[$option] : $options[$option]; + } + + parent::__construct(null, $resolvedOptions['groups'], $resolvedOptions['payload']); + + unset($resolvedOptions['groups'], $resolvedOptions['payload']); + + foreach ($resolvedOptions as $option => $resolvedValue) { + if (null === $resolvedValue) { + continue; + } + + $this->{$option} = $resolvedValue; + } if (null === $this->iso && null === $this->getter) { throw new MissingOptionsException(sprintf('Either the option "iso" or "getter" must be given for constraint %s', __CLASS__), ['iso', 'getter']); } } - } diff --git a/tests/Constraints/ZipCodeTest.php b/tests/Constraints/ZipCodeTest.php index 6de7f5f..9fa4611 100644 --- a/tests/Constraints/ZipCodeTest.php +++ b/tests/Constraints/ZipCodeTest.php @@ -3,6 +3,7 @@ namespace ZipCodeValidator\Tests\Constraints; use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Exception\InvalidOptionsException; use Symfony\Component\Validator\Exception\MissingOptionsException; use ZipCodeValidator\Constraints\ZipCode; @@ -13,4 +14,72 @@ public function testMissingOptionsExceptionWhenIsoAndGetterIsEmpty(): void $this->expectException(MissingOptionsException::class); $constraint = new ZipCode(null); } -} \ No newline at end of file + + public function testLegacyStringOptionSetsIso(): void + { + $constraint = new ZipCode('DE'); + + $this->assertSame('DE', $constraint->iso); + } + + public function testLegacyArrayOptionsAreStillSupported(): void + { + $payload = new \stdClass(); + $constraint = new ZipCode([ + 'iso' => 'GB', + 'strict' => false, + 'caseSensitiveCheck' => false, + 'message' => 'Custom message', + 'groups' => 'Address', + 'payload' => $payload, + ]); + + $this->assertSame('GB', $constraint->iso); + $this->assertFalse($constraint->strict); + $this->assertFalse($constraint->caseSensitiveCheck); + $this->assertSame('Custom message', $constraint->message); + $this->assertSame(['Address'], $constraint->groups); + $this->assertSame($payload, $constraint->payload); + } + + public function testNamedParametersAreSupported(): void + { + $constraint = new ZipCode( + iso: 'FR', + strict: false, + caseSensitiveCheck: false, + message: 'Another message', + groups: ['Checkout'] + ); + + $this->assertSame('FR', $constraint->iso); + $this->assertFalse($constraint->strict); + $this->assertFalse($constraint->caseSensitiveCheck); + $this->assertSame('Another message', $constraint->message); + $this->assertSame(['Checkout'], $constraint->groups); + } + + public function testNamedParametersTakePrecedenceOverLegacyOptionsArray(): void + { + $constraint = new ZipCode( + ['iso' => 'DE', 'strict' => true], + iso: 'US', + strict: false + ); + + $this->assertSame('US', $constraint->iso); + $this->assertFalse($constraint->strict); + } + + public function testUnknownLegacyOptionThrowsException(): void + { + $this->expectException(InvalidOptionsException::class); + new ZipCode(['foo' => 'bar', 'iso' => 'FR']); + } + + public function testLegacyStringOptionCannotBeCombinedWithNamedIso(): void + { + $this->expectException(InvalidOptionsException::class); + new ZipCode('DE', iso: 'FR'); + } +}