Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/Validator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ CHANGELOG
* Add support for the `otherwise` option in the `When` constraint
* Add support for multiple fields containing nested constraints in `Composite` constraints
* Add the `stopOnFirstError` option to the `Unique` constraint to validate all elements
* Add support for closures in the `When` constraint

7.2
---
Expand Down
6 changes: 3 additions & 3 deletions src/Symfony/Component/Validator/Constraints/When.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,21 @@
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class When extends Composite
{
public string|Expression $expression;
public string|Expression|\Closure $expression;
public array|Constraint $constraints = [];
public array $values = [];
public array|Constraint $otherwise = [];

/**
* @param string|Expression|array<string,mixed> $expression The condition to evaluate, written with the ExpressionLanguage syntax
* @param string|Expression|array<string,mixed>|\Closure(object): bool $expression The condition to evaluate, either as a closure or using the ExpressionLanguage syntax
* @param Constraint[]|Constraint|null $constraints One or multiple constraints that are applied if the expression returns true
* @param array<string,mixed>|null $values The values of the custom variables used in the expression (defaults to [])
* @param string[]|null $groups
* @param array<string,mixed>|null $options
* @param Constraint[]|Constraint $otherwise One or multiple constraints that are applied if the expression returns false
*/
#[HasNamedArguments]
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 = [])
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 = [])
{
if (!class_exists(ExpressionLanguage::class)) {
throw new LogicException(\sprintf('The "symfony/expression-language" component is required to use the "%s" constraint. Try running "composer require symfony/expression-language".', __CLASS__));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ public function validate(mixed $value, Constraint $constraint): void
$variables['this'] = $context->getObject();
$variables['context'] = $context;

$result = $this->getExpressionLanguage()->evaluate($constraint->expression, $variables);
if ($constraint->expression instanceof \Closure) {
$result = ($constraint->expression)($context->getObject());
} else {
$result = $this->getExpressionLanguage()->evaluate($constraint->expression, $variables);
}

if ($result) {
$context->getValidator()->inContext($context)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Tests\Constraints\Fixtures;

use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Constraints\When;

#[When(expression: static function () {
return true;
}, constraints: new NotNull()
)]
class WhenTestWithClosure
{
#[When(expression: static function () {
return true;
}, constraints: [
new NotNull(),
new NotBlank(),
])]
private $foo;
}
35 changes: 35 additions & 0 deletions src/Symfony/Component/Validator/Tests/Constraints/WhenTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Loader\AttributeLoader;
use Symfony\Component\Validator\Tests\Constraints\Fixtures\WhenTestWithAttributes;
use Symfony\Component\Validator\Tests\Constraints\Fixtures\WhenTestWithClosure;

final class WhenTest extends TestCase
{
Expand Down Expand Up @@ -111,4 +112,38 @@ public function testAttributes()
self::assertEquals([new Length(exactly: 10, groups: ['foo'])], $quuxConstraint->otherwise);
self::assertSame(['foo'], $quuxConstraint->groups);
}

/**
* @requires PHP 8.5
*/
public function testAttributesWithClosure()
{
$loader = new AttributeLoader();
$metadata = new ClassMetadata(WhenTestWithClosure::class);

self::assertTrue($loader->loadClassMetadata($metadata));

[$classConstraint] = $metadata->getConstraints();

self::assertInstanceOf(When::class, $classConstraint);
self::assertInstanceOf(\Closure::class, $classConstraint->expression);
self::assertEquals([
new Callback(
callback: 'callback',
groups: ['Default', 'WhenTestWithClosure'],
),
], $classConstraint->constraints);
self::assertEmpty($classConstraint->otherwise);

[$fooConstraint] = $metadata->properties['foo']->getConstraints();

self::assertInstanceOf(When::class, $fooConstraint);
self::assertInstanceOf(\Closure::class, $fooConstraint->expression);
self::assertEquals([
new NotNull(groups: ['Default', 'WhenTestWithClosure']),
new NotBlank(groups: ['Default', 'WhenTestWithClosure']),
], $fooConstraint->constraints);
self::assertEmpty($fooConstraint->otherwise);
self::assertSame(['Default', 'WhenTestWithClosure'], $fooConstraint->groups);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,34 @@ public function testConstraintsAreExecuted()
));
}

public function testConstraintsAreExecutedWhenClosureIsTrue()
{
$constraints = [
new NotNull(),
new NotBlank(),
];

$this->expectValidateValue(0, 'Foo', $constraints);

$this->validator->validate('Foo', new When(
expression: static fn () => true,
constraints: $constraints,
));
}

public function testClosureTakesSubject()
{
$subject = new \stdClass();
$this->setObject($subject);

$this->validator->validate($subject, new When(
expression: static function ($closureSubject) use ($subject) {
self::assertSame($subject, $closureSubject);
},
constraints: new NotNull(),
));
}

public function testConstraintIsExecuted()
{
$constraint = new NotNull();
Expand All @@ -65,6 +93,20 @@ public function testOtherwiseIsExecutedWhenFalse()
));
}

public function testOtherwiseIsExecutedWhenClosureReturnsFalse()
{
$constraint = new NotNull();
$otherwise = new Length(exactly: 10);

$this->expectValidateValue(0, 'Foo', [$otherwise]);

$this->validator->validate('Foo', new When(
expression: static fn () => false,
constraints: $constraint,
otherwise: $otherwise,
));
}

public function testConstraintsAreExecutedWithNull()
{
$constraints = [
Expand Down
Loading