Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand All @@ -51,19 +49,24 @@ 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
<?php

use ZipCodeValidator\Constraints\ZipCode;

class Address
{
#[ZipCode(['iso'=>'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.
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
64 changes: 59 additions & 5 deletions src/ZipCodeValidator/Constraints/ZipCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Attribute;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\InvalidOptionsException;
use Symfony\Component\Validator\Exception\MissingOptionsException;

/**
Expand All @@ -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']);
}
}

}
71 changes: 70 additions & 1 deletion tests/Constraints/ZipCodeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -13,4 +14,72 @@ public function testMissingOptionsExceptionWhenIsoAndGetterIsEmpty(): void
$this->expectException(MissingOptionsException::class);
$constraint = new ZipCode(null);
}
}

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');
}
}