Skip to content

Commit 44e1e8b

Browse files
henry2778derrabus
authored andcommitted
[Validator] Add normalizer option to Unique constraint
1 parent 49d23d4 commit 44e1e8b

File tree

5 files changed

+179
-4
lines changed

5 files changed

+179
-4
lines changed

src/Symfony/Component/Validator/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

src/Symfony/Component/Validator/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
}

src/Symfony/Component/Validator/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
}

src/Symfony/Component/Validator/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
}

src/Symfony/Component/Validator/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 = 'foo@email.com';
111+
112+
$object2 = new \stdClass();
113+
$object2->name = 'Foo';
114+
$object2->email = 'foobar@email.com';
115+
116+
$object3 = new \stdClass();
117+
$object3->name = 'Bar';
118+
$object3->email = 'foo@email.com';
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 = 'bar@email.com';
137+
138+
$object2 = new \stdClass();
139+
$object2->name = 'Foo';
140+
$object2->email = 'foo@email.com';
141+
142+
$object3 = new \stdClass();
143+
$object3->name = 'Foo';
144+
$object3->email = 'foo@email.com';
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy