[Validator] Allow to define a reusable set of constraints

This commit is contained in:
Maxime Steinhausser 2019-11-08 15:27:42 +01:00 committed by Fabien Potencier
parent 83a53a5edf
commit 8f1b0dfdb7
7 changed files with 225 additions and 2 deletions

View File

@ -6,6 +6,7 @@ CHANGELOG
* added the `Hostname` constraint and validator
* added option `alpha3` to `Country` constraint
* allow to define a reusable set of constraints by extending the `Compound` constraint
5.0.0
-----

View File

@ -105,6 +105,14 @@ abstract class Constraint
*/
public function __construct($options = null)
{
foreach ($this->normalizeOptions($options) as $name => $value) {
$this->$name = $value;
}
}
protected function normalizeOptions($options): array
{
$normalizedOptions = [];
$defaultOption = $this->getDefaultOption();
$invalidOptions = [];
$missingOptions = array_flip((array) $this->getRequiredOptions());
@ -128,7 +136,7 @@ abstract class Constraint
if ($options && \is_array($options) && \is_string(key($options))) {
foreach ($options as $option => $value) {
if (\array_key_exists($option, $knownOptions)) {
$this->$option = $value;
$normalizedOptions[$option] = $value;
unset($missingOptions[$option]);
} else {
$invalidOptions[] = $option;
@ -140,7 +148,7 @@ abstract class Constraint
}
if (\array_key_exists($defaultOption, $knownOptions)) {
$this->$defaultOption = $options;
$normalizedOptions[$defaultOption] = $options;
unset($missingOptions[$defaultOption]);
} else {
$invalidOptions[] = $defaultOption;
@ -154,6 +162,8 @@ abstract class Constraint
if (\count($missingOptions) > 0) {
throw new MissingOptionsException(sprintf('The options "%s" must be set for constraint "%s".', implode('", "', array_keys($missingOptions)), static::class), array_keys($missingOptions));
}
return $normalizedOptions;
}
/**

View File

@ -0,0 +1,52 @@
<?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\Exception\ConstraintDefinitionException;
/**
* Extend this class to create a reusable set of constraints.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
abstract class Compound extends Composite
{
/** @var Constraint[] */
public $constraints = [];
public function __construct($options = null)
{
if (isset($options[$this->getCompositeOption()])) {
throw new ConstraintDefinitionException(sprintf('You can\'t redefine the "%s" option. Use the %s::getConstraints() method instead.', $this->getCompositeOption(), __CLASS__));
}
$this->constraints = $this->getConstraints($this->normalizeOptions($options));
parent::__construct($options);
}
final protected function getCompositeOption()
{
return 'constraints';
}
final public function validatedBy()
{
return CompoundValidator::class;
}
/**
* @return Constraint[]
*/
abstract protected function getConstraints(array $options): array;
}

View File

@ -0,0 +1,35 @@
<?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 Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class CompoundValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof Compound) {
throw new UnexpectedTypeException($constraint, Compound::class);
}
$context = $this->context;
$validator = $context->getValidator()->inContext($context);
$validator->validate($value, $constraint->constraints);
}
}

View File

@ -181,6 +181,15 @@ abstract class ConstraintValidatorTestCase extends TestCase
->willReturn($validator);
}
protected function expectValidateValue(int $i, $value, array $constraints = [], $group = null)
{
$contextualValidator = $this->context->getValidator()->inContext($this->context);
$contextualValidator->expects($this->at($i))
->method('validate')
->with($value, $constraints, $group)
->willReturn($contextualValidator);
}
protected function expectValidateValueAt($i, $propertyPath, $value, $constraints, $group = null)
{
$contextualValidator = $this->context->getValidator()->inContext($this->context);

View File

@ -0,0 +1,60 @@
<?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\Compound;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
class CompoundTest extends TestCase
{
public function testItCannotRedefineConstraintsOption()
{
$this->expectException(ConstraintDefinitionException::class);
$this->expectExceptionMessage('You can\'t redefine the "constraints" option. Use the Symfony\Component\Validator\Constraints\Compound::getConstraints() method instead.');
new EmptyCompound(['constraints' => [new NotBlank()]]);
}
public function testCanDependOnNormalizedOptions()
{
$constraint = new ForwardingOptionCompound($min = 3);
$this->assertSame($min, $constraint->constraints[0]->min);
}
}
class EmptyCompound extends Compound
{
protected function getConstraints(array $options): array
{
return [];
}
}
class ForwardingOptionCompound extends Compound
{
public $min;
public function getDefaultOption()
{
return 'min';
}
protected function getConstraints(array $options): array
{
return [
new Length(['min' => $options['min'] ?? null]),
];
}
}

View File

@ -0,0 +1,56 @@
<?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\Compound;
use Symfony\Component\Validator\Constraints\CompoundValidator;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
class CompoundValidatorTest extends ConstraintValidatorTestCase
{
protected function createValidator()
{
return new CompoundValidator();
}
public function testValidValue()
{
$this->validator->validate('foo', new DummyCompoundConstraint());
$this->assertNoViolation();
}
public function testValidateWithConstraints()
{
$value = 'foo';
$constraint = new DummyCompoundConstraint();
$this->expectValidateValue(0, $value, $constraint->constraints);
$this->validator->validate($value, $constraint);
$this->assertNoViolation();
}
}
class DummyCompoundConstraint extends Compound
{
protected function getConstraints(array $options): array
{
return [
new NotBlank(),
new Length(['max' => 3]),
];
}
}