[Validator] Add normalizer option to Unique constraint
This commit is contained in:
parent
49d23d4813
commit
44e1e8bc9b
@ -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
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
/**
|
||||
* @requires PHP 8
|
||||
*/
|
||||
class UniqueTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @requires PHP 8
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user