From 5e7d3ab17b10f8bb8cdf23c4259ce8e28f048b01 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Tue, 20 Oct 2020 22:44:15 +0200 Subject: [PATCH] Enabled to use the UniqueEntity constraint as an attribute. --- .../Constraints/UniqueEntityTest.php | 85 +++++++++++ .../Constraints/UniqueEntityValidatorTest.php | 140 +++++++++++------- .../Validator/Constraints/UniqueEntity.php | 36 +++++ src/Symfony/Bridge/Doctrine/composer.json | 4 +- 4 files changed, 212 insertions(+), 53 deletions(-) create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityTest.php diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityTest.php new file mode 100644 index 0000000000..bd9791bc65 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityTest.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Validator\Constraints; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; + +/** + * @requires PHP 8 + */ +class UniqueEntityTest extends TestCase +{ + public function testAttributeWithDefaultProperty() + { + $metadata = new ClassMetadata(UniqueEntityDummyOne::class); + $loader = new AnnotationLoader(); + self::assertTrue($loader->loadClassMetadata($metadata)); + + /** @var UniqueEntity $constraint */ + list($constraint) = $metadata->getConstraints(); + self::assertSame(['email'], $constraint->fields); + self::assertTrue($constraint->ignoreNull); + self::assertSame('doctrine.orm.validator.unique', $constraint->validatedBy()); + self::assertSame(['Default', 'UniqueEntityDummyOne'], $constraint->groups); + } + + public function testAttributeWithCustomizedService() + { + $metadata = new ClassMetadata(UniqueEntityDummyTwo::class); + $loader = new AnnotationLoader(); + self::assertTrue($loader->loadClassMetadata($metadata)); + + /** @var UniqueEntity $constraint */ + list($constraint) = $metadata->getConstraints(); + self::assertSame(['isbn'], $constraint->fields); + self::assertSame('my_own_validator', $constraint->validatedBy()); + self::assertSame('my_own_entity_manager', $constraint->em); + self::assertSame('App\Entity\MyEntity', $constraint->entityClass); + self::assertSame('fetchDifferently', $constraint->repositoryMethod); + } + + public function testAttributeWithGroupsAndPaylod() + { + $metadata = new ClassMetadata(UniqueEntityDummyThree::class); + $loader = new AnnotationLoader(); + self::assertTrue($loader->loadClassMetadata($metadata)); + + /** @var UniqueEntity $constraint */ + list($constraint) = $metadata->getConstraints(); + self::assertSame('uuid', $constraint->fields); + self::assertSame('id', $constraint->errorPath); + self::assertSame('some attached data', $constraint->payload); + self::assertSame(['some_group'], $constraint->groups); + } +} + +#[UniqueEntity(['email'], message: 'myMessage')] +class UniqueEntityDummyOne +{ + private $email; +} + +#[UniqueEntity(fields: ['isbn'], service: 'my_own_validator', em: 'my_own_entity_manager', entityClass: 'App\Entity\MyEntity', repositoryMethod: 'fetchDifferently')] +class UniqueEntityDummyTwo +{ + private $isbn; +} + +#[UniqueEntity('uuid', ignoreNull: false, errorPath: 'id', payload: 'some attached data', groups: ['some_group'])] +class UniqueEntityDummyThree +{ + private $id; + private $uuid; +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php index e9e905c89c..8507df94bf 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php @@ -162,15 +162,11 @@ class UniqueEntityValidatorTest extends ConstraintValidatorTestCase /** * This is a functional test as there is a large integration necessary to get the validator working. + * + * @dataProvider provideUniquenessConstraints */ - public function testValidateUniqueness() + public function testValidateUniqueness(UniqueEntity $constraint) { - $constraint = new UniqueEntity([ - 'message' => 'myMessage', - 'fields' => ['name'], - 'em' => self::EM_NAME, - ]); - $entity1 = new SingleIntIdEntity(1, 'Foo'); $entity2 = new SingleIntIdEntity(2, 'Foo'); @@ -196,15 +192,24 @@ class UniqueEntityValidatorTest extends ConstraintValidatorTestCase ->assertRaised(); } - public function testValidateCustomErrorPath() + public function provideUniquenessConstraints(): iterable { - $constraint = new UniqueEntity([ + yield 'Doctrine style' => [new UniqueEntity([ 'message' => 'myMessage', 'fields' => ['name'], 'em' => self::EM_NAME, - 'errorPath' => 'bar', - ]); + ])]; + if (\PHP_VERSION_ID >= 80000) { + yield 'Named arguments' => [eval('return new \Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity(message: "myMessage", fields: ["name"], em: "foo");')]; + } + } + + /** + * @dataProvider provideConstraintsWithCustomErrorPath + */ + public function testValidateCustomErrorPath(UniqueEntity $constraint) + { $entity1 = new SingleIntIdEntity(1, 'Foo'); $entity2 = new SingleIntIdEntity(2, 'Foo'); @@ -222,14 +227,25 @@ class UniqueEntityValidatorTest extends ConstraintValidatorTestCase ->assertRaised(); } - public function testValidateUniquenessWithNull() + public function provideConstraintsWithCustomErrorPath(): iterable { - $constraint = new UniqueEntity([ + yield 'Doctrine style' => [new UniqueEntity([ 'message' => 'myMessage', 'fields' => ['name'], 'em' => self::EM_NAME, - ]); + 'errorPath' => 'bar', + ])]; + if (\PHP_VERSION_ID >= 80000) { + yield 'Named arguments' => [eval('return new \Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity(message: "myMessage", fields: ["name"], em: "foo", errorPath: "bar");')]; + } + } + + /** + * @dataProvider provideUniquenessConstraints + */ + public function testValidateUniquenessWithNull(UniqueEntity $constraint) + { $entity1 = new SingleIntIdEntity(1, null); $entity2 = new SingleIntIdEntity(2, null); @@ -242,15 +258,11 @@ class UniqueEntityValidatorTest extends ConstraintValidatorTestCase $this->assertNoViolation(); } - public function testValidateUniquenessWithIgnoreNullDisabled() + /** + * @dataProvider provideConstraintsWithIgnoreNullDisabled + */ + public function testValidateUniquenessWithIgnoreNullDisabled(UniqueEntity $constraint) { - $constraint = new UniqueEntity([ - 'message' => 'myMessage', - 'fields' => ['name', 'name2'], - 'em' => self::EM_NAME, - 'ignoreNull' => false, - ]); - $entity1 = new DoubleNameEntity(1, 'Foo', null); $entity2 = new DoubleNameEntity(2, 'Foo', null); @@ -276,30 +288,36 @@ class UniqueEntityValidatorTest extends ConstraintValidatorTestCase ->assertRaised(); } - public function testAllConfiguredFieldsAreCheckedOfBeingMappedByDoctrineWithIgnoreNullEnabled() + public function provideConstraintsWithIgnoreNullDisabled(): iterable { - $this->expectException('Symfony\Component\Validator\Exception\ConstraintDefinitionException'); - $constraint = new UniqueEntity([ + yield 'Doctrine style' => [new UniqueEntity([ 'message' => 'myMessage', 'fields' => ['name', 'name2'], 'em' => self::EM_NAME, - 'ignoreNull' => true, - ]); + 'ignoreNull' => false, + ])]; + if (\PHP_VERSION_ID >= 80000) { + yield 'Named arguments' => [eval('return new \Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity(message: "myMessage", fields: ["name", "name2"], em: "foo", ignoreNull: false);')]; + } + } + + /** + * @dataProvider provideConstraintsWithIgnoreNullEnabled + */ + public function testAllConfiguredFieldsAreCheckedOfBeingMappedByDoctrineWithIgnoreNullEnabled(UniqueEntity $constraint) + { $entity1 = new SingleIntIdEntity(1, null); + $this->expectException('Symfony\Component\Validator\Exception\ConstraintDefinitionException'); $this->validator->validate($entity1, $constraint); } - public function testNoValidationIfFirstFieldIsNullAndNullValuesAreIgnored() + /** + * @dataProvider provideConstraintsWithIgnoreNullEnabled + */ + public function testNoValidationIfFirstFieldIsNullAndNullValuesAreIgnored(UniqueEntity $constraint) { - $constraint = new UniqueEntity([ - 'message' => 'myMessage', - 'fields' => ['name', 'name2'], - 'em' => self::EM_NAME, - 'ignoreNull' => true, - ]); - $entity1 = new DoubleNullableNameEntity(1, null, 'Foo'); $entity2 = new DoubleNullableNameEntity(2, null, 'Foo'); @@ -319,6 +337,20 @@ class UniqueEntityValidatorTest extends ConstraintValidatorTestCase $this->assertNoViolation(); } + public function provideConstraintsWithIgnoreNullEnabled(): iterable + { + yield 'Doctrine style' => [new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['name', 'name2'], + 'em' => self::EM_NAME, + 'ignoreNull' => true, + ])]; + + if (\PHP_VERSION_ID >= 80000) { + yield 'Named arguments' => [eval('return new \Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity(message: "myMessage", fields: ["name", "name2"], em: "foo", ignoreNull: true);')]; + } + } + public function testValidateUniquenessWithValidCustomErrorPath() { $constraint = new UniqueEntity([ @@ -353,15 +385,11 @@ class UniqueEntityValidatorTest extends ConstraintValidatorTestCase ->assertRaised(); } - public function testValidateUniquenessUsingCustomRepositoryMethod() + /** + * @dataProvider provideConstraintsWithCustomRepositoryMethod + */ + public function testValidateUniquenessUsingCustomRepositoryMethod(UniqueEntity $constraint) { - $constraint = new UniqueEntity([ - 'message' => 'myMessage', - 'fields' => ['name'], - 'em' => self::EM_NAME, - 'repositoryMethod' => 'findByCustom', - ]); - $repository = $this->createRepositoryMock(); $repository->expects($this->once()) ->method('findByCustom') @@ -379,15 +407,11 @@ class UniqueEntityValidatorTest extends ConstraintValidatorTestCase $this->assertNoViolation(); } - public function testValidateUniquenessWithUnrewoundArray() + /** + * @dataProvider provideConstraintsWithCustomRepositoryMethod + */ + public function testValidateUniquenessWithUnrewoundArray(UniqueEntity $constraint) { - $constraint = new UniqueEntity([ - 'message' => 'myMessage', - 'fields' => ['name'], - 'em' => self::EM_NAME, - 'repositoryMethod' => 'findByCustom', - ]); - $entity = new SingleIntIdEntity(1, 'foo'); $repository = $this->createRepositoryMock(); @@ -414,6 +438,20 @@ class UniqueEntityValidatorTest extends ConstraintValidatorTestCase $this->assertNoViolation(); } + public function provideConstraintsWithCustomRepositoryMethod(): iterable + { + yield 'Doctrine style' => [new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['name'], + 'em' => self::EM_NAME, + 'repositoryMethod' => 'findByCustom', + ])]; + + if (\PHP_VERSION_ID >= 80000) { + yield 'Named arguments' => [eval('return new \Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity(message: "myMessage", fields: ["name"], em: "foo", repositoryMethod: "findByCustom");')]; + } + } + /** * @dataProvider resultTypesProvider */ diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php index 2c319709eb..168956da94 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php @@ -21,6 +21,7 @@ use Symfony\Component\Validator\Constraint; * * @author Benjamin Eberlei */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] class UniqueEntity extends Constraint { const NOT_UNIQUE_ERROR = '23bd9dbf-6b9b-41cd-a99e-4844bcf3077f'; @@ -38,6 +39,41 @@ class UniqueEntity extends Constraint self::NOT_UNIQUE_ERROR => 'NOT_UNIQUE_ERROR', ]; + /** + * {@inheritdoc} + * + * @param array|string $fields the combination of fields that must contain unique values or a set of options + */ + public function __construct( + $fields, + string $message = null, + string $service = null, + string $em = null, + string $entityClass = null, + string $repositoryMethod = null, + string $errorPath = null, + bool $ignoreNull = null, + array $groups = null, + $payload = null, + array $options = [] + ) { + if (\is_array($fields) && \is_string(key($fields))) { + $options = array_merge($fields, $options); + } elseif (null !== $fields) { + $options['fields'] = $fields; + } + + parent::__construct($options, $groups, $payload); + + $this->message = $message ?? $this->message; + $this->service = $service ?? $this->service; + $this->em = $em ?? $this->em; + $this->entityClass = $entityClass ?? $this->entityClass; + $this->repositoryMethod = $repositoryMethod ?? $this->repositoryMethod; + $this->errorPath = $errorPath ?? $this->errorPath; + $this->ignoreNull = $ignoreNull ?? $this->ignoreNull; + } + public function getRequiredOptions() { return ['fields']; diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index 15c5b6294c..0052cfa59c 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -40,7 +40,7 @@ "symfony/security-core": "^5.0", "symfony/expression-language": "^4.4|^5.0", "symfony/uid": "^5.1", - "symfony/validator": "^5.0.2", + "symfony/validator": "^5.2", "symfony/translation": "^4.4|^5.0", "symfony/var-dumper": "^4.4|^5.0", "doctrine/annotations": "~1.7", @@ -61,7 +61,7 @@ "symfony/property-info": "<5", "symfony/security-bundle": "<5", "symfony/security-core": "<5", - "symfony/validator": "<5.0.2" + "symfony/validator": "<5.2" }, "suggest": { "symfony/form": "",