Skip to content

Commit 46b624f

Browse files
committed
feature symfony#57436 [Validator] Add errorPath to Unique constraint (norkunas)
This PR was merged into the 7.2 branch. Discussion ---------- [Validator] Add `errorPath` to Unique constraint | Q | A | ------------- | --- | Branch? | 7.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | Fix symfony#46182 | License | MIT Commits ------- c8051ce [Validator] Add `errorPath` to Unique constraint
2 parents 5279a30 + c8051ce commit 46b624f

File tree

4 files changed

+109
-12
lines changed

4 files changed

+109
-12
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* `IbanValidator` accepts IBANs containing non-breaking and narrow non-breaking spaces
88
* Make `PasswordStrengthValidator::estimateStrength()` public
99
* Add the `Yaml` constraint for validating YAML content
10+
* Add `errorPath` to Unique constraint
1011

1112
7.1
1213
---

src/Symfony/Component/Validator/Constraints/Unique.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class Unique extends Constraint
2525
public const IS_NOT_UNIQUE = '7911c98d-b845-4da0-94b7-a8dac36bc55a';
2626

2727
public array|string $fields = [];
28+
public ?string $errorPath = null;
2829

2930
protected const ERROR_NAMES = [
3031
self::IS_NOT_UNIQUE => 'IS_NOT_UNIQUE',
@@ -46,12 +47,14 @@ public function __construct(
4647
?array $groups = null,
4748
mixed $payload = null,
4849
array|string|null $fields = null,
50+
?string $errorPath = null,
4951
) {
5052
parent::__construct($options, $groups, $payload);
5153

5254
$this->message = $message ?? $this->message;
5355
$this->normalizer = $normalizer ?? $this->normalizer;
5456
$this->fields = $fields ?? $this->fields;
57+
$this->errorPath = $errorPath ?? $this->errorPath;
5558

5659
if (null !== $this->normalizer && !\is_callable($this->normalizer)) {
5760
throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer)));

src/Symfony/Component/Validator/Constraints/UniqueValidator.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function validate(mixed $value, Constraint $constraint): void
3939

4040
$collectionElements = [];
4141
$normalizer = $this->getNormalizer($constraint);
42-
foreach ($value as $element) {
42+
foreach ($value as $index => $element) {
4343
$element = $normalizer($element);
4444

4545
if ($fields && !$element = $this->reduceElementKeys($fields, $element)) {
@@ -48,6 +48,7 @@ public function validate(mixed $value, Constraint $constraint): void
4848

4949
if (\in_array($element, $collectionElements, true)) {
5050
$this->context->buildViolation($constraint->message)
51+
->atPath("[$index]".(null !== $constraint->errorPath ? ".{$constraint->errorPath}" : ''))
5152
->setParameter('{{ value }}', $this->formatValue($value))
5253
->setCode(Unique::IS_NOT_UNIQUE)
5354
->addViolation();

src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public static function getValidValues()
6161
/**
6262
* @dataProvider getInvalidValues
6363
*/
64-
public function testInvalidValues($value)
64+
public function testInvalidValues($value, string $expectedErrorPath)
6565
{
6666
$constraint = new Unique([
6767
'message' => 'myMessage',
@@ -71,6 +71,7 @@ public function testInvalidValues($value)
7171
$this->buildViolation('myMessage')
7272
->setParameter('{{ value }}', 'array')
7373
->setCode(Unique::IS_NOT_UNIQUE)
74+
->atPath($expectedErrorPath)
7475
->assertRaised();
7576
}
7677

@@ -79,12 +80,12 @@ public static function getInvalidValues()
7980
$object = new \stdClass();
8081

8182
return [
82-
yield 'not unique booleans' => [[true, true]],
83-
yield 'not unique integers' => [[1, 2, 3, 3]],
84-
yield 'not unique floats' => [[0.1, 0.2, 0.1]],
85-
yield 'not unique string' => [['a', 'b', 'a']],
86-
yield 'not unique arrays' => [[[1, 1], [2, 3], [1, 1]]],
87-
yield 'not unique objects' => [[$object, $object]],
83+
yield 'not unique booleans' => [[true, true], 'property.path[1]'],
84+
yield 'not unique integers' => [[1, 2, 3, 3], 'property.path[3]'],
85+
yield 'not unique floats' => [[0.1, 0.2, 0.1], 'property.path[2]'],
86+
yield 'not unique string' => [['a', 'b', 'a'], 'property.path[2]'],
87+
yield 'not unique arrays' => [[[1, 1], [2, 3], [1, 1]], 'property.path[2]'],
88+
yield 'not unique objects' => [[$object, $object], 'property.path[1]'],
8889
];
8990
}
9091

@@ -96,6 +97,7 @@ public function testInvalidValueNamed()
9697
$this->buildViolation('myMessage')
9798
->setParameter('{{ value }}', 'array')
9899
->setCode(Unique::IS_NOT_UNIQUE)
100+
->atPath('property.path[3]')
99101
->assertRaised();
100102
}
101103

@@ -152,6 +154,7 @@ public function testExpectsNonUniqueObjects($callback)
152154
$this->buildViolation('myMessage')
153155
->setParameter('{{ value }}', 'array')
154156
->setCode(Unique::IS_NOT_UNIQUE)
157+
->atPath('property.path[2]')
155158
->assertRaised();
156159
}
157160

@@ -176,6 +179,7 @@ public function testExpectsInvalidNonStrictComparison()
176179
$this->buildViolation('myMessage')
177180
->setParameter('{{ value }}', 'array')
178181
->setCode(Unique::IS_NOT_UNIQUE)
182+
->atPath('property.path[1]')
179183
->assertRaised();
180184
}
181185

@@ -202,6 +206,7 @@ public function testExpectsInvalidCaseInsensitiveComparison()
202206
$this->buildViolation('myMessage')
203207
->setParameter('{{ value }}', 'array')
204208
->setCode(Unique::IS_NOT_UNIQUE)
209+
->atPath('property.path[1]')
205210
->assertRaised();
206211
}
207212

@@ -246,7 +251,7 @@ public static function getInvalidFieldNames(): array
246251
/**
247252
* @dataProvider getInvalidCollectionValues
248253
*/
249-
public function testInvalidCollectionValues(array $value, array $fields)
254+
public function testInvalidCollectionValues(array $value, array $fields, string $expectedErrorPath)
250255
{
251256
$this->validator->validate($value, new Unique([
252257
'message' => 'myMessage',
@@ -255,6 +260,7 @@ public function testInvalidCollectionValues(array $value, array $fields)
255260
$this->buildViolation('myMessage')
256261
->setParameter('{{ value }}', 'array')
257262
->setCode(Unique::IS_NOT_UNIQUE)
263+
->atPath($expectedErrorPath)
258264
->assertRaised();
259265
}
260266

@@ -264,23 +270,25 @@ public static function getInvalidCollectionValues(): array
264270
'unique string' => [[
265271
['lang' => 'eng', 'translation' => 'hi'],
266272
['lang' => 'eng', 'translation' => 'hello'],
267-
], ['lang']],
273+
], ['lang'], 'property.path[1]'],
268274
'unique floats' => [[
269275
['latitude' => 51.509865, 'longitude' => -0.118092, 'poi' => 'capital'],
270276
['latitude' => 52.520008, 'longitude' => 13.404954],
271277
['latitude' => 51.509865, 'longitude' => -0.118092],
272-
], ['latitude', 'longitude']],
278+
], ['latitude', 'longitude'], 'property.path[2]'],
273279
'unique int' => [[
274280
['id' => 1, 'email' => '[email protected]'],
275281
['id' => 1, 'email' => '[email protected]'],
276-
], ['id']],
282+
], ['id'], 'property.path[1]'],
277283
'unique null' => [
278284
[null, null],
279285
[],
286+
'property.path[1]',
280287
],
281288
'unique field null' => [
282289
[['nullField' => null], ['nullField' => null]],
283290
['nullField'],
291+
'property.path[1]',
284292
],
285293
];
286294
}
@@ -308,6 +316,90 @@ public function testArrayOfObjectsUnique()
308316
$this->assertNoViolation();
309317
}
310318

319+
public function testErrorPath()
320+
{
321+
$array = [
322+
new DummyClassOne(),
323+
new DummyClassOne(),
324+
new DummyClassOne(),
325+
];
326+
327+
$array[0]->code = 'a1';
328+
$array[1]->code = 'a2';
329+
$array[2]->code = 'a1';
330+
331+
$this->validator->validate(
332+
$array,
333+
new Unique(
334+
normalizer: [self::class, 'normalizeDummyClassOne'],
335+
fields: 'code',
336+
errorPath: 'code',
337+
)
338+
);
339+
340+
$this->buildViolation('This collection should contain only unique elements.')
341+
->setParameter('{{ value }}', 'array')
342+
->setCode(Unique::IS_NOT_UNIQUE)
343+
->atPath('property.path[2].code')
344+
->assertRaised();
345+
}
346+
347+
public function testErrorPathWithIteratorAggregate()
348+
{
349+
$array = new \ArrayObject([
350+
new DummyClassOne(),
351+
new DummyClassOne(),
352+
new DummyClassOne(),
353+
]);
354+
355+
$array[0]->code = 'a1';
356+
$array[1]->code = 'a2';
357+
$array[2]->code = 'a1';
358+
359+
$this->validator->validate(
360+
$array,
361+
new Unique(
362+
normalizer: [self::class, 'normalizeDummyClassOne'],
363+
fields: 'code',
364+
errorPath: 'code',
365+
)
366+
);
367+
368+
$this->buildViolation('This collection should contain only unique elements.')
369+
->setParameter('{{ value }}', 'object')
370+
->setCode(Unique::IS_NOT_UNIQUE)
371+
->atPath('property.path[2].code')
372+
->assertRaised();
373+
}
374+
375+
public function testErrorPathWithNonList()
376+
{
377+
$array = [
378+
'a' => new DummyClassOne(),
379+
'b' => new DummyClassOne(),
380+
'c' => new DummyClassOne(),
381+
];
382+
383+
$array['a']->code = 'a1';
384+
$array['b']->code = 'a2';
385+
$array['c']->code = 'a1';
386+
387+
$this->validator->validate(
388+
$array,
389+
new Unique(
390+
normalizer: [self::class, 'normalizeDummyClassOne'],
391+
fields: 'code',
392+
errorPath: 'code',
393+
)
394+
);
395+
396+
$this->buildViolation('This collection should contain only unique elements.')
397+
->setParameter('{{ value }}', 'array')
398+
->setCode(Unique::IS_NOT_UNIQUE)
399+
->atPath('property.path[c].code')
400+
->assertRaised();
401+
}
402+
311403
public static function normalizeDummyClassOne(DummyClassOne $obj): array
312404
{
313405
return [

0 commit comments

Comments
 (0)