diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index a7209d5377d85..957000274b2f3 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -210,7 +210,7 @@ public function addConstraint(Constraint $constraint) $this->cascadingStrategy = CascadingStrategy::CASCADE; foreach ($this->getReflectionClass()->getProperties() as $property) { - if ($property->hasType() && (('array' === $type = $property->getType()->getName()) || class_exists($type))) { + if ($this->canCascade($property->getType())) { $this->addPropertyConstraint($property->getName(), new Valid()); } } @@ -511,4 +511,33 @@ private function checkConstraint(Constraint $constraint) } } } + + private function canCascade(?\ReflectionType $type = null): bool + { + if (null === $type) { + return false; + } + + if ($type instanceof \ReflectionIntersectionType) { + foreach ($type->getTypes() as $nestedType) { + if ($this->canCascade($nestedType)) { + return true; + } + } + + return false; + } + + if ($type instanceof \ReflectionUnionType) { + foreach ($type->getTypes() as $nestedType) { + if (!$this->canCascade($nestedType)) { + return false; + } + } + + return true; + } + + return $type instanceof \ReflectionNamedType && (\in_array($type->getName(), ['array', 'null'], true) || class_exists($type->getName())); + } } diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/CascadingEntityIntersection.php b/src/Symfony/Component/Validator/Tests/Fixtures/CascadingEntityIntersection.php new file mode 100644 index 0000000000000..9478f647c4b5d --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/CascadingEntityIntersection.php @@ -0,0 +1,17 @@ + + * + * 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 CascadingEntityIntersection +{ + public CascadedChild&\stdClass $classes; +} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/CascadingEntityUnion.php b/src/Symfony/Component/Validator/Tests/Fixtures/CascadingEntityUnion.php new file mode 100644 index 0000000000000..03c808fca330f --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/CascadingEntityUnion.php @@ -0,0 +1,25 @@ + + * + * 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 CascadingEntityUnion +{ + public CascadedChild|\stdClass $classes; + public CascadedChild|array $classAndArray; + public CascadedChild|null $classAndNull; + public array|null $arrayAndNull; + public CascadedChild|array|null $classAndArrayAndNull; + public int|string $scalars; + public int|null $scalarAndNull; + public CascadedChild|int $classAndScalar; + public array|int $arrayAndScalar; +} diff --git a/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php b/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php index a9f942319af83..4e0bca845a2cb 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php @@ -25,6 +25,8 @@ use Symfony\Component\Validator\Tests\Fixtures\Annotation\EntityParent; use Symfony\Component\Validator\Tests\Fixtures\Annotation\GroupSequenceProviderEntity; use Symfony\Component\Validator\Tests\Fixtures\CascadingEntity; +use Symfony\Component\Validator\Tests\Fixtures\CascadingEntityIntersection; +use Symfony\Component\Validator\Tests\Fixtures\CascadingEntityUnion; use Symfony\Component\Validator\Tests\Fixtures\ClassConstraint; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; @@ -361,6 +363,40 @@ public function testCascadeConstraint() 'children', ], $metadata->getConstrainedProperties()); } + + /** + * @requires PHP 8.0 + */ + public function testCascadeConstraintWithUnionTypeProperties() + { + $metadata = new ClassMetadata(CascadingEntityUnion::class); + $metadata->addConstraint(new Cascade()); + + $this->assertSame(CascadingStrategy::CASCADE, $metadata->getCascadingStrategy()); + $this->assertCount(5, $metadata->properties); + $this->assertSame([ + 'classes', + 'classAndArray', + 'classAndNull', + 'arrayAndNull', + 'classAndArrayAndNull', + ], $metadata->getConstrainedProperties()); + } + + /** + * @requires PHP 8.1 + */ + public function testCascadeConstraintWithIntersectionTypeProperties() + { + $metadata = new ClassMetadata(CascadingEntityIntersection::class); + $metadata->addConstraint(new Cascade()); + + $this->assertSame(CascadingStrategy::CASCADE, $metadata->getCascadingStrategy()); + $this->assertCount(1, $metadata->properties); + $this->assertSame([ + 'classes', + ], $metadata->getConstrainedProperties()); + } } class ClassCompositeConstraint extends Composite