8000 [Validator] Add a constraint to sequentially validate a set of constr… · symfony/symfony@dfd9038 · GitHub
[go: up one dir, main page]

Skip to content

Commit dfd9038

Browse files
committed
[Validator] Add a constraint to sequentially validate a set of constraints
1 parent 83a53a5 commit dfd9038

File tree

5 files changed

+211
-0
lines changed

5 files changed

+211
-0
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

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

77
* added the `Hostname` constraint and validator
88
* added option `alpha3` to `Country` constraint
9+
* added `Sequentially` constraint, to sequentially validate a set of constraints (any violation raised will prevent further validation of the nested constraints)
910

1011
5.0.0
1112
-----
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
/**
15+
* Use this constraint to sequentially validate nested constraints.
16+
* Validation for the nested constraints collection will stop at first violation.
17+
*
18+
* @Annotation
19+
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
20+
*
21+
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
22+
*/
23+
class Sequentially extends Composite
24+
{
25+
public $constraints = [];
26+
27+
public function getDefaultOption()
28+
{
29+
return 'constraints';
30+
}
31+
32+
public function getRequiredOptions()
33+
{
34+
return ['constraints'];
35+
}
36+
37+
protected function getCompositeOption()
38+
{
39+
return 'constraints';
40+
}
41+
}
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 <fabien@symfony.com>
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+
18+
/**
19+
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
20+
*/
21+
class SequentiallyValidator extends ConstraintValidator
22+
{
23+
/**
24+
* {@inheritdoc}
25+
*/
26+
public function validate($value, Constraint $constraint)
27+
{
28+
if (!$constraint instanceof Sequentially) {
29+
throw new UnexpectedTypeException($constraint, Sequentially::class);
30+
}
31+
32+
$context = $this->context;
33+
34+
$validator = $context->getValidator()->inContext($context);
35+
36+
$originalCount = $validator->getViolations()->count();
37+
38+
foreach ($constraint->constraints as $c) {
39+
if ($originalCount !== $validator->validate($value, $c)->getViolations()->count()) {
40+
break;
41+
}
42+
}
43+
}
44+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Sequentially;
16+
use Symfony\Component\Validator\Constraints\Valid;
17+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
18+
19+
class SequentiallyTest extends TestCase
20+
{
21+
public function testRejectNonConstraints()
22+
{
23+
$this->expectException(ConstraintDefinitionException::class);
24+
$this->expectExceptionMessage('The value foo is not an instance of Constraint in constraint Symfony\Component\Validator\Constraints\Sequentially');
25+
new Sequentially([
26+
'foo',
27+
]);
28+
}
29+
30+
public function testRejectValidConstraint()
31+
{
32+
$this->expectException(ConstraintDefinitionException::class);
33+
$this->expectExceptionMessage('The constraint Valid cannot be nested inside constraint Symfony\Component\Validator\Constraints\Sequentially');
34+
new Sequentially([
35+
new Valid(),
36+
]);
37+
}
38+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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 Symfony\Component\Validator\Constraints\NotEqualTo;
15+
use Symfony\Component\Validator\Constraints\Range;
16+
use Symfony\Component\Validator\Constraints\Regex;
17+
use Symfony\Component\Validator\Constraints\Sequentially;
18+
use Symfony\Component\Validator\Constraints\SequentiallyValidator;
19+
use Symfony\Component\Validator\Constraints\Type;
20+
use Symfony\Component\Validator\ConstraintViolation;
21+
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
22+
23+
class SequentiallyValidatorTest extends ConstraintValidatorTestCase
24+
{
25+
protected function createValidator()
26+
{
27+
return new SequentiallyValidator();
28+
}
29+
30+
public function testWalkThroughConstraints()
31+
{
32+
$constraints = [
33+
new Type('number'),
34+
new Range(['min' => 4]),
35+
];
36+
37+
$value = 6;
38+
39+
$contextualValidator = $this->context->getValidator()->inContext($this->context);
40+
$contextualValidator->expects($this->any())->method('getViolations')->willReturn($this->context->getViolations());
41+
$contextualValidator->expects($this->exactly(2))
42+
->method('validate')
43+
->withConsecutive(
44+
[$value, $constraints[0]],
45+
[$value, $constraints[1]]
46+
)
47+
->willReturn($contextualValidator);
48+
49+
$this->validator->validate($value, new Sequentially($constraints));
50+
51+
$this->assertNoViolation();
52+
}
53+
54+
public function testStopsAtFirstConstraintWithViolations()
55+
{
56+
$constraints = [
57+
new Type('string'),
58+
new Regex(['pattern' => '[a-z]']),
59+
new NotEqualTo('Foo'),
60+
];
61+
62+
$value = 'Foo';
63+
64+
$contextualValidator = $this->context->getValidator()->inContext($this->context);
65+
$contextualValidator->expects($this->any())->method('getViolations')->willReturn($this->context->getViolatio 1241 ns());
66+
$contextualValidator->expects($this->exactly(2))
67+
->method('validate')
68+
->withConsecutive(
69+
[$value, $constraints[0]],
70+
[$value, $constraints[1]]
71+
)
72+
->will($this->onConsecutiveCalls(
73+
// Noop, just return the validator:
74+
$this->returnValue($contextualValidator),
75+
// Add violation on second call:
76+
$this->returnCallback(function () use ($contextualValidator) {
77+
$this->context->getViolations()->add($violation = new ConstraintViolation('regex error', null, [], null, '', null, null, 'regex'));
78+
79+
return $contextualValidator;
80+
}
81+
)));
82+
83+
$this->validator->validate($value, new Sequentially($constraints));
84+
85+
$this->assertCount(1, $this->context->getViolations());
86+
}
87+
}

0 commit comments

Comments
 (0)
0