[Validator] Add normalizer option to Unique constraint
This commit is contained in:
parent
49d23d4813
commit
44e1e8bc9b
@ -3,7 +3,7 @@ CHANGELOG
|
|||||||
|
|
||||||
5.3
|
5.3
|
||||||
---
|
---
|
||||||
|
* Add the `normalizer` option to the `Unique` constraint
|
||||||
* Add `Validation::createIsValidCallable()` that returns true/false instead of throwing exceptions
|
* Add `Validation::createIsValidCallable()` that returns true/false instead of throwing exceptions
|
||||||
|
|
||||||
5.2.0
|
5.2.0
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
namespace Symfony\Component\Validator\Constraints;
|
namespace Symfony\Component\Validator\Constraints;
|
||||||
|
|
||||||
use Symfony\Component\Validator\Constraint;
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\Exception\InvalidArgumentException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Annotation
|
* @Annotation
|
||||||
@ -29,15 +30,22 @@ class Unique extends Constraint
|
|||||||
];
|
];
|
||||||
|
|
||||||
public $message = 'This collection should contain only unique elements.';
|
public $message = 'This collection should contain only unique elements.';
|
||||||
|
public $normalizer;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
array $options = null,
|
array $options = null,
|
||||||
string $message = null,
|
string $message = null,
|
||||||
|
callable $normalizer = null,
|
||||||
array $groups = null,
|
array $groups = null,
|
||||||
$payload = null
|
$payload = null
|
||||||
) {
|
) {
|
||||||
parent::__construct($options, $groups, $payload);
|
parent::__construct($options, $groups, $payload);
|
||||||
|
|
||||||
$this->message = $message ?? $this->message;
|
$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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,10 @@ class UniqueValidator extends ConstraintValidator
|
|||||||
}
|
}
|
||||||
|
|
||||||
$collectionElements = [];
|
$collectionElements = [];
|
||||||
|
$normalizer = $this->getNormalizer($constraint);
|
||||||
foreach ($value as $element) {
|
foreach ($value as $element) {
|
||||||
|
$element = $normalizer($element);
|
||||||
|
|
||||||
if (\in_array($element, $collectionElements, true)) {
|
if (\in_array($element, $collectionElements, true)) {
|
||||||
$this->context->buildViolation($constraint->message)
|
$this->context->buildViolation($constraint->message)
|
||||||
->setParameter('{{ value }}', $this->formatValue($value))
|
->setParameter('{{ value }}', $this->formatValue($value))
|
||||||
@ -51,4 +54,15 @@ class UniqueValidator extends ConstraintValidator
|
|||||||
$collectionElements[] = $element;
|
$collectionElements[] = $element;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getNormalizer(Unique $unique): callable
|
||||||
|
{
|
||||||
|
if (null === $unique->normalizer) {
|
||||||
|
return static function ($value) {
|
||||||
|
return $value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return $unique->normalizer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,14 +13,15 @@ namespace Symfony\Component\Validator\Tests\Constraints;
|
|||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\Validator\Constraints\Unique;
|
use Symfony\Component\Validator\Constraints\Unique;
|
||||||
|
use Symfony\Component\Validator\Exception\InvalidArgumentException;
|
||||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||||
use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader;
|
use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader;
|
||||||
|
|
||||||
|
class UniqueTest extends TestCase
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* @requires PHP 8
|
* @requires PHP 8
|
||||||
*/
|
*/
|
||||||
class UniqueTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testAttributes()
|
public function testAttributes()
|
||||||
{
|
{
|
||||||
$metadata = new ClassMetadata(UniqueDummy::class);
|
$metadata = new ClassMetadata(UniqueDummy::class);
|
||||||
@ -34,6 +35,23 @@ class UniqueTest extends TestCase
|
|||||||
[$cConstraint] = $metadata->properties['c']->getConstraints();
|
[$cConstraint] = $metadata->properties['c']->getConstraints();
|
||||||
self::assertSame(['my_group'], $cConstraint->groups);
|
self::assertSame(['my_group'], $cConstraint->groups);
|
||||||
self::assertSame('some attached data', $cConstraint->payload);
|
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')]
|
#[Unique(groups: ['my_group'], payload: 'some attached data')]
|
||||||
private $c;
|
private $c;
|
||||||
|
|
||||||
|
#[Unique(normalizer: 'intval')]
|
||||||
|
private $d;
|
||||||
}
|
}
|
||||||
|
@ -99,4 +99,136 @@ class UniqueValidatorTest extends ConstraintValidatorTestCase
|
|||||||
->setCode(Unique::IS_NOT_UNIQUE)
|
->setCode(Unique::IS_NOT_UNIQUE)
|
||||||
->assertRaised();
|
->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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user