diff --git a/src/Symfony/Component/Validator/Constraints/Composite.php b/src/Symfony/Component/Validator/Constraints/Composite.php index b14276c3bb..d8c6079ee2 100644 --- a/src/Symfony/Component/Validator/Constraints/Composite.php +++ b/src/Symfony/Component/Validator/Constraints/Composite.php @@ -136,6 +136,17 @@ abstract class Composite extends Constraint */ abstract protected function getCompositeOption(); + /** + * @internal Used by metadata + * + * @return Constraint[] + */ + public function getNestedContraints() + { + /* @var Constraint[] $nestedConstraints */ + return $this->{$this->getCompositeOption()}; + } + /** * Initializes the nested constraints. * diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 572bbc6d55..cf8782c085 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Composite; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; @@ -178,9 +179,7 @@ class ClassMetadata extends GenericMetadata implements ClassMetadataInterface */ public function addConstraint(Constraint $constraint) { - if (!\in_array(Constraint::CLASS_CONSTRAINT, (array) $constraint->getTargets())) { - throw new ConstraintDefinitionException(sprintf('The constraint "%s" cannot be put on classes.', \get_class($constraint))); - } + $this->checkConstraint($constraint); if ($constraint instanceof Traverse) { if ($constraint->traverse) { @@ -495,4 +494,17 @@ class ClassMetadata extends GenericMetadata implements ClassMetadataInterface $this->members[$property][] = $metadata; } + + private function checkConstraint(Constraint $constraint) + { + if (!\in_array(Constraint::CLASS_CONSTRAINT, (array) $constraint->getTargets(), true)) { + throw new ConstraintDefinitionException(sprintf('The constraint "%s" cannot be put on classes.', \get_class($constraint))); + } + + if ($constraint instanceof Composite) { + foreach ($constraint->getNestedContraints() as $nestedContraint) { + $this->checkConstraint($nestedContraint); + } + } + } } diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index fb2fcb439f..124e75a0ee 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Composite; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** @@ -71,9 +72,7 @@ abstract class MemberMetadata extends GenericMetadata implements PropertyMetadat */ public function addConstraint(Constraint $constraint) { - if (!\in_array(Constraint::PROPERTY_CONSTRAINT, (array) $constraint->getTargets())) { - throw new ConstraintDefinitionException(sprintf('The constraint "%s" cannot be put on properties or getters.', \get_class($constraint))); - } + $this->checkConstraint($constraint); parent::addConstraint($constraint); @@ -181,4 +180,17 @@ abstract class MemberMetadata extends GenericMetadata implements PropertyMetadat * @return \ReflectionMethod|\ReflectionProperty The reflection instance */ abstract protected function newReflectionMember($objectOrClassName); + + private function checkConstraint(Constraint $constraint) + { + if (!\in_array(Constraint::PROPERTY_CONSTRAINT, (array) $constraint->getTargets(), true)) { + throw new ConstraintDefinitionException(sprintf('The constraint "%s" cannot be put on properties or getters.', \get_class($constraint))); + } + + if ($constraint instanceof Composite) { + foreach ($constraint->getNestedContraints() as $nestedContraint) { + $this->checkConstraint($nestedContraint); + } + } + } } diff --git a/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php b/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php index 73af5c1894..02fe484525 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php @@ -13,8 +13,11 @@ namespace Symfony\Component\Validator\Tests\Mapping; use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Composite; use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Tests\Fixtures\ClassConstraint; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; use Symfony\Component\Validator\Tests\Fixtures\PropertyConstraint; @@ -52,6 +55,20 @@ class ClassMetadataTest extends TestCase $this->metadata->addConstraint(new PropertyConstraint()); } + public function testAddCompositeConstraintRejectsNestedPropertyConstraints() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The constraint "Symfony\Component\Validator\Tests\Fixtures\PropertyConstraint" cannot be put on classes.'); + + $this->metadata->addConstraint(new ClassCompositeConstraint([new PropertyConstraint()])); + } + + public function testAddCompositeConstraintAcceptsNestedClassConstraints() + { + $this->metadata->addConstraint($constraint = new ClassCompositeConstraint([new ClassConstraint()])); + $this->assertSame($this->metadata->getConstraints(), [$constraint]); + } + public function testAddPropertyConstraints() { $this->metadata->addPropertyConstraint('firstName', new ConstraintA()); @@ -311,3 +328,23 @@ 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'); } } + +class ClassCompositeConstraint extends Composite +{ + public $nested; + + public function getDefaultOption() + { + return $this->getCompositeOption(); + } + + protected function getCompositeOption() + { + return 'nested'; + } + + public function getTargets() + { + return [self::CLASS_CONSTRAINT]; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php b/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php index 651ba95642..c387e39797 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php @@ -12,11 +12,16 @@ namespace Symfony\Component\Validator\Tests\Mapping; use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Constraints\Collection; +use Symfony\Component\Validator\Constraints\Composite; +use Symfony\Component\Validator\Constraints\Required; use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Mapping\MemberMetadata; use Symfony\Component\Validator\Tests\Fixtures\ClassConstraint; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; +use Symfony\Component\Validator\Tests\Fixtures\PropertyConstraint; class MemberMetadataTest extends TestCase { @@ -43,6 +48,34 @@ class MemberMetadataTest extends TestCase $this->metadata->addConstraint(new ClassConstraint()); } + public function testAddCompositeConstraintRejectsNestedClassConstraints() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The constraint "Symfony\Component\Validator\Tests\Fixtures\ClassConstraint" cannot be put on properties or getters.'); + + $this->metadata->addConstraint(new PropertyCompositeConstraint([new ClassConstraint()])); + } + + public function testAddCompositeConstraintRejectsDeepNestedClassConstraints() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The constraint "Symfony\Component\Validator\Tests\Fixtures\ClassConstraint" cannot be put on properties or getters.'); + + $this->metadata->addConstraint(new Collection(['field1' => new Required([new ClassConstraint()])])); + } + + public function testAddCompositeConstraintAcceptsNestedPropertyConstraints() + { + $this->metadata->addConstraint($constraint = new PropertyCompositeConstraint([new PropertyConstraint()])); + $this->assertSame($this->metadata->getConstraints(), [$constraint]); + } + + public function testAddCompositeConstraintAcceptsDeepNestedPropertyConstraints() + { + $this->metadata->addConstraint($constraint = new Collection(['field1' => new Required([new PropertyConstraint()])])); + $this->assertSame($this->metadata->getConstraints(), [$constraint]); + } + public function testSerialize() { $this->metadata->addConstraint(new ConstraintA(['property1' => 'A'])); @@ -82,3 +115,18 @@ class TestMemberMetadata extends MemberMetadata { } } + +class PropertyCompositeConstraint extends Composite +{ + public $nested; + + public function getDefaultOption() + { + return $this->getCompositeOption(); + } + + protected function getCompositeOption() + { + return 'nested'; + } +}