diff --git a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php index de2edce631837..2233a75e837a2 100644 --- a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php @@ -11,8 +11,12 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\LogicException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; @@ -21,6 +25,13 @@ */ class UniqueValidator extends ConstraintValidator { + private ?PropertyAccessorInterface $propertyAccessor; + + public function __construct(PropertyAccessorInterface $propertyAccessor = null) + { + $this->propertyAccessor = $propertyAccessor; + } + public function validate(mixed $value, Constraint $constraint) { if (!$constraint instanceof Unique) { @@ -69,18 +80,37 @@ private function getNormalizer(Unique $unique): callable return $unique->normalizer; } - private function reduceElementKeys(array $fields, array $element): array + private function reduceElementKeys(array $fields, array|object $element): array { $output = []; foreach ($fields as $field) { if (!\is_string($field)) { throw new UnexpectedTypeException($field, 'string'); } - if (isset($element[$field])) { - $output[$field] = $element[$field]; + + // For no BC, because PropertyAccessor require brackets for array keys + // Previous implementation, only check in array + if (\is_array($element) && !str_contains($field, '[')) { + $field = "[{$field}]"; + } + + if (null !== $value = $this->getPropertyAccessor()->getValue($element, $field)) { + $output[$field] = $value; } } return $output; } + + private function getPropertyAccessor(): PropertyAccessor + { + if (null === $this->propertyAccessor) { + if (!class_exists(PropertyAccess::class)) { + throw new LogicException('Unable to use property path as the Symfony PropertyAccess component is not installed.'); + } + $this->propertyAccessor = PropertyAccess::createPropertyAccessor(); + } + + return $this->propertyAccessor; + } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php index 2f045ccf64ff7..7c39dd521da02 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php @@ -280,6 +280,27 @@ public function getInvalidCollectionValues(): array ['id' => 1, 'email' => 'bar@email.com'], ['id' => 1, 'email' => 'foo@email.com'], ], ['id']], + 'unique string sub array' => [[ + ['id' => 1, 'translation' => ['lang' => 'eng', 'translation' => 'hi']], + ['id' => 2, 'translation' => ['lang' => 'eng', 'translation' => 'hello']], + ], ['[translation][lang]']], + 'unique string attribute' => [[ + (object) ['lang' => 'eng', 'translation' => 'hi'], + (object) ['lang' => 'eng', 'translation' => 'hello'], + ], ['lang']], + 'unique float attribute' => [[ + (object) ['latitude' => 51.509865, 'longitude' => -0.118092, 'poi' => 'capital'], + (object) ['latitude' => 52.520008, 'longitude' => 13.404954], + (object) ['latitude' => 51.509865, 'longitude' => -0.118092], + ], ['latitude', 'longitude']], + 'unique int attribute' => [[ + (object) ['id' => 1, 'email' => 'bar@email.com'], + (object) ['id' => 1, 'email' => 'foo@email.com'], + ], ['id']], + 'unique string sub object attribute' => [[ + (object) ['id' => 1, 'translation' => (object) ['lang' => 'eng', 'translation' => 'hi']], + (object) ['id' => 2, 'translation' => (object) ['lang' => 'eng', 'translation' => 'hello']], + ], ['translation.lang']], ]; } }