Skip to content

Commit 064b461

Browse files
committed
feature #42593 [Validator] Add the When constraint and validator (wuchen90)
This PR was merged into the 6.2 branch. Discussion ---------- [Validator] Add the `When` constraint and validator | Q | A | ------------- | --- | Branch? | 6.2 | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | License | MIT | Doc PR | symfony/symfony-docs#15722 This constraint allows you to apply constraints validation only if the provided condition is matched. Usage: ```php namespace App\Model; use Symfony\Component\Validator\Constraints as Assert; class Discount { private $type; // 'percent' or 'absolute' /** * `@Assert`\GreaterThan(0) * `@Assert`\When( * expression="this.type == 'percent'", * constraints={`@LessThan`(100, message="The value should be between 0 and 100!")} * ) */ private $value; // ... } ``` See the documentation for details. Commits ------- 9b7bdc9b18 [Validator] Add the When constraint and validator
2 parents 304b72f + b9a2d5b commit 064b461

File tree

6 files changed

+586
-0
lines changed

6 files changed

+586
-0
lines changed

CHANGELOG.md

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

7+
* Add the `When` constraint and validator
78
* Deprecate the "loose" e-mail validation mode, use "html5" instead
89
* Add the `negate` option to the `Expression` constraint, to inverse the logic of the violation's creation
910

Constraints/When.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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\ExpressionLanguage\Expression;
15+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
16+
use Symfony\Component\Validator\Exception\LogicException;
17+
18+
/**
19+
* @Annotation
20+
* @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"})
21+
*/
22+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
23+
class When extends Composite
24+
{
25+
public $expression;
26+
public $constraints = [];
27+
public $values = [];
28+
29+
public function __construct(string|Expression|array $expression, array $constraints = null, array $values = null, array $groups = null, $payload = null, array $options = [])
30+
{
31+
if (!class_exists(ExpressionLanguage::class)) {
32+
throw new LogicException(sprintf('The "symfony/expression-language" component is required to use the "%s" constraint. Try running "composer require symfony/expression-language".', __CLASS__));
33+
}
34+
35+
if (\is_array($expression)) {
36+
$options = array_merge($expression, $options);
37+
} else {
38+
$options['expression'] = $expression;
39+
$options['constraints'] = $constraints;
40+
}
41+
42+
if (null !== $groups) {
43+
$options['groups'] = $groups;
44+
}
45+
46+
if (null !== $payload) {
47+
$options['payload'] = $payload;
48+
}
49+
50+
parent::__construct($options);
51+
52+
$this->values = $values ?? $this->values;
53+
}
54+
55+
public function getRequiredOptions(): array
56+
{
57+
return ['expression', 'constraints'];
58+
}
59+
60+
public function getTargets(): string|array
61+
{
62+
return [self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT];
63+
}
64+
65+
protected function getCompositeOption(): string
66+
{
67+
return 'constraints';
68+
}
69+
}

Constraints/WhenValidator.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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\ExpressionLanguage\ExpressionLanguage;
15+
use Symfony\Component\Validator\Constraint;
16+
use Symfony\Component\Validator\ConstraintValidator;
17+
use Symfony\Component\Validator\Exception\LogicException;
18+
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
19+
20+
final class WhenValidator extends ConstraintValidator
21+
{
22+
private ?ExpressionLanguage $expressionLanguage;
23+
24+
public function __construct(ExpressionLanguage $expressionLanguage = null)
25+
{
26+
$this->expressionLanguage = $expressionLanguage;
27+
}
28+
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public function validate(mixed $value, Constraint $constraint): void
33+
{
34+
if (!$constraint instanceof When) {
35+
throw new UnexpectedTypeException($constraint, When::class);
36+
}
37+
38+
$context = $this->context;
39+
$variables = $constraint->values;
40+
$variables['value'] = $value;
41+
$variables['this'] = $context->getObject();
42+
43+
if ($this->getExpressionLanguage()->evaluate($constraint->expression, $variables)) {
44+
$context->getValidator()->inContext($context)
45+
->validate($value, $constraint->constraints);
46+
}
47+
}
48+
49+
private function getExpressionLanguage(): ExpressionLanguage
50+
{
51+
if (null !== $this->expressionLanguage) {
52+
return $this->expressionLanguage;
53+
}
54+
55+
if (!class_exists(ExpressionLanguage::class)) {
56+
throw new LogicException(sprintf('The "symfony/expression-language" component is required to use the "%s" validator. Try running "composer require symfony/expression-language".', __CLASS__));
57+
}
58+
59+
return $this->expressionLanguage = new ExpressionLanguage();
60+
}
61+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Symfony\Component\Validator\Tests\Constraints\Fixtures;
4+
5+
use Symfony\Component\Validator\Constraints\Callback;
6+
use Symfony\Component\Validator\Constraints\NotBlank;
7+
use Symfony\Component\Validator\Constraints\NotNull;
8+
use Symfony\Component\Validator\Constraints\When;
9+
10+
#[When(expression: 'true', constraints: [
11+
new Callback('callback'),
12+
])]
13+
class WhenTestWithAttributes
14+
{
15+
#[When(expression: 'true', constraints: [
16+
new NotNull(),
17+
new NotBlank(),
18+
])]
19+
private $foo;
20+
21+
#[When(expression: 'false', constraints: [
22+
new NotNull(),
23+
new NotBlank(),
24+
], groups: ['foo'])]
25+
private $bar;
26+
27+
#[When(expression: 'true', constraints: [
28+
new NotNull(),
29+
new NotBlank(),
30+
])]
31+
public function getBaz()
32+
{
33+
return null;
34+
}
35+
36+
public function callback()
37+
{
38+
}
39+
}

Tests/Constraints/WhenTest.php

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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 Doctrine\Common\Annotations\AnnotationReader;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\Validator\Constraints\Callback;
17+
use Symfony\Component\Validator\Constraints\NotBlank;
18+
use Symfony\Component\Validator\Constraints\NotNull;
19+
use Symfony\Component\Validator\Constraints\When;
20+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
21+
use Symfony\Component\Validator\Exception\MissingOptionsException;
22+
use Symfony\Component\Validator\Mapping\ClassMetadata;
23+
use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader;
24+
use Symfony\Component\Validator\Tests\Constraints\Fixtures\WhenTestWithAttributes;
25+
26+
final class WhenTest extends TestCase
27+
{
28+
public function testMissingOptionsExceptionIsThrown()
29+
{
30+
$this->expectException(MissingOptionsException::class);
31+
$this->expectExceptionMessage('The options "expression", "constraints" must be set for constraint "Symfony\Component\Validator\Constraints\When".');
32+
33+
new When([]);
34+
}
35+
36+
public function testNonConstraintsAreRejected()
37+
{
38+
$this->expectException(ConstraintDefinitionException::class);
39+
$this->expectExceptionMessage('The value "foo" is not an instance of Constraint in constraint "Symfony\Component\Validator\Constraints\When"');
40+
new When('true', [
41+
'foo',
42+
]);
43+
}
44+
45+
public function testAnnotations()
46+
{
47+
$loader = new AnnotationLoader(new AnnotationReader());
48+
$metadata = new ClassMetadata(WhenTestWithAnnotations::class);
49+
50+
self::assertTrue($loader->loadClassMetadata($metadata));
51+
52+
[$classConstraint] = $metadata->getConstraints();
53+
54+
self::assertInstanceOf(When::class, $classConstraint);
55+
self::assertSame('true', $classConstraint->expression);
56+
self::assertEquals([
57+
new Callback([
58+
'callback' => 'callback',
59+
'groups' => ['Default', 'WhenTestWithAnnotations'],
60+
]),
61+
], $classConstraint->constraints);
62+
63+
[$fooConstraint] = $metadata->properties['foo']->getConstraints();
64+
65+
self::assertInstanceOf(When::class, $fooConstraint);
66+
self::assertSame('true', $fooConstraint->expression);
67+
self::assertEquals([
68+
new NotNull([
69+
'groups' => ['Default', 'WhenTestWithAnnotations'],
70+
]),
71+
new NotBlank([
72+
'groups' => ['Default', 'WhenTestWithAnnotations'],
73+
]),
74+
], $fooConstraint->constraints);
75+
self::assertSame(['Default', 'WhenTestWithAnnotations'], $fooConstraint->groups);
76+
77+
[$barConstraint] = $metadata->properties['bar']->getConstraints();
78+
79+
self::assertInstanceOf(When::class, $fooConstraint);
80+
self::assertSame('false', $barConstraint->expression);
81+
self::assertEquals([
82+
new NotNull([
83+
'groups' => ['foo'],
84+
]),
85+
new NotBlank([
86+
'groups' => ['foo'],
87+
]),
88+
], $barConstraint->constraints);
89+
self::assertSame(['foo'], $barConstraint->groups);
90+
91+
[$bazConstraint] = $metadata->getters['baz']->getConstraints();
92+
93+
self::assertInstanceOf(When::class, $bazConstraint);
94+
self::assertSame('true', $bazConstraint->expression);
95+
self::assertEquals([
96+
new NotNull([
97+
'groups' => ['Default', 'WhenTestWithAnnotations'],
98+
]),
99+
new NotBlank([
100+
'groups' => ['Default', 'WhenTestWithAnnotations'],
101+
]),
102+
], $bazConstraint->constraints);
103+
self::assertSame(['Default', 'WhenTestWithAnnotations'], $bazConstraint->groups);
104+
}
105+
106+
/**
107+
* @requires PHP 8.1
108+
*/
109+
public function testAttributes()
110+
{
111+
$loader = new AnnotationLoader(new AnnotationReader());
112+
$metadata = new ClassMetadata(WhenTestWithAttributes::class);
113+
114+
self::assertTrue($loader->loadClassMetadata($metadata));
115+
116+
[$classConstraint] = $metadata->getConstraints();
117+
118+
self::assertInstanceOf(When::class, $classConstraint);
119+
self::assertSame('true', $classConstraint->expression);
120+
self::assertEquals([
121+
new Callback([
122+
'callback' => 'callback',
123+
'groups' => ['Default', 'WhenTestWithAttributes'],
124+
]),
125+
], $classConstraint->constraints);
126+
127+
[$fooConstraint] = $metadata->properties['foo']->getConstraints();
128+
129+
self::assertInstanceOf(When::class, $fooConstraint);
130+
self::assertSame('true', $fooConstraint->expression);
131+
self::assertEquals([
132+
new NotNull([
133+
'groups' => ['Default', 'WhenTestWithAttributes'],
134+
]),
135+
new NotBlank([
136+
'groups' => ['Default', 'WhenTestWithAttributes'],
137+
]),
138+
], $fooConstraint->constraints);
139+
self::assertSame(['Default', 'WhenTestWithAttributes'], $fooConstraint->groups);
140+
141+
[$barConstraint] = $metadata->properties['bar']->getConstraints();
142+
143+
self::assertInstanceOf(When::class, $fooConstraint);
144+
self::assertSame('false', $barConstraint->expression);
145+
self::assertEquals([
146+
new NotNull([
147+
'groups' => ['foo'],
148+
]),
149+
new NotBlank([
150+
'groups' => ['foo'],
151+
]),
152+
], $barConstraint->constraints);
153+
self::assertSame(['foo'], $barConstraint->groups);
154+
155+
[$bazConstraint] = $metadata->getters['baz']->getConstraints();
156+
157+
self::assertInstanceOf(When::class, $bazConstraint);
158+
self::assertSame('true', $bazConstraint->expression);
159+
self::assertEquals([
160+
new NotNull([
161+
'groups' => ['Default', 'WhenTestWithAttributes'],
162+
]),
163+
new NotBlank([
164+
'groups' => ['Default', 'WhenTestWithAttributes'],
165+
]),
166+
], $bazConstraint->constraints);
167+
self::assertSame(['Default', 'WhenTestWithAttributes'], $bazConstraint->groups);
168+
}
169+
}
170+
171+
/**
172+
* @When(expression="true", constraints={@Callback("callback")})
173+
*/
174+
class WhenTestWithAnnotations
175+
{
176+
/**
177+
* @When(expression="true", constraints={@NotNull, @NotBlank})
178+
*/
179+
private $foo;
180+
181+
/**
182+
* @When(expression="false", constraints={@NotNull, @NotBlank}, groups={"foo"})
183+
*/
184+
private $bar;
185+
186+
/**
187+
* @When(expression="true", constraints={@NotNull, @NotBlank})
188+
*/
189+
public function getBaz()
190+
{
191+
return null;
192+
}
193+
194+
public function callback()
195+
{
196+
}
197+
}

0 commit comments

Comments
 (0)