diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 509f331767..54d6120a21 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * added UATP cards support to `CardSchemeValidator` * added option `allowNull` to NotBlank constraint * added `Json` constraint + * added `Unique` constraint 4.2.0 ----- diff --git a/src/Symfony/Component/Validator/Constraints/Unique.php b/src/Symfony/Component/Validator/Constraints/Unique.php new file mode 100644 index 0000000000..391c4eaa13 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/Unique.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; + +/** + * @Annotation + * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) + * + * @author Yevgeniy Zholkevskiy + */ +class Unique extends Constraint +{ + public const IS_NOT_UNIQUE = '7911c98d-b845-4da0-94b7-a8dac36bc55a'; + + protected static $errorNames = [ + self::IS_NOT_UNIQUE => 'IS_NOT_UNIQUE', + ]; + + public $message = 'This collection should contain only unique elements'; +} diff --git a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php new file mode 100644 index 0000000000..72403b59bd --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedValueException; + +/** + * @author Yevgeniy Zholkevskiy + */ +class UniqueValidator extends ConstraintValidator +{ + /** + * {@inheritdoc} + */ + public function validate($value, Constraint $constraint) + { + if (!$constraint instanceof Unique) { + throw new UnexpectedTypeException($constraint, Unique::class); + } + + if (null === $value) { + return; + } + + if (!\is_array($value) && !$value instanceof \IteratorAggregate) { + throw new UnexpectedValueException($value, 'array|IteratorAggregate'); + } + + $collectionElements = []; + foreach ($value as $element) { + if (\in_array($element, $collectionElements, true)) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $this->formatValue($value)) + ->setCode(Unique::IS_NOT_UNIQUE) + ->addViolation(); + + return; + } + $collectionElements[] = $element; + } + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php new file mode 100644 index 0000000000..65564abf9c --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Validator\Constraints\Unique; +use Symfony\Component\Validator\Constraints\UniqueValidator; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; + +class UniqueValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator() + { + return new UniqueValidator(); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\UnexpectedValueException + */ + public function testExpectsUniqueConstraintCompatibleType() + { + $this->validator->validate('', new Unique()); + } + + /** + * @dataProvider getValidValues + */ + public function testValidValues($value) + { + $this->validator->validate($value, new Unique()); + + $this->assertNoViolation(); + } + + public function getValidValues() + { + return [ + yield 'null' => [[null]], + yield 'empty array' => [[]], + yield 'single integer' => [[5]], + yield 'single string' => [['a']], + yield 'single object' => [[new \stdClass()]], + yield 'unique booleans' => [[true, false]], + yield 'unique integers' => [[1, 2, 3, 4, 5, 6]], + yield 'unique floats' => [[0.1, 0.2, 0.3]], + yield 'unique strings' => [['a', 'b', 'c']], + yield 'unique arrays' => [[[1, 2], [2, 4], [4, 6]]], + yield 'unique objects' => [[new \stdClass(), new \stdClass()]], + ]; + } + + /** + * @dataProvider getInvalidValues + */ + public function testInvalidValues($value) + { + $constraint = new Unique([ + 'message' => 'myMessage', + ]); + $this->validator->validate($value, $constraint); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', 'array') + ->setCode(Unique::IS_NOT_UNIQUE) + ->assertRaised(); + } + + public 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]], + ]; + } +}