Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 0e8f4ce

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

File tree

4 files changed

+100
-7
lines changed
Filter options

4 files changed

+100
-7
lines changed

‎src/Symfony/Component/Validator/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/CHANGELOG.md
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/Constraints/Unique.php
+10-1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/Constraints/UniqueValidator.php
+21Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php
+68-6Lines 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' => 'bar@email.com'],
281+
['id' => 1, 'email' => 'foo@email.com'],
282+
], ['id']],
283+
];
284+
}
223285
}
224286

225287
class CallableClass

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.