8000 feature #59800 [Validator] Add support for closures in `When` (alexan… · symfony/symfony@dfd8e53 · GitHub
[go: up one dir, main page]

Skip to content

Commit dfd8e53

Browse files
feature #59800 [Validator] Add support for closures in When (alexandre-daubois)
This PR was merged into the 7.3 branch. Discussion ---------- [Validator] Add support for closures in `When` | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | - | License | MIT Closures can be used instead of Expressions in the `When` constraint since PHP 8.5, like in this example: ```php use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\When; class BlogPost { #[When(expression: static function (BlogPost $subject) { return $subject->published && !$subject->draft; }, constraints: [ new NotNull(), new NotBlank(), ])] public ?string $content; public bool $published; public bool $draft; } ``` Commits ------- f0adbea [Validator] Add support for closures in `When`
2 parents 560a484 + f0adbea commit dfd8e53

File tree

6 files changed

+117
-4
lines changed

6 files changed

+117
-4
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CHANGELOG
1111
* Add support for the `otherwise` option in the `When` constraint
1212
* Add support for multiple fields containing nested constraints in `Composite` constraints
1313
* Add the `stopOnFirstError` option to the `Unique` constraint to validate all elements
14+
* Add support for closures in the `When` constraint
1415

1516
7.2
1617
---

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,21 @@
2525
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
2626
class When extends Composite
2727
{
28-
public string|Expression $expression;
28+
public string|Expression|\Closure $expression;
2929
public array|Constraint $constraints = [];
3030
public array $values = [];
3131
public array|Constraint $otherwise = [];
3232

3333
/**
34-
* @param string|Expression|array<string,mixed> $expression The condition to evaluate, written with the ExpressionLanguage syntax
34+
* @param string|Expression|array<string,mixed>|\Closure(object): bool $expression The condition to evaluate, either as a closure or using the ExpressionLanguage syntax
3535
* @param Constraint[]|Constraint|null $constraints One or multiple constraints that are applied if the expression returns true
3636
* @param array<string,mixed>|null $values The values of the custom variables used in the expression (defaults to [])
3737
* @param string[]|null $groups
3838
* @param array<string,mixed>|null $options
3939
* @param Constraint[]|Constraint $otherwise One or multiple constraints that are applied if the expression returns false
4040
*/
4141
#[HasNamedArguments]
42-
public function __construct(string|Expression|array $expression, array|Constraint|null $constraints = null, ?array $values = null, ?array $groups = null, $payload = null, ?array $options = null, array|Constraint $otherwise = [])
42+
public function __construct(string|Expression|array|\Closure $expression, array|Constraint|null $constraints = null, ?array $values = null, ?array $groups = null, $payload = null, ?array $options = null, array|Constraint $otherwise = [])
4343
{
4444
if (!class_exists(ExpressionLanguage::class)) {
4545
throw new LogicException(\sprintf('The "symfony/expression-language" component is required to use the "%s" constraint. Try running "composer require symfony/expression-language".', __CLASS__));

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ public function validate(mixed $value, Constraint $constraint): void
3535
$variables['this'] = $context->getObject();
3636
$variables['context'] = $context;
3737

38-
$result = $this->getExpressionLanguage()->evaluate($constraint->expression, $variables);
38+
if ($constraint->expression instanceof \Closure) {
39+
$result = ($constraint->expression)($context->getObject());
40+
} else {
41+
$result = $this->getExpressionLanguage()->evaluate($constraint->expression, $variables);
42+
}
3943

4044
if ($result) {
4145
$context->getValidator()->inContext($context)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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\Fixtures;
13+
14+
use Symfony\Component\Validator\Constraints\NotBlank;
15+
use Symfony\Component\Validator\Constraints\NotNull;
16+
use Symfony\Component\Validator\Constraints\When;
17+
18+
#[When(expression: static function () {
19+
return true;
20+
}, constraints: new NotNull()
21+
)]
22+
class WhenTestWithClosure
23+
{
24+
#[When(expression: static function () {
25+
return true;
26+
}, constraints: [
27+
new NotNull(),
28+
new NotBlank(),
29+
])]
30+
private $foo;
31+
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\Validator\Mapping\ClassMetadata;
2323
use Symfony\Component\Validator\Mapping\Loader\AttributeLoader;
2424
use Symfony\Component\Validator\Tests\Constraints\Fixtures\WhenTestWithAttributes;
25+
use Symfony\Component\Validator\Tests\Constraints\Fixtures\WhenTestWithClosure;
2526

2627
final class WhenTest extends TestCase
2728
{
@@ -111,4 +112,38 @@ public function testAttributes()
111112
self::assertEquals([new Length(exactly: 10, groups: ['foo'])], $quuxConstraint->otherwise);
112113
self::assertSame(['foo'], $quuxConstraint->groups);
113114
}
115+
116+
/**
117+
* @requires PHP 8.5
118+
*/
119+
public function testAttributesWithClosure()
120+
{
121+
$loader = new AttributeLoader();
122+
$metadata = new ClassMetadata(WhenTestWithClosure::class);
123+
124+
self::assertTrue($loader->loadClassMetadata($metadata));
125+
126+
[$classConstraint] = $metadata->getConstraints();
127+
128+
self::assertInstanceOf(When::class, $classConstraint);
129+
self::assertInstanceOf(\Closure::class, $classConstraint->expression);
130+
self::assertEquals([
131+
new Callback(
132+
callback: 'callback',
133+
groups: ['Default', 'WhenTestWithClosure'],
134+
),
135+
], $classConstraint->constraints);
136+
self::assertEmpty($classConstraint->otherwise);
137+
138+
[$fooConstraint] = $metadata->properties['foo']->getConstraints();
139+
140+
self::assertInstanceOf(When::class, $fooConstraint);
141+
self::assertInstanceOf(\Closure::class, $fooConstraint->expression);
142+
self::assertEquals([
143+
new NotNull(groups: ['Default', 'WhenTestWithClosure']),
144+
new NotBlank(groups: ['Default', 'WhenTestWithClosure']),
145+
], $fooConstraint->constraints);
146+
self::assertEmpty($fooConstraint->otherwise);
147+
self::assertSame(['Default', 'WhenTestWithClosure'], $fooConstraint->groups);
148+
}
114149
}

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,34 @@ public function testConstraintsAreExecuted()
4040
));
4141
}
4242

43+
public function testConstraintsAreExecutedWhenClosureIsTrue()
44+
{
45+
$constraints = [
46+
new NotNull(),
47+
new NotBlank(),
48+
];
49+
50+
$this->expectValidateValue(0, 'Foo', $constraints);
51+
52+
$this->validator->validate('Foo', new When(
53+
expression: static fn () => true,
54+
constraints: $constraints,
55+
));
56+
}
57+
58+
public function testClosureTakesSubject()
59+
{
60+
$subject = new \stdClass();
61+
$this->setObject($subject);
62+
63+
$this->validator->validate($subject, new When(
64+
expression: static function ($closureSubject) use ($subject) {
65+
self::assertSame($subject, $closureSubject);
66+
},
67+
constraints: new NotNull(),
68+
));
69+
}
70+
4371
public function testConstraintIsExecuted()
4472
{
4573
$constraint = new NotNull();
@@ -65,6 +93,20 @@ public function testOtherwiseIsExecutedWhenFalse()
6593
));
6694
}
6795

96+
public function testOtherwiseIsExecutedWhenClosureReturnsFalse()
97+
{
98+
$constraint = new NotNull();
99+
$otherwise = new Length(exactly: 10);
100+
101+
$this->expectValidateValue(0, 'Foo', [$otherwise]);
102+
103+
$this->validator->validate('Foo', new When(
104+
expression: static fn () => false,
105+
constraints: $constraint,
106+
otherwise: $otherwise,
107+
));
108+
}
109+
68110
public function testConstraintsAreExecutedWithNull()
69111
{
70112
$constraints = [

0 commit comments

Comments
 (0)
0