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

Skip to content

Commit 158c5ff

Browse files
committed
[Validator] Add Some and None collection constraints
1 parent 680eb90 commit 158c5ff

File tree

9 files changed

+870
-0
lines changed

9 files changed

+870
-0
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
6.1
5+
---
6+
7+
* Add Some and None collection constraints
8+
49
6.0
510
---
611

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
15+
16+
/**
17+
* @Annotation
18+
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
19+
*
20+
* @author Tomas Norkūnas <[email protected]>
21+
*/
22+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
23+
class None extends Composite
24+
{
25+
public array $constraints = [];
26+
public string $message = 'None of values should satisfy one of the following constraints:';
27+
public bool $includeInternalMessages = true;
28+
29+
public function __construct(
30+
mixed $constraints = null,
31+
array $groups = null,
32+
mixed $payload = null,
33+
string $message = null,
34+
bool $includeInternalMessages = null,
35+
) {
36+
parent::__construct($constraints ?? [], $groups, $payload);
37+
38+
$this->message = $message ?? $this->message;
39+
$this->includeInternalMessages = $includeInternalMessages ?? $this->includeInternalMessages;
40+
}
41+
42+
public function getDefaultOption(): ?string
43+
{
44+
return 'constraints';
45+
}
46+
47+
public function getRequiredOptions(): array
48+
{
49+
return ['constraints'];
50+
}
51+
52+
protected function getCompositeOption(): string
53+
{
54+
return 'constraints';
55+
}
56+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\ConstraintValidator;
16+
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
17+
use Symfony\Component\Validator\Exception\UnexpectedValueException;
18+
19+
/**
20+
* @author Tomas Norkūnas <[email protected]>
21+
*/
22+
class NoneValidator extends ConstraintValidator
23+
{
24+
/**
25+
* {@inheritdoc}
26+
*/
27+
public function validate(mixed $value, Constraint $constraint)
28+
{
29+
if (!$constraint instanceof None) {
30+
throw new UnexpectedTypeException($constraint, None::class);
31+
}
32+
33+
if (null === $value) {
34+
return;
35+
}
36+
37+
if (!is_iterable($value)) {
38+
throw new UnexpectedValueException($value, 'iterable');
39+
}
40+
41+
$this->context
42+
->getValidator()
43+
->inContext($this->context)
44+
->validate($value, new Some([
45+
'constraints' => $constraint->constraints,
46+
'exactly' => 0,
47+
'exactMessage' => $constraint->message,
48+
'includeInternalMessages' => $constraint->includeInternalMessages,
49+
]), $this->context->getGroup());
50+
}
51+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
15+
16+
/**
17+
* @Annotation
18+
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
19+
*
20+
* @author Tomas Norkūnas <[email protected]>
21+
*/
22+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
23+
class Some extends Composite
24+
{
25+
public const SOME_TOO_FEW_ERROR = 'a7ea059b-f8e6-4e85-a48a-bc5eddc0103b';
26+
public const SOME_TOO_MANY_ERROR = '63d385ab-9101-4195-bc32-7283e13a5283';
27+
public const SOME_EXACTLY_ERROR = '6466f661-8b8e-495d-ac96-408aa2e7ee33';
28+
29+
protected static $errorNames = [
30+
self::SOME_TOO_FEW_ERROR => 'SOME_TOO_FEW_ERROR',
31+
self::SOME_TOO_MANY_ERROR => 'SOME_TOO_MANY_ERROR',
32+
self::SOME_EXACTLY_ERROR => 'SOME_EXACTLY_ERROR',
33+
];
34+
35+
public array $constraints = [];
36+
public int $min = 1;
37+
public ?int $max = null;
38+
public string $minMessage = 'At least {{ limit }} value should satisfy one of the following constraints:|At least {{ limit }} values should satisfy one of the following constraints:';
39+
public string $maxMessage = 'At most {{ limit }} value should satisfy one of the following constraints:|At most {{ limit }} values should satisfy one of the following constraints:';
40+
public string $exactMessage = 'Exactly {{ limit }} value should satisfy one of the following constraints:|Exactly {{ limit }} values should satisfy one of the following constraints:';
41+
public bool $includeInternalMessages = true;
42+
43+
public function __construct(
44+
array $options = null,
45+
mixed $constraints = null,
46+
int $exactly = null,
47+
int $min = null,
48+
int $max = null,
49+
array $groups = null,
50+
mixed $payload = null,
51+
string $minMessage = null,
52+
string $maxMessage = null,
53+
string $exactMessage = null,
54+
bool $includeInternalMessages = null,
55+
) {
56+
$exactly ??= $options['exactly'] ?? null;
57+
58+
if (null !== $exactly && null === $min && null === $max) {
59+
$min = $max = $exactly;
60+
}
61+
62+
if (is_array($constraints)) {
63+
$options['constraints'] = $constraints;
64+
}
65+
66+
unset($options['exactly']);
67+
68+
parent::__construct($options, $groups, $payload);
69+
70+
$this->min = $min ?? $this->min;
71+
$this->max = $max ?? $this->max;
72+
$this->minMessage = $minMessage ?? $this->minMessage;
73+
$this->maxMessage = $maxMessage ?? $this->maxMessage;
74+
$this->exactMessage = $exactMessage ?? $this->exactMessage;
75+
$this->includeInternalMessages = $includeInternalMessages ?? $this->includeInternalMessages;
76+
77+
if ($this->min < 0) {
78+
throw new ConstraintDefinitionException('The "min" option must be greater than 0.');
79+
}
80+
}
81+
82+
public function getDefaultOption(): ?string
83+
{
84+
return 'constraints';
85+
}
86+
87+
public function getRequiredOptions(): array
88+
{
89+
return ['constraints'];
90+
}
91+
92+
protected function getCompositeOption(): string
93+
{
94+
return 'constraints';
95+
}
96+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\ConstraintValidator;
16+
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
17+
use Symfony\Component\Validator\Exception\UnexpectedValueException;
18+
19+
/**
20+
* @author Tomas Norkūnas <[email protected]>
21+
*/
22+
class SomeValidator extends ConstraintValidator
23+
{
24+
/**
25+
* {@inheritdoc}
26+
*/
27+
public function validate(mixed $value, Constraint $constraint)
28+
{
29+
if (!$constraint instanceof Some) {
30+
throw new UnexpectedTypeException($constraint, Some::class);
31+
}
32+
33+
if (null === $value) {
34+
return;
35+
}
36+
37+
if (!is_iterable($value)) {
38+
throw new UnexpectedValueException($value, 'iterable');
39+
}
40+
41+
$validator = $this->context->getValidator();
42+
$atLeastOneOf = new AtLeastOneOf([
43+
'constraints' => $constraint->constraints,
44+
'message' => '',
45+
'includeInternalMessages' => $constraint->includeInternalMessages,
46+
]);
47+
$validValues = 0;
48+
$violationMessages = [];
49+
50+
foreach ($value as $element) {
51+
$executionContext = clone $this->context;
52+
$executionContext->setNode($element, $this->context->getObject(), $this->context->getMetadata(), $this->context->getPropertyPath());
53+
$violations = $validator->inContext($executionContext)->validate($element, $atLeastOneOf, $this->context->getGroup())->getViolations();
54+
55+
if (\count($violations) === 0) {
56+
++$validValues;
57+
} elseif (\count($violationMessages) === 0) {
58+
foreach ($violations as $violation) {
59+
$violationMessages[] = $violation->getMessage();
60+
}
61+
}
62+
}
63+
64+
if ($constraint->min === $constraint->max && $validValues !== $constraint->min) {
65+
$this->context->buildViolation($this->buildMessage($constraint->exactMessage, $violationMessages))
66+
->setParameter('{{ count }}', $this->formatValue($validValues))
67+
->setParameter('{{ limit }}', $this->formatValue($constraint->min))
68+
->setPlural($constraint->min)
69+
->setInvalidValue($value)
70+
->setCode(Some::SOME_EXACTLY_ERROR)
71+
->addViolation();
72+
} elseif ($constraint->max !== null && $validValues > $constraint->max) {
73+
$this->context->buildViolation($this->buildMessage($constraint->maxMessage, $violationMessages))
74+
->setParameter('{{ count }}', $this->formatValue($validValues))
75+
->setParameter('{{ limit }}', $this->formatValue($constraint->max))
76+
->setPlural($constraint->max)
77+
->setInvalidValue($value)
78+
->setCode(Some::SOME_TOO_MANY_ERROR)
79+
->addViolation();
80+
} elseif ($validValues < $constraint->min) {
81+
$this->context->buildViolation($this->buildMessage($constraint->minMessage, $violationMessages))
82+
->setParameter('{{ count }}', $this->formatValue($validValues))
83+
->setParameter('{{ limit }}', $this->formatValue($constraint->min))
84+
->setPlural($constraint->min)
85+
->setInvalidValue($value)
86+
->setCode(Some::SOME_TOO_FEW_ERROR)
87+
->addViolation();
88+
}
89+
}
90+
91+
private function buildMessage(string $message, array $violationMessages): string
92+
{
93+
if (!str_contains($message, '|')) {
94+
return implode('', [$message, ...$violationMessages]);
95+
}
96+
97+
$messageParts = explode('|', $message);
98+
$messageParts = array_map(static fn (string $part) => implode('', [$part, ...$violationMessages]), $messageParts);
99+
100+
return implode('|', $messageParts);
101+
}
102+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Constraints;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Validator\Constraints\None;
16+
use Symfony\Component\Validator\Constraints\Valid;
17+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
18+
19+
/**
20+
* @author Tomas Norkūnas <[email protected]>
21+
*/
22+
class NoneTest extends TestCase
23+
{
24+
public function testRejectNonConstraints()
25+
{
26+
$this->expectException(ConstraintDefinitionException::class);
27+
28+
new None(['foo']);
29+
}
30+
31+
public function testRejectValidConstraint()
32+
{
33+
$this->expectException(ConstraintDefinitionException::class);
34+
35+
new None([new Valid()]);
36+
}
37+
38+
public function testCustomMessage()
39+
{
40+
$constraint = new None(['constraints' => [], 'message' => 'No values should:']);
41+
42+
self::assertSame('No values should:', $constraint->message);
43+
}
44+
}

0 commit comments

Comments
 (0)