diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 960195c95da4b..e421f748ebbb0 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -5,59 +5,12 @@ CHANGELOG --- * Deprecate defining custom constraints not supporting named arguments - - Before: - - ```php - use Symfony\Component\Validator\Constraint; - - class CustomConstraint extends Constraint - { - public function __construct(array $options) - { - // ... - } - } - ``` - - After: - - ```php - use Symfony\Component\Validator\Attribute\HasNamedArguments; - use Symfony\Component\Validator\Constraint; - - class CustomConstraint extends Constraint - { - #[HasNamedArguments] - public function __construct($option1, $option2, $groups, $payload) - { - // ... - } - } - ``` * Deprecate passing an array of options to the constructors of the constraint classes, pass each option as a dedicated argument instead - - Before: - - ```php - new NotNull([ - 'groups' => ['foo', 'bar'], - 'message' => 'a custom constraint violation message', - ]) - ``` - - After: - - ```php - new NotNull( - groups: ['foo', 'bar'], - message: 'a custom constraint violation message', - ) - ``` * Add support for ratio checks for SVG files to the `Image` constraint * Add the `Slug` constraint * Add support for the `otherwise` option in the `When` constraint * Add support for multiple fields containing nested constraints in `Composite` constraints + * Add the `stopOnFirstError` option to the `Unique` constraint to validate all elements 7.2 --- diff --git a/src/Symfony/Component/Validator/Constraints/Unique.php b/src/Symfony/Component/Validator/Constraints/Unique.php index 857672daf6f41..1e6503785f07e 100644 --- a/src/Symfony/Component/Validator/Constraints/Unique.php +++ b/src/Symfony/Component/Validator/Constraints/Unique.php @@ -27,6 +27,7 @@ class Unique extends Constraint public array|string $fields = []; public ?string $errorPath = null; + public bool $stopOnFirstError = true; protected const ERROR_NAMES = [ self::IS_NOT_UNIQUE => 'IS_NOT_UNIQUE', @@ -50,6 +51,7 @@ public function __construct( mixed $payload = null, array|string|null $fields = null, ?string $errorPath = null, + ?bool $stopOnFirstError = null, ) { if (\is_array($options)) { 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( $this->normalizer = $normalizer ?? $this->normalizer; $this->fields = $fields ?? $this->fields; $this->errorPath = $errorPath ?? $this->errorPath; + $this->stopOnFirstError = $stopOnFirstError ?? $this->stopOnFirstError; if (null !== $this->normalizer && !\is_callable($this->normalizer)) { throw new InvalidArgumentException(\sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer))); diff --git a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php index 8977fd2210809..bd78cac721d1f 100644 --- a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php @@ -46,20 +46,24 @@ public function validate(mixed $value, Constraint $constraint): void continue; } - if (\in_array($element, $collectionElements, true)) { - $violationBuilder = $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($element)) - ->setCode(Unique::IS_NOT_UNIQUE); + if (!\in_array($element, $collectionElements, true)) { + $collectionElements[] = $element; + continue; + } - if (null !== $constraint->errorPath) { - $violationBuilder->atPath("[$index].{$constraint->errorPath}"); - } + $violationBuilder = $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($element)) + ->setCode(Unique::IS_NOT_UNIQUE); + + if (!$constraint->stopOnFirstError || null !== $constraint->errorPath) { + $violationBuilder->atPath("[$index]".(null !== $constraint->errorPath ? ".{$constraint->errorPath}" : '')); + } - $violationBuilder->addViolation(); + $violationBuilder->addViolation(); + if ($constraint->stopOnFirstError) { return; } - $collectionElements[] = $element; } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php index ac43ed2b8ab0c..12efb76982e24 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php @@ -387,6 +387,77 @@ public function testErrorPathWithNonList() ->assertRaised(); } + public function testWithoutStopOnFirstError() + { + $this->validator->validate( + ['a1', 'a2', 'a1', 'a1', 'a2'], + new Unique(stopOnFirstError: false), + ); + + $this + ->buildViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', '"a1"') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[2]') + + ->buildNextViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', '"a1"') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[3]') + + ->buildNextViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', '"a2"') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[4]') + + ->assertRaised(); + } + + public function testWithoutStopOnFirstErrorWithErrorPath() + { + $array = [ + new DummyClassOne(), + new DummyClassOne(), + new DummyClassOne(), + new DummyClassOne(), + new DummyClassOne(), + ]; + + $array[0]->code = 'a1'; + $array[1]->code = 'a2'; + $array[2]->code = 'a1'; + $array[3]->code = 'a1'; + $array[4]->code = 'a2'; + + $this->validator->validate( + $array, + new Unique( + normalizer: [self::class, 'normalizeDummyClassOne'], + fields: 'code', + errorPath: 'code', + stopOnFirstError: false, + ) + ); + + $this + ->buildViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', 'array') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[2].code') + + ->buildNextViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', 'array') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[3].code') + + ->buildNextViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', 'array') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[4].code') + + ->assertRaised(); + } + public static function normalizeDummyClassOne(DummyClassOne $obj): array { return [