8000 [Validator] Allow Unique constraint validation on all elements · symfony/symfony@3fc871e · GitHub
[go: up one dir, main page]

Skip to content

Commit 3fc871e

Browse files
Jean-Berunicolas-grekas
authored andcommitted
[Validator] Allow Unique constraint validation on all elements
1 parent 8d8183f commit 3fc871e

File tree

4 files changed

+88
-57
lines changed

4 files changed

+88
-57
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

+1-48
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,12 @@ CHANGELOG
55
---
66

77
* Deprecate defining custom constraints not supporting named arguments
8-
9-
Before:
10-
11-
```php
12-
use Symfony\Component\Validator\Constraint;
13-
14-
class CustomConstraint extends Constraint
15-
{
16-
public function __construct(array $options)
17-
{
18-
// ...
19-
}
20-
}
21-
```
22-
23-
After:
24-
25-
```php
26-
use Symfony\Component\Validator\Attribute\HasNamedArguments;
27-
use Symfony\Component\Validator\Constraint;
28-
29-
class CustomConstraint extends Constraint
30-
{
31-
#[HasNamedArguments]
32-
public function __construct($option1, $option2, $groups, $payload)
33-
{
34-
// ...
35-
}
36-
}
37-
```
388
* Deprecate passing an array of options to the constructors of the constraint classes, pass each option as a dedicated argument instead
39-
40-
Before:
41-
42-
```php
43-
new NotNull([
44-
'groups' => ['foo', 'bar'],
45-
'message' => 'a custom constraint violation message',
46-
])
47-
```
48-
49-
After:
50-
51-
```php
52-
new NotNull(
53-
groups: ['foo', 'bar'],
54-
message: 'a custom constraint violation message',
55-
)
56-
```
579
* Add support for ratio checks for SVG files to the `Image` constraint
5810
* Add the `Slug` constraint
5911
* Add support for the `otherwise` option in the `When` constraint
6012
* Add support for multiple fields containing nested constraints in `Composite` constraints
13+
* Add the `stopOnFirstError` option to the `Unique` constraint to validate all elements
6114

6215
7.2
6316
---

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

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class Unique extends Constraint
2727

2828
public array|string $fields = [];
2929
public ?string $errorPath = null;
30+
public bool $stopOnFirstError = true;
3031

3132
protected const ERROR_NAMES = [
3233
self::IS_NOT_UNIQUE => 'IS_NOT_UNIQUE',
@@ -50,6 +51,7 @@ public function __construct(
5051
mixed $payload = null,
5152
array|string|null $fields = null,
5253
?string $errorPath = null,
54+
?bool $stopOnFirstError = null,
5355
) {
5456
if (\is_array($options)) {
5557
trigger_deprecation('symfony/validator', '7.3', 'Passing an array of options to configure the "%s" constraint is deprecated, use named arguments instead.', static::class);
@@ -61,6 +63,7 @@ public function __construct(
6163
$this->normalizer = $normalizer ?? $this->normalizer;
6264
$this->fields = $fields ?? $this->fields;
6365
$this->errorPath = $errorPath ?? $this->errorPath;
66+
$this->stopOnFirstError = $stopOnFirstError ?? $this->stopOnFirstError;
6467

6568
if (null !== $this->normalizer && !\is_callable($this->normalizer)) {
6669
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

+13-9
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,24 @@ public function validate(mixed $value, Constraint $constraint): void
4646
continue;
4747
}
4848

49-
if (\in_array($element, $collectionElements, true)) {
50-
$violationBuilder = $this->context->buildViolation($constraint->message)
51-
->setParameter('{{ value }}', $this->formatValue($element))
52-
->setCode(Unique::IS_NOT_UNIQUE);
49+
if (!\in_array($element, $collectionElements, true)) {
50+
$collectionElements[] = $element;
51+
continue;
52+
}
5353

54-
if (null !== $constraint->errorPath) {
55-
$violationBuilder->atPath("[$index].{$constraint->errorPath}");
56-
}
54+
$violationBuilder = $this->context->buildViolation($constraint->message)
55+
->setParameter('{{ value }}', $this->formatValue($element))
56+
->setCode(Unique::IS_NOT_UNIQUE);
57+
58+
if (!$constraint->stopOnFirstError || null !== $constraint->errorPath) {
59+
$violationBuilder->atPath("[$index]".(null !== $constraint->errorPath ? ".{$constraint->errorPath}" : ''));
60+
}
5761

58-
$violationBuilder->addViolation();
62+
$violationBuilder->addViolation();
5963

64+
if ($constraint->stopOnFirstError) {
6065
return;
6166
}
62-
$collectionElements[] = $element;
6367
}
6468
}
6569

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

+71
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,77 @@ public function testErrorPathWithNonList()
387387
->assertRaised();
388388
}
389389

390+
public function testWithoutStopOnFirstError()
391+
{
392+
$this->validator->validate(
393+
['a1', 'a2', 'a1', 'a1', 'a2'],
394+
new Unique(stopOnFirstError: false),
395+
);
396+
397+
$this
398+
->buildViolation('This collection should contain only unique elements.')
399+
->setParameter('{{ value }}', '"a1"')
400+
->setCode(Unique::IS_NOT_UNIQUE)
401+
->atPath('property.path[2]')
402+
403+
->buildNextViolation('This collection should contain only unique elements.')
404+
->setParameter('{{ value }}', '"a1"')
405+
->setCode(Unique::IS_NOT_UNIQUE)
406+
->atPath('property.path[3]')
407+
408+
->buildNextViolation('This collection should contain only unique elements.')
409+
->setParameter('{{ value }}', '"a2"')
410+
->setCode(Unique::IS_NOT_UNIQUE)
411+
->atPath('property.path[4]')
412+
413+
->assertRaised();
414+
}
415+
416+
public function testWithoutStopOnFirstErrorWithErrorPath()
417+
{
418+
$array = [
419+
new DummyClassOne(),
420+
new DummyClassOne(),
421+
new DummyClassOne(),
422+
new DummyClassOne(),
423+
new DummyClassOne(),
424+
];
425+
426+
$array[0]->code = 'a1';
427+
$array[1]->code = 'a2';
428+
$array[2]->code = 'a1';
429+
$array[3]->code = 'a1';
430+
$array[4]->code = 'a2';
431+
432+
$this->validator->validate(
433+
$array,
434+
new Unique(
435+
normalizer: [self::class, 'normalizeDummyClassOne'],
436+
fields: 'code',
437+
errorPath: 'code',
438+
stopOnFirstError: false,
439+
)
440+
);
441+
442+
$this
443+
->buildViolation('This collection should contain only unique elements.')
444+
->setParameter('{{ value }}', 'array')
445+
->setCode(Unique::IS_NOT_UNIQUE)
446+
->atPath('property.path[2].code')
447+
448+
->buildNextViolation('This collection should contain only unique elements.')
449+
->setParameter('{{ value }}', 'array')
450+
->setCode(Unique::IS_NOT_UNIQUE)
451+
->atPath('property.path[3].code')
452+
453+
->buildNextViolation('This collection should contain only unique elements.')
454+
->setParameter('{{ value }}', 'array')
455+
->setCode(Unique::IS_NOT_UNIQUE)
456+
->atPath('property.path[4].code')
457+
458+
->assertRaised();
459+
}
460+
390461
public static function normalizeDummyClassOne(DummyClassOne $obj): array
391462
{
392463
return [

0 commit comments

Comments
 (0)
0