Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0e8f4ce

Browse files
wkaniafabpot
authored andcommitted
[Validator] Define which collection keys should be checked for uniqueness
1 parent 9e566e4 commit 0e8f4ce

File tree

4 files changed

+100
-7
lines changed

4 files changed

+100
-7
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
6.1
55
---
66

7+
* Add the `fields` option to the `Unique` constraint, to define which collection keys should be checked for uniqueness
78
* Deprecate `Constraint::$errorNames`, use `Constraint::ERROR_NAMES` instead
89
* Deprecate constraint `ExpressionLanguageSyntax`, use `ExpressionSyntax` instead
910
* Add method `__toString()` to `ConstraintViolationInterface` & `ConstraintViolationListInterface`

src/Symfony/Component/Validator/Constraints/Unique.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class Unique extends Constraint
2525
{
2626
public const IS_NOT_UNIQUE = '7911c98d-b845-4da0-94b7-a8dac36bc55a';
2727

28+
public array|string $fields = [];
29+
2830
protected const ERROR_NAMES = [
2931
self::IS_NOT_UNIQUE => 'IS_NOT_UNIQUE',
3032
];
@@ -37,17 +39,24 @@ class Unique extends Constraint
3739
public $message = 'This collection should contain only unique elements.';
3840
public $normalizer;
3941

42+
/**
43+
* {@inheritdoc}
44+
*
45+
* @param array|string $fields the combination of fields that must contain unique values or a set of options
46+
*/
4047
public function __construct(
4148
array $options = null,
4249
string $message = null,
4350
callable $normalizer = null,
4451
array $groups = null,
45-
mixed $payload = null
52+
mixed $payload = null,
53+
array|string $fields = null,
4654
) {
4755
parent::__construct($options, $groups, $payload);
4856

4957
$this->message = $message ?? $this->message;
5058
$this->normalizer = $normalizer ?? $this->normalizer;
59+
$this->fields = $fields ?? $this->fields;
5160

5261
if (null !== $this->normalizer && !\is_callable($this->normalizer)) {
5362
throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer)));

src/Symfony/Component/Validator/Constraints/UniqueValidator.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public function validate(mixed $value, Constraint $constraint)
3030
throw new UnexpectedTypeException($constraint, Unique::class);
3131
}
3232

33+
$fields = (array) $constraint->fields;
34+
3335
if (null === $value) {
3436
return;
3537
}
@@ -41,6 +43,10 @@ public function validate(mixed $value, Constraint $constraint)
4143
$collectionElements = [];
4244
$normalizer = $this->getNormalizer($constraint);
4345
foreach ($value as $element) {
46+
if ($fields && !$element = $this->reduceElementKeys($fields, $element)) {
47+
continue;
48+
}
49+
4450
$element = $normalizer($element);
4551

4652
if (\in_array($element, $collectionElements, true)) {
@@ -65,4 +71,19 @@ private function getNormalizer(Unique $unique): callable
6571

6672
return $unique->normalizer;
6773
}
74+
75+
private function reduceElementKeys(array $fields, array $element): array
76+
{
77+
$output = [];
78+
foreach ($fields as $field) {
79+
if (!\is_string($field)) {
80+
throw new UnexpectedTypeException($field, 'string');
81+
}
82+
if (isset($element[$field])) {
83+
$output[$field] = $element[$field];
84+
}
85+
}
86+
87+
return $output;
88+
}
6889
}

src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313

1414
use Symfony\Component\Validator\Constraints\Unique;
1515
use Symfony\Component\Validator\Constraints\UniqueValidator;
16+
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
1617
use Symfony\Component\Validator\Exception\UnexpectedValueException;
1718
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
1819

1920
class UniqueValidatorTest extends ConstraintValidatorTestCase
2021
{
21-
protected function createValidator()
22+
protected function createValidator(): UniqueValidator
2223
{
2324
return new UniqueValidator();
2425
}
@@ -153,15 +154,15 @@ public function testExpectsNonUniqueObjects($callback)
153154
->assertRaised();
154155
}
155156

156-
public function getCallback()
157+
public function getCallback(): array
157158
{
158159
return [
159-
yield 'static function' => [static function (\stdClass $object) {
160+
'static function' => [static function (\stdClass $object) {
160161
return [$object->name, $object->email];
161162
}],
162-
yield 'callable with string notation' => ['Symfony\Component\Validator\Tests\Constraints\CallableClass::execute'],
163-
yield 'callable with static notation' => [[CallableClass::class, 'execute']],
164-
yield 'callable with object' => [[new CallableClass(), 'execute']],
163+
'callable with string notation' => ['Symfony\Component\Validator\Tests\Constraints\CallableClass::execute'],
164+
'callable with static notation' => [[CallableClass::class, 'execute']],
165+
'callable with object' => [[new CallableClass(), 'execute']],
165166
];
166167
}
167168

@@ -220,6 +221,67 @@ public function testExpectsValidCaseInsensitiveComparison()
220221

221222
$this->assertNoViolation();
222223
}
224+
225+
public function testCollectionFieldsAreOptional()
226+
{
227+
$this->validator->validate([['value' => 5], ['id' => 1, 'value' => 6]], new Unique(fields: 'id'));
228+
229+
$this->assertNoViolation();
230+
}
231+
232+
/**
233+
* @dataProvider getInvalidFieldNames
234+
*/
235+
public function testCollectionFieldNamesMustBeString(string $type, mixed $field)
236+
{
237+
$this->expectException(UnexpectedTypeException::class);
238+
$this->expectExceptionMessage(sprintf('Expected argument of type "string", "%s" given', $type));
239+
240+
$this->validator->validate([['value' => 5], ['id' => 1, 'value' => 6]], new Unique(fields: [$field]));
241+
}
242+
243+
public function getInvalidFieldNames(): array
244+
{
245+
return [
246+
['stdClass', new \stdClass()],
247+
['int', 2],
248+
['bool', false],
249+
];
250+
}
251+
252+
/**
253+
* @dataProvider getInvalidCollectionValues
254+
*/
255+
public function testInvalidCollectionValues(array $value, array $fields)
256+
{
257+
$this->validator->validate($value, new Unique([
258+
'message' => 'myMessage',
259+
], fields: $fields));
260+
261+
$this->buildViolation('myMessage')
262+
->setParameter('{{ value }}', 'array')
263+
->setCode(Unique::IS_NOT_UNIQUE)
264+
->assertRaised();
265+
}
266+
267+
public function getInvalidCollectionValues(): array
268+
{
269+
return [
270+
'unique string' => [[
271+
['lang' => 'eng', 'translation' => 'hi'],
272+
['lang' => 'eng', 'translation' => 'hello'],
273+
], ['lang']],
274+
'unique floats' => [[
275+
['latitude' => 51.509865, 'longitude' => -0.118092, 'poi' => 'capital'],
276+
['latitude' => 52.520008, 'longitude' => 13.404954],
277+
['latitude' => 51.509865, 'longitude' => -0.118092],
278+
], ['latitude', 'longitude']],
279+
'unique int' => [[
280+
['id' => 1, 'email' => '[email protected]'],
281+
['id' => 1, 'email' => '[email protected]'],
282+
], ['id']],
283+
];
284+
}
223285
}
224286

225287
class CallableClass

0 commit comments

Comments
 (0)