Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature #35744 [Validator] Add AtLeastOne constraint and validator (p…
…rzemyslaw-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 ------- e6209a6 [Validator] Add AtLeastOne constraint and validator
- Loading branch information
Showing
5 changed files
with
328 additions
and
0 deletions.
There are no files selected for viewing
47 changes: 47 additions & 0 deletions
47
src/Symfony/Component/Validator/Constraints/AtLeastOneOf.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'; | ||
} | ||
} |
61 changes: 61 additions & 0 deletions
61
src/Symfony/Component/Validator/Constraints/AtLeastOneOfValidator.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
38 changes: 38 additions & 0 deletions
38
src/Symfony/Component/Validator/Tests/Constraints/AtLeastOneOfTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(), | ||
]); | ||
} | ||
} |
163 changes: 163 additions & 0 deletions
163
src/Symfony/Component/Validator/Tests/Constraints/AtLeastOneOfValidatorTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(), | ||
]], | ||
]; | ||
} | ||
} |