Skip to content

Commit c642453

Browse files
henry2778derrabus
authored andcommitted
[Validator] Add normalizer option to Unique constraint
1 parent 268c90a commit c642453

File tree

5 files changed

+179
-4
lines changed

5 files changed

+179
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ CHANGELOG
33

44
5.3
55
---
6-
6+
* Add the `normalizer` option to the `Unique` constraint
77
* Add `Validation::createIsValidCallable()` that returns true/false instead of throwing exceptions
88

99
5.2.0

Constraints/Unique.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Validator\Constraints;
1313

1414
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\Exception\InvalidArgumentException;
1516

1617
/**
1718
* @Annotation
@@ -29,15 +30,22 @@ class Unique extends Constraint
2930
];
3031

3132
public $message = 'This collection should contain only unique elements.';
33+
public $normalizer;
3234

3335
public function __construct(
3436
array $options = null,
3537
string $message = null,
38+
callable $normalizer = null,
3639
array $groups = null,
3740
$payload = null
3841
) {
3942
parent::__construct($options, $groups, $payload);
4043

4144
$this->message = $message ?? $this->message;
45+
$this->normalizer = $normalizer ?? $this->normalizer;
46+
47+
if (null !== $this->normalizer && !\is_callable($this->normalizer)) {
48+
throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer)));
49+
}
4250
}
4351
}

Constraints/UniqueValidator.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ public function validate($value, Constraint $constraint)
3939
}
4040

4141
$collectionElements = [];
42+
$normalizer = $this->getNormalizer($constraint);
4243
foreach ($value as $element) {
44+
$element = $normalizer($element);
45+
4346
if (\in_array($element, $collectionElements, true)) {
4447
$this->context->buildViolation($constraint->message)
4548
->setParameter('{{ value }}', $this->formatValue($value))
@@ -51,4 +54,15 @@ public function validate($value, Constraint $constraint)
5154
$collectionElements[] = $element;
5255
}
5356
}
57+
58+
private function getNormalizer(Unique $unique): callable
59+
{
60+
if (null === $unique->normalizer) {
61+
return static function ($value) {
62+
return $value;
63+
};
64+
}
65+
66+
return $unique->normalizer;
67+
}
5468
}

Tests/Constraints/UniqueTest.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Validator\Constraints\Unique;
16+
use Symfony\Component\Validator\Exception\InvalidArgumentException;
1617
use Symfony\Component\Validator\Mapping\ClassMetadata;
1718
use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader;
1819

19-
/**
20-
* @requires PHP 8
21-
*/
2220
class UniqueTest extends TestCase
2321
{
22+
/**
23+
* @requires PHP 8
24+
*/
2425
public function testAttributes()
2526
{
2627
$metadata = new ClassMetadata(UniqueDummy::class);
@@ -34,6 +35,23 @@ public function testAttributes()
3435
[$cConstraint] = $metadata->properties['c']->getConstraints();
3536
self::assertSame(['my_group'], $cConstraint->groups);
3637
self::assertSame('some attached data', $cConstraint->payload);
38+
39+
[$dConstraint] = $metadata->properties['d']->getConstraints();
40+
self::assertSame('intval', $dConstraint->normalizer);
41+
}
42+
43+
public function testInvalidNormalizerThrowsException()
44+
{
45+
$this->expectException(InvalidArgumentException::class);
46+
$this->expectExceptionMessage('The "normalizer" option must be a valid callable ("string" given).');
47+
new Unique(['normalizer' => 'Unknown Callable']);
48+
}
49+
50+
public function testInvalidNormalizerObjectThrowsException()
51+
{
52+
$this->expectException(InvalidArgumentException::class);
53+
$this->expectExceptionMessage('The "normalizer" option must be a valid callable ("stdClass" given).');
54+
new Unique(['normalizer' => new \stdClass()]);
3755
}
3856
}
3957

@@ -47,4 +65,7 @@ class UniqueDummy
4765

4866
#[Unique(groups: ['my_group'], payload: 'some attached data')]
4967
private $c;
68+
69+
#[Unique(normalizer: 'intval')]
70+
private $d;
5071
}

Tests/Constraints/UniqueValidatorTest.php

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,136 @@ public function testInvalidValueNamed()
9999
->setCode(Unique::IS_NOT_UNIQUE)
100100
->assertRaised();
101101
}
102+
103+
/**
104+
* @dataProvider getCallback
105+
*/
106+
public function testExpectsUniqueObjects($callback)
107+
{
108+
$object1 = new \stdClass();
109+
$object1->name = 'Foo';
110+
$object1->email = '[email protected]';
111+
112+
$object2 = new \stdClass();
113+
$object2->name = 'Foo';
114+
$object2->email = '[email protected]';
115+
116+
$object3 = new \stdClass();
117+
$object3->name = 'Bar';
118+
$object3->email = '[email protected]';
119+
120+
$value = [$object1, $object2, $object3];
121+
122+
$this->validator->validate($value, new Unique([
123+
'normalizer' => $callback,
124+
]));
125+
126+
$this->assertNoViolation();
127+
}
128+
129+
/**
130+
* @dataProvider getCallback
131+
*/
132+
public function testExpectsNonUniqueObjects($callback)
133+
{
134+
$object1 = new \stdClass();
135+
$object1->name = 'Foo';
136+
$object1->email = '[email protected]';
137+
138+
$object2 = new \stdClass();
139+
$object2->name = 'Foo';
140+
$object2->email = '[email protected]';
141+
142+
$object3 = new \stdClass();
143+
$object3->name = 'Foo';
144+
$object3->email = '[email protected]';
145+
146+
$value = [$object1, $object2, $object3];
147+
148+
$this->validator->validate($value, new Unique([
149+
'message' => 'myMessage',
150+
'normalizer' => $callback,
151+
]));
152+
153+
$this->buildViolation('myMessage')
154+
->setParameter('{{ value }}', 'array')
155+
->setCode(Unique::IS_NOT_UNIQUE)
156+
->assertRaised();
157+
}
158+
159+
public function getCallback()
160+
{
161+
return [
162+
yield 'static function' => [static function (\stdClass $object) {
163+
return [$object->name, $object->email];
164+
}],
165+
yield 'callable with string notation' => ['Symfony\Component\Validator\Tests\Constraints\CallableClass::execute'],
166+
yield 'callable with static notation' => [[CallableClass::class, 'execute']],
167+
yield 'callable with object' => [[new CallableClass(), 'execute']],
168+
];
169+
}
170+
171+
public function testExpectsInvalidNonStrictComparison()
172+
{
173+
$this->validator->validate([1, '1', 1.0, '1.0'], new Unique([
174+
'message' => 'myMessage',
175+
'normalizer' => 'intval',
176+
]));
177+
178+
$this->buildViolation('myMessage')
179+
->setParameter('{{ value }}', 'array')
180+
->setCode(Unique::IS_NOT_UNIQUE)
181+
->assertRaised();
182+
}
183+
184+
public function testExpectsValidNonStrictComparison()
185+
{
186+
$callback = static function ($item) {
187+
return (int) $item;
188+
};
189+
190+
$this->validator->validate([1, '2', 3, '4.0'], new Unique([
191+
'normalizer' => $callback,
192+
]));
193+
194+
$this->assertNoViolation();
195+
}
196+
197+
public function testExpectsInvalidCaseInsensitiveComparison()
198+
{
199+
$callback = static function ($item) {
200+
return mb_strtolower($item);
201+
};
202+
203+
$this->validator->validate(['Hello', 'hello', 'HELLO', 'hellO'], new Unique([
204+
'message' => 'myMessage',
205+
'normalizer' => $callback,
206+
]));
207+
208+
$this->buildViolation('myMessage')
209+
->setParameter('{{ value }}', 'array')
210+
->setCode(Unique::IS_NOT_UNIQUE)
211+
->assertRaised();
212+
}
213+
214+
public function testExpectsValidCaseInsensitiveComparison()
215+
{
216+
$callback = static function ($item) {
217+
return mb_strtolower($item);
218+
};
219+
220+
$this->validator->validate(['Hello', 'World'], new Unique([
221+
'normalizer' => $callback,
222+
]));
223+
224+
$this->assertNoViolation();
225+
}
226+
}
227+
228+
class CallableClass
229+
{
230+
public static function execute(\stdClass $object)
231+
{
232+
return [$object->name, $object->email];
233+
}
102234
}

0 commit comments

Comments
 (0)