diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 32ac5c86415ef..337534645ffe2 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * `IbanValidator` accepts IBANs containing non-breaking and narrow non-breaking spaces * Make `PasswordStrengthValidator::estimateStrength()` public * Add the `Yaml` constraint for validating YAML content + * Add `errorPath` to Unique constraint 7.1 --- diff --git a/src/Symfony/Component/Validator/Constraints/Unique.php b/src/Symfony/Component/Validator/Constraints/Unique.php index c759fac634349..7e4d6f626cef7 100644 --- a/src/Symfony/Component/Validator/Constraints/Unique.php +++ b/src/Symfony/Component/Validator/Constraints/Unique.php @@ -25,6 +25,7 @@ class Unique extends Constraint public const IS_NOT_UNIQUE = '7911c98d-b845-4da0-94b7-a8dac36bc55a'; public array|string $fields = []; + public ?string $errorPath = null; protected const ERROR_NAMES = [ self::IS_NOT_UNIQUE => 'IS_NOT_UNIQUE', @@ -46,12 +47,14 @@ public function __construct( ?array $groups = null, mixed $payload = null, array|string|null $fields = null, + ?string $errorPath = null, ) { parent::__construct($options, $groups, $payload); $this->message = $message ?? $this->message; $this->normalizer = $normalizer ?? $this->normalizer; $this->fields = $fields ?? $this->fields; + $this->errorPath = $errorPath ?? $this->errorPath; 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 e62c94179ecb4..e188e3f250101 100644 --- a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php @@ -39,7 +39,7 @@ public function validate(mixed $value, Constraint $constraint): void $collectionElements = []; $normalizer = $this->getNormalizer($constraint); - foreach ($value as $element) { + foreach ($value as $index => $element) { $element = $normalizer($element); if ($fields && !$element = $this->reduceElementKeys($fields, $element)) { @@ -48,6 +48,7 @@ public function validate(mixed $value, Constraint $constraint): void if (\in_array($element, $collectionElements, true)) { $this->context->buildViolation($constraint->message) + ->atPath("[$index]".(null !== $constraint->errorPath ? ".{$constraint->errorPath}" : '')) ->setParameter('{{ value }}', $this->formatValue($value)) ->setCode(Unique::IS_NOT_UNIQUE) ->addViolation(); diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php index 3c2dd9f21c98f..0382eb5b6198a 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php @@ -61,7 +61,7 @@ public static function getValidValues() /** * @dataProvider getInvalidValues */ - public function testInvalidValues($value) + public function testInvalidValues($value, string $expectedErrorPath) { $constraint = new Unique([ 'message' => 'myMessage', @@ -71,6 +71,7 @@ public function testInvalidValues($value) $this->buildViolation('myMessage') ->setParameter('{{ value }}', 'array') ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath($expectedErrorPath) ->assertRaised(); } @@ -79,12 +80,12 @@ public static function getInvalidValues() $object = new \stdClass(); return [ - yield 'not unique booleans' => [[true, true]], - yield 'not unique integers' => [[1, 2, 3, 3]], - yield 'not unique floats' => [[0.1, 0.2, 0.1]], - yield 'not unique string' => [['a', 'b', 'a']], - yield 'not unique arrays' => [[[1, 1], [2, 3], [1, 1]]], - yield 'not unique objects' => [[$object, $object]], + yield 'not unique booleans' => [[true, true], 'property.path[1]'], + yield 'not unique integers' => [[1, 2, 3, 3], 'property.path[3]'], + yield 'not unique floats' => [[0.1, 0.2, 0.1], 'property.path[2]'], + yield 'not unique string' => [['a', 'b', 'a'], 'property.path[2]'], + yield 'not unique arrays' => [[[1, 1], [2, 3], [1, 1]], 'property.path[2]'], + yield 'not unique objects' => [[$object, $object], 'property.path[1]'], ]; } @@ -96,6 +97,7 @@ public function testInvalidValueNamed() $this->buildViolation('myMessage') ->setParameter('{{ value }}', 'array') ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[3]') ->assertRaised(); } @@ -152,6 +154,7 @@ public function testExpectsNonUniqueObjects($callback) $this->buildViolation('myMessage') ->setParameter('{{ value }}', 'array') ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[2]') ->assertRaised(); } @@ -176,6 +179,7 @@ public function testExpectsInvalidNonStrictComparison() $this->buildViolation('myMessage') ->setParameter('{{ value }}', 'array') ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[1]') ->assertRaised(); } @@ -202,6 +206,7 @@ public function testExpectsInvalidCaseInsensitiveComparison() $this->buildViolation('myMessage') ->setParameter('{{ value }}', 'array') ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[1]') ->assertRaised(); } @@ -246,7 +251,7 @@ public static function getInvalidFieldNames(): array /** * @dataProvider getInvalidCollectionValues */ - public function testInvalidCollectionValues(array $value, array $fields) + public function testInvalidCollectionValues(array $value, array $fields, string $expectedErrorPath) { $this->validator->validate($value, new Unique([ 'message' => 'myMessage', @@ -255,6 +260,7 @@ public function testInvalidCollectionValues(array $value, array $fields) $this->buildViolation('myMessage') ->setParameter('{{ value }}', 'array') ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath($expectedErrorPath) ->assertRaised(); } @@ -264,23 +270,25 @@ public static function getInvalidCollectionValues(): array 'unique string' => [[ ['lang' => 'eng', 'translation' => 'hi'], ['lang' => 'eng', 'translation' => 'hello'], - ], ['lang']], + ], ['lang'], 'property.path[1]'], 'unique floats' => [[ ['latitude' => 51.509865, 'longitude' => -0.118092, 'poi' => 'capital'], ['latitude' => 52.520008, 'longitude' => 13.404954], ['latitude' => 51.509865, 'longitude' => -0.118092], - ], ['latitude', 'longitude']], + ], ['latitude', 'longitude'], 'property.path[2]'], 'unique int' => [[ ['id' => 1, 'email' => 'bar@email.com'], ['id' => 1, 'email' => 'foo@email.com'], - ], ['id']], + ], ['id'], 'property.path[1]'], 'unique null' => [ [null, null], [], + 'property.path[1]', ], 'unique field null' => [ [['nullField' => null], ['nullField' => null]], ['nullField'], + 'property.path[1]', ], ]; } @@ -308,6 +316,90 @@ public function testArrayOfObjectsUnique() $this->assertNoViolation(); } + public function testErrorPath() + { + $array = [ + new DummyClassOne(), + new DummyClassOne(), + new DummyClassOne(), + ]; + + $array[0]->code = 'a1'; + $array[1]->code = 'a2'; + $array[2]->code = 'a1'; + + $this->validator->validate( + $array, + new Unique( + normalizer: [self::class, 'normalizeDummyClassOne'], + fields: 'code', + errorPath: 'code', + ) + ); + + $this->buildViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', 'array') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[2].code') + ->assertRaised(); + } + + public function testErrorPathWithIteratorAggregate() + { + $array = new \ArrayObject([ + new DummyClassOne(), + new DummyClassOne(), + new DummyClassOne(), + ]); + + $array[0]->code = 'a1'; + $array[1]->code = 'a2'; + $array[2]->code = 'a1'; + + $this->validator->validate( + $array, + new Unique( + normalizer: [self::class, 'normalizeDummyClassOne'], + fields: 'code', + errorPath: 'code', + ) + ); + + $this->buildViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', 'object') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[2].code') + ->assertRaised(); + } + + public function testErrorPathWithNonList() + { + $array = [ + 'a' => new DummyClassOne(), + 'b' => new DummyClassOne(), + 'c' => new DummyClassOne(), + ]; + + $array['a']->code = 'a1'; + $array['b']->code = 'a2'; + $array['c']->code = 'a1'; + + $this->validator->validate( + $array, + new Unique( + normalizer: [self::class, 'normalizeDummyClassOne'], + fields: 'code', + errorPath: 'code', + ) + ); + + $this->buildViolation('This collection should contain only unique elements.') + ->setParameter('{{ value }}', 'array') + ->setCode(Unique::IS_NOT_UNIQUE) + ->atPath('property.path[c].code') + ->assertRaised(); + } + public static function normalizeDummyClassOne(DummyClassOne $obj): array { return [ 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