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);
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
$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