8000 [Validator] Added support for cascade validation on typed properties · symfony/symfony@757d8f7 · GitHub
[go: up one dir, main page]

Skip to content

Commit 757d8f7

Browse files
committed
[Validator] Added support for cascade validation on typed properties
1 parent 402909f commit 757d8f7

File tree

10 files changed

+292
-6
lines changed

10 files changed

+292
-6
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
5.1.0
55
-----
66

7+
* added a `Cascade` constraint to ease validating typed nested objects
78
* added the `Hostname` constraint and validator
89
* added the `alpha3` option to the `Country` and `Language` constraints
910
* allow to define a reusable set of constraints by extending the `Compound` constraint
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
16+
17+
/**
18+
* @Annotation
19+
* @Target({"CLASS"})
20+
*
21+
* @author Jules Pietri <jules@heahprod.com>
22+
*/
23+
class Cascade extends Constraint
24+
{
25+
public function __construct($options = null)
26+
{
27+
if (\is_array($options) && \array_key_exists('groups', $options)) {
28+
throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__));
29+
}
30+
31+
parent::__construct($options);
32+
}
33+
34+
/**
35+
* {@inheritdoc}
36+
*/
37+
public function getTargets()
38+
{
39+
return self::CLASS_CONSTRAINT;
40+
}
41+
}

src/Symfony/Component/Validator/Mapping/ClassMetadata.php

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
namespace Symfony\Component\Validator\Mapping;
1313

1414
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\Constraints\Cascade;
1516
use Symfony\Component\Validator\Constraints\GroupSequence;
1617
use Symfony\Component\Validator\Constraints\Traverse;
18+
use Symfony\Component\Validator\Constraints\Valid;
1719
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
1820
use Symfony\Component\Validator\Exception\GroupDefinitionException;
1921

@@ -170,6 +172,17 @@ public function getDefaultGroup()
170172

171173
/**
172174
* {@inheritdoc}
175+
*
176+
* If the constraint {@link Cascade} is added, the cascading strategy will be
177+
* changed to {@link CascadingStrategy::CASCADE}.
178+
*
179+
* If the constraint {@link Traverse} is added, the traversal strategy will be
180+
* changed. Depending on the $traverse property of that constraint,
181+
* the traversal strategy will be set to one of the following:
182+
*
183+
* - {@link TraversalStrategy::IMPLICIT} by default
184+
* - {@link TraversalStrategy::NONE} if $traverse is disabled
185+
* - {@link TraversalStrategy::TRAVERSE} if $traverse is enabled
173186
*/
174187
public function addConstraint(Constraint $constraint)
175188
{
@@ -190,6 +203,24 @@ public function addConstraint(Constraint $constraint)
190203
return $this;
191204
}
192205

206+
if ($constraint instanceof Cascade) {
207+
$this->cascadingStrategy = CascadingStrategy::CASCADE;
208+
209+
foreach ($this->getReflectionClass()->getProperties() as $property) {
210+
if ($property->hasType() && class_exists($property->getType()->getName())) {
211+
$this->addPropertyConstraint($property->getName(), new Valid());
212+
}
213+
}
214+
foreach ($this->getReflectionClass()->getStaticProperties() as $property) {
215+
if ($property->hasType() && class_exists($property->getType()->getName())) {
216+
$this->addPropertyConstraint($property->getName(), new Valid());
217+
}
218+
}
219+
220+
// The constraint is not added
221+
return $this;
222+
}
223+
193224
$constraint->addImplicitGroupName($this->getDefaultGroup());
194225

195226
parent::addConstraint($constraint);
@@ -459,13 +490,11 @@ public function isGroupSequenceProvider()
459490
}
460491

461492
/**
462-
* Class nodes are never cascaded.
463-
*
464493
* {@inheritdoc}
465494
*/
466495
public function getCascadingStrategy()
467496
{
468-
return CascadingStrategy::NONE;
497+
return $this->cascadingStrategy;
469498
}
470499

471500
private function addPropertyMetadata(PropertyMetadataInterface $metadata)

src/Symfony/Component/Validator/Mapping/GenericMetadata.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Validator\Mapping;
1313

1414
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\Constraints\Cascade;
1516
use Symfony\Component\Validator\Constraints\DisableAutoMapping;
1617
use Symfony\Component\Validator\Constraints\EnableAutoMapping;
1718
use Symfony\Component\Validator\Constraints\Traverse;
@@ -132,12 +133,12 @@ public function __clone()
132133
*
133134
* @return $this
134135
*
135-
* @throws ConstraintDefinitionException When trying to add the
136-
* {@link Traverse} constraint
136+
* @throws ConstraintDefinitionException When trying to add the {@link Cascade}
137+
* or {@link Traverse} constraint
137138
*/
138139
public function addConstraint(Constraint $constraint)
139140
{
140-
if ($constraint instanceof Traverse) {
141+
if ($constraint instanceof Traverse || $constraint instanceof Cascade) {
141142
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)));
142143
}
143144

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Fixtures;
13+
14+
class CascadedChild
15+
{
16+
public $name;
17+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Fixtures;
13+
14+
class CascadingEntity
15+
{
16+
public ?CascadedChild $childOne;
17+
18+
public static ?CascadedChild $childTwo;
19+
20+
public $children = [];
21+
}

src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Validator\Constraints\All;
1616
use Symfony\Component\Validator\Constraints\Callback;
17+
use Symfony\Component\Validator\Constraints\Cascade;
1718
use Symfony\Component\Validator\Constraints\Choice;
1819
use Symfony\Component\Validator\Constraints\Collection;
1920
use Symfony\Component\Validator\Constraints\IsTrue;
@@ -59,6 +60,7 @@ public function testLoadClassMetadata()
5960
$expected->addConstraint(new Callback('validateMe'));
6061
$expected->addConstraint(new Callback('validateMeStatic'));
6162
$expected->addConstraint(new Callback(['Symfony\Component\Validator\Tests\Fixtures\CallbackClass', 'callback']));
63+
$expected->addConstraint(new Cascade());
6264
$expected->addConstraint(new Traverse(false));
6365
$expected->addPropertyConstraint('firstName', new NotNull());
6466
$expected->addPropertyConstraint('firstName', new Range(['min' => 3]));

src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
<value>callback</value>
3232
</constraint>
3333

34+
<!-- Cascade -->
35+
<constraint name="Cascade" />
36+
3437
<!-- Traverse with boolean default option -->
3538
<constraint name="Traverse">
3639
false

src/Symfony/Component/Validator/Tests/Validator/AbstractTest.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Validator\Tests\Validator;
1313

1414
use Symfony\Component\Validator\Constraints\Callback;
15+
use Symfony\Component\Validator\Constraints\Cascade;
1516
use Symfony\Component\Validator\Constraints\Collection;
1617
use Symfony\Component\Validator\Constraints\Expression;
1718
use Symfony\Component\Validator\Constraints\GroupSequence;
@@ -23,6 +24,8 @@
2324
use Symfony\Component\Validator\Context\ExecutionContextInterface;
2425
use Symfony\Component\Validator\Mapping\ClassMetadata;
2526
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
27+
use Symfony\Component\Validator\Tests\Fixtures\CascadedChild;
28+
use Symfony\Component\Validator\Tests\Fixtures\CascadingEntity;
2629
use Symfony\Component\Validator\Tests\Fixtures\Entity;
2730
use Symfony\Component\Validator\Tests\Fixtures\FailingConstraint;
2831
use Symfony\Component\Validator\Tests\Fixtures\Reference;
@@ -497,6 +500,60 @@ public function testReferenceTraversalDisabledOnReferenceEnabledOnClass()
497500
$this->assertCount(0, $violations);
498501
}
499502

503+
public function testReferenceCascadeDisabledByDefault()
504+
{
505+
$entity = new Entity();
506+
$entity->reference = new Reference();
507+
508+
$callback = function ($value, ExecutionContextInterface $context) {
509+
$this->fail('Should not be called');
510+
};
511+
512+
$this->referenceMetadata->addConstraint(new Callback([
513+
'callback' => $callback,
514+
'groups' => 'Group',
515+
]));
516+
517+
$violations = $this->validate($entity, new Valid(), 'Group');
518+
519+
/* @var ConstraintViolationInterface[] $violations */
520+
$this->assertCount(0, $violations);
521+
}
522+
523+
/**
524+
* @requires PHP 7.4
525+
*/
526+
public function testReferenceCascadeEnabled()
527+
{
528+
$entity = new CascadingEntity();
529+
$entity->childOne = new CascadedChild();
530+
531+
$callback = function ($value, ExecutionContextInterface $context) {
532+
$context->buildViolation('Invalid reference')
533+
->atPath('reference')
534+
->addViolation()
535+
;
536+
};
537+
538+
$cascadingMetadata = new ClassMetadata(CascadingEntity::class);
539+
$cascadingMetadata->addConstraint(new Cascade());
540+
541+
$cascadedMetadata = new ClassMetadata(CascadedChild::class);
542+
$cascadedMetadata->addConstraint(new Callback([
543+
'callback' => $callback,
544+
'groups' => 'Group',
545+
]));
546+
547+
$this->metadataFactory->addMetadata($cascadingMetadata);
548+
$this->metadataFactory->addMetadata($cascadedMetadata);
549+
550+
$violation 6B2E s = $this->validate($entity, new Valid(), 'Group');
551+
552+
/* @var ConstraintViolationInterface[] $violations */
553+
$this->assertCount(1, $violations);
554+
$this->assertInstanceOf(Callback::class, $violations->get(0)->getConstraint());
555+
}
556+
500557
public function testAddCustomizedViolation()
501558
{
502559
$entity = new Entity();

0 commit comments

Comments
 (0)
0