[Validator] Add normalizer option to Unique constraint

This commit is contained in:
Andrii Popov 2020-10-08 22:05:27 +03:00 committed by Alexander M. Turek
parent 49d23d4813
commit 44e1e8bc9b
5 changed files with 179 additions and 4 deletions

View File

@ -3,7 +3,7 @@ CHANGELOG
5.3
---
* Add the `normalizer` option to the `Unique` constraint
* Add `Validation::createIsValidCallable()` that returns true/false instead of throwing exceptions
5.2.0

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\InvalidArgumentException;
/**
* @Annotation
@ -29,15 +30,22 @@ class Unique extends Constraint
];
public $message = 'This collection should contain only unique elements.';
public $normalizer;
public function __construct(
array $options = null,
string $message = null,
callable $normalizer = null,
array $groups = null,
$payload = null
) {
parent::__construct($options, $groups, $payload);
$this->message = $message ?? $this->message;
$this->normalizer = $normalizer ?? $this->normalizer;
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)));
}
}
}

View File

@ -39,7 +39,10 @@ class UniqueValidator extends ConstraintValidator
}
$collectionElements = [];
$normalizer = $this->getNormalizer($constraint);
foreach ($value as $element) {
$element = $normalizer($element);
if (\in_array($element, $collectionElements, true)) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($value))
@ -51,4 +54,15 @@ class UniqueValidator extends ConstraintValidator
$collectionElements[] = $element;
}
}
private function getNormalizer(Unique $unique): callable
{
if (null === $unique->normalizer) {
return static function ($value) {
return $value;
};
}
return $unique->normalizer;
}
}

View File

@ -13,14 +13,15 @@ namespace Symfony\Component\Validator\Tests\Constraints;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraints\Unique;
use Symfony\Component\Validator\Exception\InvalidArgumentException;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader;
class UniqueTest extends TestCase
{
/**
* @requires PHP 8
*/
class UniqueTest extends TestCase
{
public function testAttributes()
{
$metadata = new ClassMetadata(UniqueDummy::class);
@ -34,6 +35,23 @@ class UniqueTest extends TestCase
[$cConstraint] = $metadata->properties['c']->getConstraints();
self::assertSame(['my_group'], $cConstraint->groups);
self::assertSame('some attached data', $cConstraint->payload);
[$dConstraint] = $metadata->properties['d']->getConstraints();
self::assertSame('intval', $dConstraint->normalizer);
}
public function testInvalidNormalizerThrowsException()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The "normalizer" option must be a valid callable ("string" given).');
new Unique(['normalizer' => 'Unknown Callable']);
}
public function testInvalidNormalizerObjectThrowsException()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The "normalizer" option must be a valid callable ("stdClass" given).');
new Unique(['normalizer' => new \stdClass()]);
}
}
@ -47,4 +65,7 @@ class UniqueDummy
#[Unique(groups: ['my_group'], payload: 'some attached data')]
private $c;
#[Unique(normalizer: 'intval')]
private $d;
}

View File

@ -99,4 +99,136 @@ class UniqueValidatorTest extends ConstraintValidatorTestCase
->setCode(Unique::IS_NOT_UNIQUE)
->assertRaised();
}
/**
* @dataProvider getCallback
*/
public function testExpectsUniqueObjects($callback)
{
$object1 = new \stdClass();
$object1->name = 'Foo';
$object1->email = 'foo@email.com';
$object2 = new \stdClass();
$object2->name = 'Foo';
$object2->email = 'foobar@email.com';
$object3 = new \stdClass();
$object3->name = 'Bar';
$object3->email = 'foo@email.com';
$value = [$object1, $object2, $object3];
$this->validator->validate($value, new Unique([
'normalizer' => $callback,
]));
$this->assertNoViolation();
}
/**
* @dataProvider getCallback
*/
public function testExpectsNonUniqueObjects($callback)
{
$object1 = new \stdClass();
$object1->name = 'Foo';
$object1->email = 'bar@email.com';
$object2 = new \stdClass();
$object2->name = 'Foo';
$object2->email = 'foo@email.com';
$object3 = new \stdClass();
$object3->name = 'Foo';
$object3->email = 'foo@email.com';
$value = [$object1, $object2, $object3];
$this->validator->validate($value, new Unique([
'message' => 'myMessage',
'normalizer' => $callback,
]));
$this->buildViolation('myMessage')
->setParameter('{{ value }}', 'array')
->setCode(Unique::IS_NOT_UNIQUE)
->assertRaised();
}
public function getCallback()
{
return [
yield 'static function' => [static function (\stdClass $object) {
return [$object->name, $object->email];
}],
yield 'callable with string notation' => ['Symfony\Component\Validator\Tests\Constraints\CallableClass::execute'],
yield 'callable with static notation' => [[CallableClass::class, 'execute']],
yield 'callable with object' => [[new CallableClass(), 'execute']],
];
}
public function testExpectsInvalidNonStrictComparison()
{
$this->validator->validate([1, '1', 1.0, '1.0'], new Unique([
'message' => 'myMessage',
'normalizer' => 'intval',
]));
$this->buildViolation('myMessage')
->setParameter('{{ value }}', 'array')
->setCode(Unique::IS_NOT_UNIQUE)
->assertRaised();
}
public function testExpectsValidNonStrictComparison()
{
$callback = static function ($item) {
return (int) $item;
};
$this->validator->validate([1, '2', 3, '4.0'], new Unique([
'normalizer' => $callback,
]));
$this->assertNoViolation();
}
public function testExpectsInvalidCaseInsensitiveComparison()
{
$callback = static function ($item) {
return mb_strtolower($item);
};
$this->validator->validate(['Hello', 'hello', 'HELLO', 'hellO'], new Unique([
'message' => 'myMessage',
'normalizer' => $callback,
]));
$this->buildViolation('myMessage')
->setParameter('{{ value }}', 'array')
->setCode(Unique::IS_NOT_UNIQUE)
->assertRaised();
}
public function testExpectsValidCaseInsensitiveComparison()
{
$callback = static function ($item) {
return mb_strtolower($item);
};
$this->validator->validate(['Hello', 'World'], new Unique([
'normalizer' => $callback,
]));
$this->assertNoViolation();
}
}
class CallableClass
{
public static function execute(\stdClass $object)
{
return [$object->name, $object->email];
}
}