feature #35744 [Validator] Add AtLeastOne constraint and validator (przemyslaw-bogusz)
This PR was squashed before being merged into the 5.1-dev branch.
Discussion
----------
[Validator] Add AtLeastOne constraint and validator
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| Deprecations? | no
| License | MIT
| Doc PR | TODO
This constraint allows you to apply a collection of constraints to a value, and it will be considered valid, if it satisfies at least one of the constraints from the collection.
Some examples:
```php
/**
* @Assert\AtLeastOne({
* @Assert\Length(min=5),
* @Assert\EqualTo("bar")
* })
*/
public $name = 'foo';
/**
* @Assert\AtLeastOne({
* @Assert\All({@Assert\GreaterThanOrEqual(10)}),
* @Assert\Count(20)
* })
*/
public $numbers = ['3', '5'];
/**
* @Assert\All({
* @Assert\AtLeastOne({
* @Assert\GreaterThanOrEqual(5),
* @Assert\LessThanOrEqual(3)
* })
* })
*/
public $otherNumbers = ['4', '5'];
```
The respective default messages would be:
`name: This value should satisfy at least one of the following constraints: [1] This value is too short. It should have 5 characters or more. [2] This value should be equal to "bar".`
`numbers: This value should satisfy at least one of the following constraints: [1] Each element of this collection should satisfy its own set of constraints. [2] This collection should contain exactly 20 elements.`
`otherNumbers[0]: This value should satisfy at least one of the following constraints: [1] This value should be greater than or equal to 5. [2] This value should be less than or equal to 3.`
But of course you could also create a simple custom message like `None of the constraints are satisfied`.
Commits
-------
e6209a697c
[Validator] Add AtLeastOne constraint and validator
This commit is contained in:
commit
b4f03d0c3b
47
src/Symfony/Component/Validator/Constraints/AtLeastOneOf.php
Normal file
47
src/Symfony/Component/Validator/Constraints/AtLeastOneOf.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?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\Constraints;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Annotation
|
||||||
|
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
|
||||||
|
*
|
||||||
|
* @author Przemysław Bogusz <przemyslaw.bogusz@tubotax.pl>
|
||||||
|
*/
|
||||||
|
class AtLeastOneOf extends Composite
|
||||||
|
{
|
||||||
|
public const AT_LEAST_ONE_ERROR = 'f27e6d6c-261a-4056-b391-6673a623531c';
|
||||||
|
|
||||||
|
protected static $errorNames = [
|
||||||
|
self::AT_LEAST_ONE_ERROR => 'AT_LEAST_ONE_ERROR',
|
||||||
|
];
|
||||||
|
|
||||||
|
public $constraints = [];
|
||||||
|
public $message = 'This value should satisfy at least one of the following constraints:';
|
||||||
|
public $messageCollection = 'Each element of this collection should satisfy its own set of constraints.';
|
||||||
|
public $includeInternalMessages = true;
|
||||||
|
|
||||||
|
public function getDefaultOption()
|
||||||
|
{
|
||||||
|
return 'constraints';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRequiredOptions()
|
||||||
|
{
|
||||||
|
return ['constraints'];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getCompositeOption()
|
||||||
|
{
|
||||||
|
return 'constraints';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
<?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\Constraints;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Przemysław Bogusz <przemyslaw.bogusz@tubotax.pl>
|
||||||
|
*/
|
||||||
|
class AtLeastOneOfValidator extends ConstraintValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function validate($value, Constraint $constraint)
|
||||||
|
{
|
||||||
|
if (!$constraint instanceof AtLeastOneOf) {
|
||||||
|
throw new UnexpectedTypeException($constraint, AtLeastOneOf::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validator = $this->context->getValidator();
|
||||||
|
|
||||||
|
$messages = [$constraint->message];
|
||||||
|
|
||||||
|
foreach ($constraint->constraints as $key => $item) {
|
||||||
|
$violations = $validator->validate($value, $item);
|
||||||
|
|
||||||
|
if (0 === \count($violations)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($constraint->includeInternalMessages) {
|
||||||
|
$message = ' ['.($key + 1).'] ';
|
||||||
|
|
||||||
|
if ($item instanceof All || $item instanceof Collection) {
|
||||||
|
$message .= $constraint->messageCollection;
|
||||||
|
} else {
|
||||||
|
$message .= $violations->get(0)->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
$messages[] = $message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->context->buildViolation(implode('', $messages))
|
||||||
|
->setCode(AtLeastOneOf::AT_LEAST_ONE_ERROR)
|
||||||
|
->addViolation()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
@ -203,6 +203,25 @@ abstract class ConstraintValidatorTestCase extends TestCase
|
|||||||
->willReturn($contextualValidator);
|
->willReturn($contextualValidator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function expectViolationsAt($i, $value, Constraint $constraint)
|
||||||
|
{
|
||||||
|
$context = $this->createContext();
|
||||||
|
|
||||||
|
$validatorClassname = $constraint->validatedBy();
|
||||||
|
|
||||||
|
$validator = new $validatorClassname();
|
||||||
|
$validator->initialize($context);
|
||||||
|
$validator->validate($value, $constraint);
|
||||||
|
|
||||||
|
$this->context->getValidator()
|
||||||
|
->expects($this->at($i))
|
||||||
|
->method('validate')
|
||||||
|
->willReturn($context->getViolations())
|
||||||
|
;
|
||||||
|
|
||||||
|
return $context->getViolations();
|
||||||
|
}
|
||||||
|
|
||||||
protected function assertNoViolation()
|
protected function assertNoViolation()
|
||||||
{
|
{
|
||||||
$this->assertSame(0, $violationsCount = \count($this->context->getViolations()), sprintf('0 violation expected. Got %u.', $violationsCount));
|
$this->assertSame(0, $violationsCount = \count($this->context->getViolations()), sprintf('0 violation expected. Got %u.', $violationsCount));
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Validator\Constraints\AtLeastOneOf;
|
||||||
|
use Symfony\Component\Validator\Constraints\Valid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Przemysław Bogusz <przemyslaw.bogusz@tubotax.pl>
|
||||||
|
*/
|
||||||
|
class AtLeastOneOfTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testRejectNonConstraints()
|
||||||
|
{
|
||||||
|
$this->expectException('Symfony\Component\Validator\Exception\ConstraintDefinitionException');
|
||||||
|
new AtLeastOneOf([
|
||||||
|
'foo',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRejectValidConstraint()
|
||||||
|
{
|
||||||
|
$this->expectException('Symfony\Component\Validator\Exception\ConstraintDefinitionException');
|
||||||
|
new AtLeastOneOf([
|
||||||
|
new Valid(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,163 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraints\AtLeastOneOf;
|
||||||
|
use Symfony\Component\Validator\Constraints\AtLeastOneOfValidator;
|
||||||
|
use Symfony\Component\Validator\Constraints\Choice;
|
||||||
|
use Symfony\Component\Validator\Constraints\Count;
|
||||||
|
use Symfony\Component\Validator\Constraints\Country;
|
||||||
|
use Symfony\Component\Validator\Constraints\DivisibleBy;
|
||||||
|
use Symfony\Component\Validator\Constraints\EqualTo;
|
||||||
|
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
|
||||||
|
use Symfony\Component\Validator\Constraints\IdenticalTo;
|
||||||
|
use Symfony\Component\Validator\Constraints\Language;
|
||||||
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
|
use Symfony\Component\Validator\Constraints\LessThan;
|
||||||
|
use Symfony\Component\Validator\Constraints\Negative;
|
||||||
|
use Symfony\Component\Validator\Constraints\Range;
|
||||||
|
use Symfony\Component\Validator\Constraints\Regex;
|
||||||
|
use Symfony\Component\Validator\Constraints\Unique;
|
||||||
|
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Przemysław Bogusz <przemyslaw.bogusz@tubotax.pl>
|
||||||
|
*/
|
||||||
|
class AtLeastOneOfValidatorTest extends ConstraintValidatorTestCase
|
||||||
|
{
|
||||||
|
protected function createValidator()
|
||||||
|
{
|
||||||
|
return new AtLeastOneOfValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getValidCombinations
|
||||||
|
*/
|
||||||
|
public function testValidCombinations($value, $constraints)
|
||||||
|
{
|
||||||
|
$i = 0;
|
||||||
|
|
||||||
|
foreach ($constraints as $constraint) {
|
||||||
|
$this->expectViolationsAt($i++, $value, $constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->validator->validate($value, new AtLeastOneOf($constraints));
|
||||||
|
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getValidCombinations()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['symfony', [
|
||||||
|
new Length(['min' => 10]),
|
||||||
|
new EqualTo(['value' => 'symfony']),
|
||||||
|
]],
|
||||||
|
[150, [
|
||||||
|
new Range(['min' => 10, 'max' => 20]),
|
||||||
|
new GreaterThanOrEqual(['value' => 100]),
|
||||||
|
]],
|
||||||
|
[7, [
|
||||||
|
new LessThan(['value' => 5]),
|
||||||
|
new IdenticalTo(['value' => 7]),
|
||||||
|
]],
|
||||||
|
[-3, [
|
||||||
|
new DivisibleBy(['value' => 4]),
|
||||||
|
new Negative(),
|
||||||
|
]],
|
||||||
|
['FOO', [
|
||||||
|
new Choice(['choices' => ['bar', 'BAR']]),
|
||||||
|
new Regex(['pattern' => '/foo/i']),
|
||||||
|
]],
|
||||||
|
['fr', [
|
||||||
|
new Country(),
|
||||||
|
new Language(),
|
||||||
|
]],
|
||||||
|
[[1, 3, 5], [
|
||||||
|
new Count(['min' => 5]),
|
||||||
|
new Unique(),
|
||||||
|
]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getInvalidCombinations
|
||||||
|
*/
|
||||||
|
public function testInvalidCombinationsWithDefaultMessage($value, $constraints)
|
||||||
|
{
|
||||||
|
$atLeastOneOf = new AtLeastOneOf(['constraints' => $constraints]);
|
||||||
|
|
||||||
|
$message = [$atLeastOneOf->message];
|
||||||
|
|
||||||
|
$i = 0;
|
||||||
|
|
||||||
|
foreach ($constraints as $constraint) {
|
||||||
|
$message[] = ' ['.($i + 1).'] '.$this->expectViolationsAt($i++, $value, $constraint)->get(0)->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->validator->validate($value, $atLeastOneOf);
|
||||||
|
|
||||||
|
$this->buildViolation(implode('', $message))->setCode(AtLeastOneOf::AT_LEAST_ONE_ERROR)->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getInvalidCombinations
|
||||||
|
*/
|
||||||
|
public function testInvalidCombinationsWithCustomMessage($value, $constraints)
|
||||||
|
{
|
||||||
|
$atLeastOneOf = new AtLeastOneOf(['constraints' => $constraints, 'message' => 'foo', 'includeInternalMessages' => false]);
|
||||||
|
|
||||||
|
$i = 0;
|
||||||
|
|
||||||
|
foreach ($constraints as $constraint) {
|
||||||
|
$this->expectViolationsAt($i++, $value, $constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->validator->validate($value, $atLeastOneOf);
|
||||||
|
|
||||||
|
$this->buildViolation('foo')->setCode(AtLeastOneOf::AT_LEAST_ONE_ERROR)->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getInvalidCombinations()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['symphony', [
|
||||||
|
new Length(['min' => 10]),
|
||||||
|
new EqualTo(['value' => 'symfony']),
|
||||||
|
]],
|
||||||
|
[70, [
|
||||||
|
new Range(['min' => 10, 'max' => 20]),
|
||||||
|
new GreaterThanOrEqual(['value' => 100]),
|
||||||
|
]],
|
||||||
|
[8, [
|
||||||
|
new LessThan(['value' => 5]),
|
||||||
|
new IdenticalTo(['value' => 7]),
|
||||||
|
]],
|
||||||
|
[3, [
|
||||||
|
new DivisibleBy(['value' => 4]),
|
||||||
|
new Negative(),
|
||||||
|
]],
|
||||||
|
['F_O_O', [
|
||||||
|
new Choice(['choices' => ['bar', 'BAR']]),
|
||||||
|
new Regex(['pattern' => '/foo/i']),
|
||||||
|
]],
|
||||||
|
['f_r', [
|
||||||
|
new Country(),
|
||||||
|
new Language(),
|
||||||
|
]],
|
||||||
|
[[1, 3, 3], [
|
||||||
|
new Count(['min' => 5]),
|
||||||
|
new Unique(),
|
||||||
|
]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user