Enabled to use the UniqueEntity constraint as an attribute.

This commit is contained in:
Alexander M. Turek 2020-10-20 22:44:15 +02:00
parent 7b62f099c2
commit 5e7d3ab17b
4 changed files with 212 additions and 53 deletions

View File

@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
}

View File

@ -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
*/

View File

@ -21,6 +21,7 @@ use Symfony\Component\Validator\Constraint;
*
* @author Benjamin Eberlei <kontakt@beberlei.de>
*/
#[\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'];

View File

@ -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": "",