[Validator] Added support for cascade validation on typed properties
This commit is contained in:
parent
f1dc422394
commit
f4679ef08a
@ -32,6 +32,7 @@ CHANGELOG
|
||||
5.1.0
|
||||
-----
|
||||
|
||||
* added a `Cascade` constraint to ease validating typed nested objects
|
||||
* added the `Hostname` constraint and validator
|
||||
* added the `alpha3` option to the `Country` and `Language` constraints
|
||||
* allow to define a reusable set of constraints by extending the `Compound` constraint
|
||||
|
41
src/Symfony/Component/Validator/Constraints/Cascade.php
Normal file
41
src/Symfony/Component/Validator/Constraints/Cascade.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target({"CLASS"})
|
||||
*
|
||||
* @author Jules Pietri <jules@heahprod.com>
|
||||
*/
|
||||
class Cascade extends Constraint
|
||||
{
|
||||
public function __construct($options = null)
|
||||
{
|
||||
if (\is_array($options) && \array_key_exists('groups', $options)) {
|
||||
throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__));
|
||||
}
|
||||
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTargets()
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
}
|
@ -12,8 +12,10 @@
|
||||
namespace Symfony\Component\Validator\Mapping;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Constraints\Cascade;
|
||||
use Symfony\Component\Validator\Constraints\GroupSequence;
|
||||
use Symfony\Component\Validator\Constraints\Traverse;
|
||||
use Symfony\Component\Validator\Constraints\Valid;
|
||||
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
|
||||
use Symfony\Component\Validator\Exception\GroupDefinitionException;
|
||||
|
||||
@ -170,6 +172,17 @@ class ClassMetadata extends GenericMetadata implements ClassMetadataInterface
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* If the constraint {@link Cascade} is added, the cascading strategy will be
|
||||
* changed to {@link CascadingStrategy::CASCADE}.
|
||||
*
|
||||
* If the constraint {@link Traverse} is added, the traversal strategy will be
|
||||
* changed. Depending on the $traverse property of that constraint,
|
||||
* the traversal strategy will be set to one of the following:
|
||||
*
|
||||
* - {@link TraversalStrategy::IMPLICIT} by default
|
||||
* - {@link TraversalStrategy::NONE} if $traverse is disabled
|
||||
* - {@link TraversalStrategy::TRAVERSE} if $traverse is enabled
|
||||
*/
|
||||
public function addConstraint(Constraint $constraint)
|
||||
{
|
||||
@ -190,6 +203,23 @@ class ClassMetadata extends GenericMetadata implements ClassMetadataInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($constraint instanceof Cascade) {
|
||||
if (\PHP_VERSION_ID < 70400) {
|
||||
throw new ConstraintDefinitionException(sprintf('The constraint "%s" requires PHP 7.4.', Cascade::class));
|
||||
}
|
||||
|
||||
$this->cascadingStrategy = CascadingStrategy::CASCADE;
|
||||
|
||||
foreach ($this->getReflectionClass()->getProperties() as $property) {
|
||||
if ($property->hasType() && (('array' === $type = $property->getType()->getName()) || class_exists(($type)))) {
|
||||
$this->addPropertyConstraint($property->getName(), new Valid());
|
||||
}
|
||||
}
|
||||
|
||||
// The constraint is not added
|
||||
return $this;
|
||||
}
|
||||
|
||||
$constraint->addImplicitGroupName($this->getDefaultGroup());
|
||||
|
||||
parent::addConstraint($constraint);
|
||||
@ -459,13 +489,11 @@ class ClassMetadata extends GenericMetadata implements ClassMetadataInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Class nodes are never cascaded.
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCascadingStrategy()
|
||||
{
|
||||
return CascadingStrategy::NONE;
|
||||
return $this->cascadingStrategy;
|
||||
}
|
||||
|
||||
private function addPropertyMetadata(PropertyMetadataInterface $metadata)
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace Symfony\Component\Validator\Mapping;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Constraints\Cascade;
|
||||
use Symfony\Component\Validator\Constraints\DisableAutoMapping;
|
||||
use Symfony\Component\Validator\Constraints\EnableAutoMapping;
|
||||
use Symfony\Component\Validator\Constraints\Traverse;
|
||||
@ -132,12 +133,12 @@ class GenericMetadata implements MetadataInterface
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws ConstraintDefinitionException When trying to add the
|
||||
* {@link Traverse} constraint
|
||||
* @throws ConstraintDefinitionException When trying to add the {@link Cascade}
|
||||
* or {@link Traverse} constraint
|
||||
*/
|
||||
public function addConstraint(Constraint $constraint)
|
||||
{
|
||||
if ($constraint instanceof Traverse) {
|
||||
if ($constraint instanceof Traverse || $constraint instanceof Cascade) {
|
||||
throw new ConstraintDefinitionException(sprintf('The constraint "%s" can only be put on classes. Please use "Symfony\Component\Validator\Constraints\Valid" instead.', get_debug_type($constraint)));
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,17 @@
|
||||
<?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\Fixtures;
|
||||
|
||||
class CascadedChild
|
||||
{
|
||||
public $name;
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
<?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\Fixtures;
|
||||
|
||||
class CascadingEntity
|
||||
{
|
||||
public string $scalar;
|
||||
|
||||
public CascadedChild $requiredChild;
|
||||
|
||||
public ?CascadedChild $optionalChild;
|
||||
|
||||
public static ?CascadedChild $staticChild;
|
||||
|
||||
/**
|
||||
* @var CascadedChild[]
|
||||
*/
|
||||
public array $children;
|
||||
}
|
@ -13,8 +13,12 @@ namespace Symfony\Component\Validator\Tests\Mapping;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Constraints\Cascade;
|
||||
use Symfony\Component\Validator\Constraints\Valid;
|
||||
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
|
||||
use Symfony\Component\Validator\Mapping\CascadingStrategy;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
use Symfony\Component\Validator\Tests\Fixtures\CascadingEntity;
|
||||
use Symfony\Component\Validator\Tests\Fixtures\ConstraintA;
|
||||
use Symfony\Component\Validator\Tests\Fixtures\ConstraintB;
|
||||
use Symfony\Component\Validator\Tests\Fixtures\PropertyConstraint;
|
||||
@ -310,4 +314,36 @@ class ClassMetadataTest extends TestCase
|
||||
{
|
||||
$this->assertCount(0, $this->metadata->getPropertyMetadata('foo'), '->getPropertyMetadata() returns an empty collection if no metadata is configured for the given property');
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires PHP < 7.4
|
||||
*/
|
||||
public function testCascadeConstraintIsNotAvailable()
|
||||
{
|
||||
$metadata = new ClassMetadata(CascadingEntity::class);
|
||||
|
||||
$this->expectException(ConstraintDefinitionException::class);
|
||||
$this->expectExceptionMessage('The constraint "Symfony\Component\Validator\Constraints\Cascade" requires PHP 7.4.');
|
||||
|
||||
$metadata->addConstraint(new Cascade());
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires PHP 7.4
|
||||
*/
|
||||
public function testCascadeConstraint()
|
||||
{
|
||||
$metadata = new ClassMetadata(CascadingEntity::class);
|
||||
|
||||
$metadata->addConstraint(new Cascade());
|
||||
|
||||
$this->assertSame(CascadingStrategy::CASCADE, $metadata->getCascadingStrategy());
|
||||
$this->assertCount(4, $metadata->properties);
|
||||
$this->assertSame([
|
||||
'requiredChild',
|
||||
'optionalChild',
|
||||
'staticChild',
|
||||
'children',
|
||||
], $metadata->getConstrainedProperties());
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace Symfony\Component\Validator\Tests\Validator;
|
||||
|
||||
use Symfony\Component\Validator\Constraints\Callback;
|
||||
use Symfony\Component\Validator\Constraints\Cascade;
|
||||
use Symfony\Component\Validator\Constraints\Collection;
|
||||
use Symfony\Component\Validator\Constraints\Expression;
|
||||
use Symfony\Component\Validator\Constraints\GroupSequence;
|
||||
@ -23,6 +24,8 @@ use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
|
||||
use Symfony\Component\Validator\Tests\Fixtures\CascadedChild;
|
||||
use Symfony\Component\Validator\Tests\Fixtures\CascadingEntity;
|
||||
use Symfony\Component\Validator\Tests\Fixtures\Entity;
|
||||
use Symfony\Component\Validator\Tests\Fixtures\FailingConstraint;
|
||||
use Symfony\Component\Validator\Tests\Fixtures\Reference;
|
||||
@ -497,6 +500,85 @@ abstract class AbstractTest extends AbstractValidatorTest
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
|
||||
public function testReferenceCascadeDisabledByDefault()
|
||||
{
|
||||
$entity = new Entity();
|
||||
$entity->reference = new Reference();
|
||||
|
||||
$callback = function ($value, ExecutionContextInterface $context) {
|
||||
$this->fail('Should not be called');
|
||||
};
|
||||
|
||||
$this->referenceMetadata->addConstraint(new Callback([
|
||||
'callback' => $callback,
|
||||
'groups' => 'Group',
|
||||
]));
|
||||
|
||||
$violations = $this->validate($entity, new Valid(), 'Group');
|
||||
|
||||
/* @var ConstraintViolationInterface[] $violations */
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires PHP 7.4
|
||||
*/
|
||||
public function testReferenceCascadeEnabledIgnoresUntyped()
|
||||
{
|
||||
$entity = new Entity();
|
||||
$entity->reference = new Reference();
|
||||
|
||||
$this->metadata->addConstraint(new Cascade());
|
||||
|
||||
$callback = function ($value, ExecutionContextInterface $context) {
|
||||
$this->fail('Should not be called');
|
||||
};
|
||||
|
||||
$this->referenceMetadata->addConstraint(new Callback([
|
||||
'callback' => $callback,
|
||||
'groups' => 'Group',
|
||||
]));
|
||||
|
||||
$violations = $this->validate($entity, new Valid(), 'Group');
|
||||
|
||||
/* @var ConstraintViolationInterface[] $violations */
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires PHP 7.4
|
||||
*/
|
||||
public function testTypedReferenceCascadeEnabled()
|
||||
{
|
||||
$entity = new CascadingEntity();
|
||||
$entity->requiredChild = new CascadedChild();
|
||||
|
||||
$callback = function ($value, ExecutionContextInterface $context) {
|
||||
$context->buildViolation('Invalid child')
|
||||
->atPath('name')
|
||||
->addViolation()
|
||||
;
|
||||
};
|
||||
|
||||
$cascadingMetadata = new ClassMetadata(CascadingEntity::class);
|
||||
$cascadingMetadata->addConstraint(new Cascade());
|
||||
|
||||
$cascadedMetadata = new ClassMetadata(CascadedChild::class);
|
||||
$cascadedMetadata->addConstraint(new Callback([
|
||||
'callback' => $callback,
|
||||
'groups' => 'Group',
|
||||
]));
|
||||
|
||||
$this->metadataFactory->addMetadata($cascadingMetadata);
|
||||
$this->metadataFactory->addMetadata($cascadedMetadata);
|
||||
|
||||
$violations = $this->validate($entity, new Valid(), 'Group');
|
||||
|
||||
/* @var ConstraintViolationInterface[] $violations */
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertInstanceOf(Callback::class, $violations->get(0)->getConstraint());
|
||||
}
|
||||
|
||||
public function testAddCustomizedViolation()
|
||||
{
|
||||
$entity = new Entity();
|
||||
|
@ -13,6 +13,7 @@ namespace Symfony\Component\Validator\Tests\Validator;
|
||||
|
||||
use Symfony\Component\Translation\IdentityTranslator;
|
||||
use Symfony\Component\Validator\Constraints\All;
|
||||
use Symfony\Component\Validator\Constraints\Cascade;
|
||||
use Symfony\Component\Validator\Constraints\Collection;
|
||||
use Symfony\Component\Validator\Constraints\GroupSequence;
|
||||
use Symfony\Component\Validator\Constraints\IsTrue;
|
||||
@ -21,6 +22,7 @@ use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Constraints\NotNull;
|
||||
use Symfony\Component\Validator\Constraints\Optional;
|
||||
use Symfony\Component\Validator\Constraints\Required;
|
||||
use Symfony\Component\Validator\Constraints\Type;
|
||||
use Symfony\Component\Validator\Constraints\Valid;
|
||||
use Symfony\Component\Validator\ConstraintValidatorFactory;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextFactory;
|
||||
@ -28,6 +30,8 @@ use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
|
||||
use Symfony\Component\Validator\Tests\Constraints\Fixtures\ChildA;
|
||||
use Symfony\Component\Validator\Tests\Constraints\Fixtures\ChildB;
|
||||
use Symfony\Component\Validator\Tests\Fixtures\CascadedChild;
|
||||
use Symfony\Component\Validator\Tests\Fixtures\CascadingEntity;
|
||||
use Symfony\Component\Validator\Tests\Fixtures\Entity;
|
||||
use Symfony\Component\Validator\Tests\Fixtures\EntityParent;
|
||||
use Symfony\Component\Validator\Tests\Fixtures\EntityWithGroupedConstraintOnMethods;
|
||||
@ -202,4 +206,116 @@ class RecursiveValidatorTest extends AbstractTest
|
||||
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires PHP 7.4
|
||||
*/
|
||||
public function testValidateDoNotCascadeNestedObjectsAndArraysByDefault()
|
||||
{
|
||||
$this->metadataFactory->addMetadata(new ClassMetadata(CascadingEntity::class));
|
||||
$this->metadataFactory->addMetadata((new ClassMetadata(CascadedChild::class))
|
||||
->addPropertyConstraint('name', new NotNull())
|
||||
);
|
||||
|
||||
$entity = new CascadingEntity();
|
||||
$entity->requiredChild = new CascadedChild();
|
||||
$entity->optionalChild = new CascadedChild();
|
||||
$entity->children[] = new CascadedChild();
|
||||
CascadingEntity::$staticChild = new CascadedChild();
|
||||
|
||||
$violations = $this->validator->validate($entity);
|
||||
|
||||
$this->assertCount(0, $violations);
|
||||
|
||||
CascadingEntity::$staticChild = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires PHP 7.4
|
||||
*/
|
||||
public function testValidateTraverseNestedArrayByDefaultIfConstrainedWithoutCascading()
|
||||
{
|
||||
$this->metadataFactory->addMetadata((new ClassMetadata(CascadingEntity::class))
|
||||
->addPropertyConstraint('children', new All([
|
||||
new Type(CascadedChild::class),
|
||||
]))
|
||||
);
|
||||
$this->metadataFactory->addMetadata((new ClassMetadata(CascadedChild::class))
|
||||
->addPropertyConstraint('name', new NotNull())
|
||||
);
|
||||
|
||||
$entity = new CascadingEntity();
|
||||
$entity->children[] = new \stdClass();
|
||||
$entity->children[] = new CascadedChild();
|
||||
|
||||
$violations = $this->validator->validate($entity);
|
||||
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertInstanceOf(Type::class, $violations->get(0)->getConstraint());
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires PHP 7.4
|
||||
*/
|
||||
public function testValidateCascadeWithValid()
|
||||
{
|
||||
$this->metadataFactory->addMetadata((new ClassMetadata(CascadingEntity::class))
|
||||
->addPropertyConstraint('requiredChild', new Valid())
|
||||
->addPropertyConstraint('optionalChild', new Valid())
|
||||
->addPropertyConstraint('staticChild', new Valid())
|
||||
->addPropertyConstraint('children', new Valid())
|
||||
);
|
||||
$this->metadataFactory->addMetadata((new ClassMetadata(CascadedChild::class))
|
||||
->addPropertyConstraint('name', new NotNull())
|
||||
);
|
||||
|
||||
$entity = new CascadingEntity();
|
||||
$entity->requiredChild = new CascadedChild();
|
||||
$entity->children[] = new CascadedChild();
|
||||
$entity->children[] = null;
|
||||
CascadingEntity::$staticChild = new CascadedChild();
|
||||
|
||||
$violations = $this->validator->validate($entity);
|
||||
|
||||
$this->assertCount(3, $violations);
|
||||
$this->assertInstanceOf(NotNull::class, $violations->get(0)->getConstraint());
|
||||
$this->assertInstanceOf(NotNull::class, $violations->get(1)->getConstraint());
|
||||
$this->assertInstanceOf(NotNull::class, $violations->get(2)->getConstraint());
|
||||
$this->assertSame('requiredChild.name', $violations->get(0)->getPropertyPath());
|
||||
$this->assertSame('staticChild.name', $violations->get(1)->getPropertyPath());
|
||||
$this->assertSame('children[0].name', $violations->get(2)->getPropertyPath());
|
||||
|
||||
CascadingEntity::$staticChild = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires PHP 7.4
|
||||
*/
|
||||
public function testValidateWithExplicitCascade()
|
||||
{
|
||||
$this->metadataFactory->addMetadata((new ClassMetadata(CascadingEntity::class))
|
||||
->addConstraint(new Cascade())
|
||||
);
|
||||
$this->metadataFactory->addMetadata((new ClassMetadata(CascadedChild::class))
|
||||
->addPropertyConstraint('name', new NotNull())
|
||||
);
|
||||
|
||||
$entity = new CascadingEntity();
|
||||
$entity->requiredChild = new CascadedChild();
|
||||
$entity->children[] = new CascadedChild();
|
||||
$entity->children[] = null;
|
||||
CascadingEntity::$staticChild = new CascadedChild();
|
||||
|
||||
$violations = $this->validator->validate($entity);
|
||||
|
||||
$this->assertCount(3, $violations);
|
||||
$this->assertInstanceOf(NotNull::class, $violations->get(0)->getConstraint());
|
||||
$this->assertInstanceOf(NotNull::class, $violations->get(1)->getConstraint());
|
||||
$this->assertInstanceOf(NotNull::class, $violations->get(2)->getConstraint());
|
||||
$this->assertSame('requiredChild.name', $violations->get(0)->getPropertyPath());
|
||||
$this->assertSame('staticChild.name', $violations->get(1)->getPropertyPath());
|
||||
$this->assertSame('children[0].name', $violations->get(2)->getPropertyPath());
|
||||
|
||||
CascadingEntity::$staticChild = null;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user