Skip to content

Commit 309ea5a

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

File tree

10 files changed

+287
-6
lines changed

10 files changed

+287
-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: 27 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,19 @@ 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+
215+
// The constraint is not added
216+
return $this;
217+
}
218+
193219
$constraint->addImplicitGroupName($this->getDefaultGroup());
194220

195221
parent::addConstraint($constraint);
@@ -459,13 +485,11 @@ public function isGroupSequenceProvider()
459485
}
460486

461487
/**
462-
* Class nodes are never cascaded.
463-
*
464488
* {@inheritdoc}
465489
*/
466490
public function getCascadingStrategy()
467491
{
468-
return CascadingStrategy::NONE;
492+
return $this->cascadingStrategy;
469493
}
470494

471495
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 ?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+
$violations = $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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy