From ef2bad61bfadd3470ef69d617ceefdfbb7bf9021 Mon Sep 17 00:00:00 2001 From: Mathieu Lemoine Date: Fri, 11 Dec 2015 19:23:09 -0500 Subject: [PATCH 1/9] [Validator] Target aware constraint's interface and trait --- .../TargetAwareConstraintInterface.php | 31 ++++++++++++++++ .../TargetAwareConstraintTrait.php | 37 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/Symfony/Component/Validator/Constraints/TargetAwareConstraintInterface.php create mode 100644 src/Symfony/Component/Validator/Constraints/TargetAwareConstraintTrait.php diff --git a/src/Symfony/Component/Validator/Constraints/TargetAwareConstraintInterface.php b/src/Symfony/Component/Validator/Constraints/TargetAwareConstraintInterface.php new file mode 100644 index 0000000000000..ed07c1f275119 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/TargetAwareConstraintInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +/** + * This interface is to be implemented by constraints that need to be + * aware of the exact target they have been declared on. + * + * This interface only makes sense for class constraints. Which could be + * attached to multiple classes because of class inheritance. + * + * @since 3.1 + * + * @author Mathieu Lemoine + */ +interface TargetAwareConstraintInterface +{ + /* + * Since constraints are implemented using public properties, + * the interface is intended as a tag and declare no method. + */ +} diff --git a/src/Symfony/Component/Validator/Constraints/TargetAwareConstraintTrait.php b/src/Symfony/Component/Validator/Constraints/TargetAwareConstraintTrait.php new file mode 100644 index 0000000000000..0de2444ce932e --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/TargetAwareConstraintTrait.php @@ -0,0 +1,37 @@ + + * + * 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; + +/** + * This trait is intended as a helper for implementing TargetAwareConstraintInterface. + * + * Since the interface interface only makes sense for class constraints, + * the default targets is set to class constraint. + * + * @since 3.1 + * + * @author Mathieu Lemoine + */ +trait TargetAwareConstraintTrait +{ + public $target; + + /** + * {@inheritdoc} + */ + public function getTargets() + { + return Constraint::CLASS_CONSTRAINT; + } +} From 9427089279be6226087476dc0b157ef0c2b484a3 Mon Sep 17 00:00:00 2001 From: Mathieu Lemoine Date: Fri, 11 Dec 2015 19:23:32 -0500 Subject: [PATCH 2/9] [Validator] Target aware constraint test fixture --- .../Validator/Tests/Fixtures/ConstraintD.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Symfony/Component/Validator/Tests/Fixtures/ConstraintD.php diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintD.php b/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintD.php new file mode 100644 index 0000000000000..73f0d3d0f0e0d --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintD.php @@ -0,0 +1,22 @@ + + * + * 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; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\TargetAwareConstraintInterface; +use Symfony\Component\Validator\Constraints\TargetAwareConstraintTrait; + +/** @Annotation */ +class ConstraintD extends Constraint implements TargetAwareConstraintInterface +{ + use TargetAwareConstraintTrait; +} From 42dc2d2d1db389250dcd8cca297a5debff3ad4b0 Mon Sep 17 00:00:00 2001 From: Mathieu Lemoine Date: Fri, 11 Dec 2015 19:23:35 -0500 Subject: [PATCH 3/9] [Validator] Target aware constraint support for YamlFileLoader --- .../Mapping/Loader/YamlFileLoader.php | 5 ++ .../Mapping/Loader/YamlFileLoaderTest.php | 69 +++++++++++++++++++ .../Mapping/Loader/constraint-mapping.yml | 8 +++ 3 files changed, 82 insertions(+) diff --git a/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php index cf6dd92dcad9b..cbba7b1c501d3 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Mapping\Loader; use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Constraints\TargetAwareConstraintInterface; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Parser as YamlParser; @@ -157,6 +158,10 @@ private function loadClassMetadataFromYaml(ClassMetadata $metadata, array $class if (isset($classDescription['constraints']) && is_array($classDescription['constraints'])) { foreach ($this->parseNodes($classDescription['constraints']) as $constraint) { + if ($constraint instanceof TargetAwareConstraintInterface) { + $constraint->target = $metadata->getClassName(); + } + $metadata->addConstraint($constraint); } } diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php index 2ca64122aafae..89133631b14c2 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -22,6 +22,7 @@ use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintD; class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase { @@ -119,6 +120,74 @@ public function testLoadClassMetadata() $this->assertEquals($expected, $metadata); } + /** + * Test MetaData merge with parent annotation. + */ + public function testLoadParentClassMetadata() + { + $loader = new YamlFileLoader(__DIR__.'/constraint-mapping.yml'); + + // Load Parent MetaData + $parent_metadata = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\EntityParent'); + $loader->loadClassMetadata($parent_metadata); + + $expected_parent = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\EntityParent'); + $expected_parent->addConstraint(new ConstraintD(array('target' => 'Symfony\Component\Validator\Tests\Fixtures\EntityParent'))); + $expected_parent->addPropertyConstraint('other', new NotNull()); + + $this->assertEquals($expected_parent, $parent_metadata); + } + /** + * Test MetaData merge with parent annotation. + */ + public function testLoadClassMetadataAndMerge() + { + $loader = new YamlFileLoader(__DIR__.'/constraint-mapping.yml'); + + // Load Parent MetaData + $parent_metadata = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\EntityParent'); + $loader->loadClassMetadata($parent_metadata); + + $metadata = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\Entity'); + + // Merge parent metaData. + $metadata->mergeConstraints($parent_metadata); + + $loader->loadClassMetadata($metadata); + + $expected_parent = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\EntityParent'); + $expected_parent->addConstraint(new ConstraintD(array('target' => 'Symfony\Component\Validator\Tests\Fixtures\EntityParent'))); + $expected_parent->addPropertyConstraint('other', new NotNull()); + + $expected = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\Entity'); + $expected->mergeConstraints($expected_parent); + + $expected->setGroupSequence(array('Foo', 'Entity')); + $expected->addConstraint(new ConstraintA()); + $expected->addConstraint(new ConstraintB()); + $expected->addConstraint(new Callback('validateMe')); + $expected->addConstraint(new Callback('validateMeStatic')); + $expected->addConstraint(new Callback(array('Symfony\Component\Validator\Tests\Fixtures\CallbackClass', 'callback'))); + $expected->addPropertyConstraint('firstName', new NotNull()); + $expected->addPropertyConstraint('firstName', new Range(array('min' => 3))); + $expected->addPropertyConstraint('firstName', new Choice(array('A', 'B'))); + $expected->addPropertyConstraint('firstName', new All(array(new NotNull(), new Range(array('min' => 3))))); + $expected->addPropertyConstraint('firstName', new All(array('constraints' => array(new NotNull(), new Range(array('min' => 3)))))); + $expected->addPropertyConstraint('firstName', new Collection(array('fields' => array( + 'foo' => array(new NotNull(), new Range(array('min' => 3))), + 'bar' => array(new Range(array('min' => 5))), + )))); + $expected->addPropertyConstraint('firstName', new Choice(array( + 'message' => 'Must be one of %choices%', + 'choices' => array('A', 'B'), + ))); + $expected->addGetterConstraint('lastName', new NotNull()); + $expected->addGetterConstraint('valid', new IsTrue()); + $expected->addGetterConstraint('permissions', new IsTrue()); + + $this->assertEquals($expected, $metadata); + } + public function testLoadGroupSequenceProvider() { $loader = new YamlFileLoader(__DIR__.'/constraint-mapping.yml'); diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.yml b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.yml index c39168c96cac0..113038feee197 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.yml +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.yml @@ -1,6 +1,14 @@ namespaces: custom: Symfony\Component\Validator\Tests\Fixtures\ +Symfony\Component\Validator\Tests\Fixtures\EntityParent: + constraints: + # Target aware constraint + - Symfony\Component\Validator\Tests\Fixtures\ConstraintD: ~ + properties: + other: + - NotNull: ~ + Symfony\Component\Validator\Tests\Fixtures\Entity: group_sequence: - Foo From 04d3bdccbdc1cd0f7176921cd855bc3a99e4153d Mon Sep 17 00:00:00 2001 From: Mathieu Lemoine Date: Fri, 11 Dec 2015 19:23:36 -0500 Subject: [PATCH 4/9] [Validator] Target aware constraint support for XmlFileLoader --- .../Mapping/Loader/XmlFileLoader.php | 5 ++ .../Mapping/Loader/XmlFileLoaderTest.php | 69 +++++++++++++++++++ .../Mapping/Loader/constraint-mapping.xml | 9 +++ 3 files changed, 83 insertions(+) diff --git a/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php index 3458c7146a8f9..079ac5c00cab9 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Mapping\Loader; use Symfony\Component\Config\Util\XmlUtils; +use Symfony\Component\Validator\Constraints\TargetAwareConstraintInterface; use Symfony\Component\Validator\Exception\MappingException; use Symfony\Component\Validator\Mapping\ClassMetadata; @@ -201,6 +202,10 @@ private function loadClassMetadataFromXml(ClassMetadata $metadata, $classDescrip } foreach ($this->parseConstraints($classDescription->constraint) as $constraint) { + if ($constraint instanceof TargetAwareConstraintInterface) { + $constraint->target = $metadata->getClassName(); + } + $metadata->addConstraint($constraint); } diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php index e6326b9a3154d..e31350692e272 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -24,6 +24,7 @@ use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintD; class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase { @@ -77,6 +78,74 @@ public function testLoadClassMetadata() $this->assertEquals($expected, $metadata); } + /** + * Test MetaData merge with parent annotation. + */ + public function testLoadParentClassMetadata() + { + $loader = new XmlFileLoader(__DIR__.'/constraint-mapping.xml'); + + // Load Parent MetaData + $parent_metadata = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\EntityParent'); + $loader->loadClassMetadata($parent_metadata); + + $expected_parent = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\EntityParent'); + $expected_parent->addConstraint(new ConstraintD(array('target' => 'Symfony\Component\Validator\Tests\Fixtures\EntityParent'))); + $expected_parent->addPropertyConstraint('other', new NotNull()); + + $this->assertEquals($expected_parent, $parent_metadata); + } + /** + * Test MetaData merge with parent annotation. + */ + public function testLoadClassMetadataAndMerge() + { + $loader = new XmlFileLoader(__DIR__.'/constraint-mapping.xml'); + + // Load Parent MetaData + $parent_metadata = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\EntityParent'); + $loader->loadClassMetadata($parent_metadata); + + $metadata = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\Entity'); + + // Merge parent metaData. + $metadata->mergeConstraints($parent_metadata); + + $loader->loadClassMetadata($metadata); + + $expected_parent = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\EntityParent'); + $expected_parent->addConstraint(new ConstraintD(array('target' => 'Symfony\Component\Validator\Tests\Fixtures\EntityParent'))); + $expected_parent->addPropertyConstraint('other', new NotNull()); + + $expected = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\Entity'); + $expected->mergeConstraints($expected_parent); + + $expected->setGroupSequence(array('Foo', 'Entity')); + $expected->addConstraint(new ConstraintA()); + $expected->addConstraint(new ConstraintB()); + $expected->addConstraint(new Callback('validateMe')); + $expected->addConstraint(new Callback('validateMeStatic')); + $expected->addConstraint(new Callback(array('Symfony\Component\Validator\Tests\Fixtures\CallbackClass', 'callback'))); + $expected->addPropertyConstraint('firstName', new NotNull()); + $expected->addPropertyConstraint('firstName', new Range(array('min' => 3))); + $expected->addPropertyConstraint('firstName', new Choice(array('A', 'B'))); + $expected->addPropertyConstraint('firstName', new All(array(new NotNull(), new Range(array('min' => 3))))); + $expected->addPropertyConstraint('firstName', new All(array('constraints' => array(new NotNull(), new Range(array('min' => 3)))))); + $expected->addPropertyConstraint('firstName', new Collection(array('fields' => array( + 'foo' => array(new NotNull(), new Range(array('min' => 3))), + 'bar' => array(new Range(array('min' => 5))), + )))); + $expected->addPropertyConstraint('firstName', new Choice(array( + 'message' => 'Must be one of %choices%', + 'choices' => array('A', 'B'), + ))); + $expected->addGetterConstraint('lastName', new NotNull()); + $expected->addGetterConstraint('valid', new IsTrue()); + $expected->addGetterConstraint('permissions', new IsTrue()); + + $this->assertEquals($expected, $metadata); + } + public function testLoadClassMetadataWithNonStrings() { $loader = new XmlFileLoader(__DIR__.'/constraint-mapping-non-strings.xml'); diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml index 689184ecf2d81..a50e0b0af0cff 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml @@ -6,6 +6,15 @@ Symfony\Component\Validator\Tests\Fixtures\ + + + + + + + + + From c02329d1475cdfa0f8032efdfaccbc05407dd77e Mon Sep 17 00:00:00 2001 From: Mathieu Lemoine Date: Fri, 11 Dec 2015 19:23:38 -0500 Subject: [PATCH 5/9] [Validator] Target aware constraint support for AnnotationLoader --- .../Component/Validator/Mapping/Loader/AnnotationLoader.php | 5 +++++ .../Component/Validator/Tests/Fixtures/EntityParent.php | 3 +++ .../Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php | 3 +++ 3 files changed, 11 insertions(+) diff --git a/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php index b95ef164a346b..42ff3ee860cc9 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php @@ -16,6 +16,7 @@ use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\GroupSequenceProvider; +use Symfony\Component\Validator\Constraints\TargetAwareConstraintInterface; use Symfony\Component\Validator\Exception\MappingException; use Symfony\Component\Validator\Mapping\ClassMetadata; @@ -51,6 +52,10 @@ public function loadClassMetadata(ClassMetadata $metadata) } elseif ($constraint instanceof GroupSequenceProvider) { $metadata->setGroupSequenceProvider(true); } elseif ($constraint instanceof Constraint) { + if ($constraint instanceof TargetAwareConstraintInterface) { + $constraint->target = $metadata->getClassName(); + } + $metadata->addConstraint($constraint); } diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/EntityParent.php b/src/Symfony/Component/Validator/Tests/Fixtures/EntityParent.php index 422bb28d0b5ea..6e95dde06b00c 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/EntityParent.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/EntityParent.php @@ -13,6 +13,9 @@ use Symfony\Component\Validator\Constraints\NotNull; +/** + * @Symfony\Component\Validator\Tests\Fixtures\ConstraintD + */ class EntityParent { protected $firstName; diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php index 5a2beff29a109..675c7402372ab 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php @@ -22,6 +22,7 @@ use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintD; class AnnotationLoaderTest extends \PHPUnit_Framework_TestCase { @@ -89,6 +90,7 @@ public function testLoadParentClassMetadata() $loader->loadClassMetadata($parent_metadata); $expected_parent = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\EntityParent'); + $expected_parent->addConstraint(new ConstraintD(array('target' => 'Symfony\Component\Validator\Tests\Fixtures\EntityParent'))); $expected_parent->addPropertyConstraint('other', new NotNull()); $expected_parent->getReflectionClass(); @@ -113,6 +115,7 @@ public function testLoadClassMetadataAndMerge() $loader->loadClassMetadata($metadata); $expected_parent = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\EntityParent'); + $expected_parent->addConstraint(new ConstraintD(array('target' => 'Symfony\Component\Validator\Tests\Fixtures\EntityParent'))); $expected_parent->addPropertyConstraint('other', new NotNull()); $expected_parent->getReflectionClass(); From 697833b89159710bc4def52729e8d69a5d003e3c Mon Sep 17 00:00:00 2001 From: Mathieu Lemoine Date: Fri, 11 Dec 2015 19:23:41 -0500 Subject: [PATCH 6/9] Update CHANGELOG --- src/Symfony/Component/Validator/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 0ff667b770685..16e7fb491d7ec 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +3.1.0 +----- + + * added Target Aware Constraints and its support in loaders for class constraints + 2.8.0 ----- From dba386e31ef3beafcaf6906e388b3c138ad88306 Mon Sep 17 00:00:00 2001 From: Mathieu Lemoine Date: Tue, 2 Feb 2016 16:50:27 -0500 Subject: [PATCH 7/9] Add Mock MetadataFactory in UniqueEntityValidatorTest --- .../Constraints/UniqueEntityValidatorTest.php | 75 ++++++++++++++----- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php index e48a0488ef0cc..7f846cccad010 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php @@ -87,30 +87,69 @@ protected function createEntityManagerMock($repositoryMock) ->will($this->returnValue($repositoryMock)) ; - $classMetadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); - $classMetadata + $metadataFactory = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadataFactory'); + + $metadata = []; + + $metadataFactory ->expects($this->any()) - ->method('hasField') - ->will($this->returnValue(true)) - ; - $reflParser = $this->getMockBuilder('Doctrine\Common\Reflection\StaticReflectionParser') - ->disableOriginalConstructor() - ->getMock() - ; - $refl = $this->getMockBuilder('Doctrine\Common\Reflection\StaticReflectionProperty') - ->setConstructorArgs(array($reflParser, 'property-name')) - ->setMethods(array('getValue')) - ->getMock() + ->method('getMetadataFor') + ->will($this->returnCallback(function($className) use (&$metadata) { + if (isset($metadata[$className])) { + return $metadata[$className]; + } + + $classMetadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + + $classMetadata + ->expects($this->any()) + ->method('getName') + ->will($this->returnValue($className)) + ; + + $classMetadata + ->expects($this->any()) + ->method('hasField') + ->will($this->returnValue(true)) + ; + $reflParser = $this->getMockBuilder('Doctrine\Common\Reflection\StaticReflectionParser') + ->disableOriginalConstructor() + ->getMock() + ; + $refl = $this->getMockBuilder('Doctrine\Common\Reflection\StaticReflectionProperty') + ->setConstructorArgs(array($reflParser, 'property-name')) + ->setMethods(array('getValue')) + ->getMock() + ; + $refl + ->expects($this->any()) + ->method('getValue') + ->will($this->returnValue(true)) + ; + $classMetadata->reflFields = array('name' => $refl); + + $classMetadata->isMappedSuperclass = false !== strpos($className, 'MappedSuperClass'); + + return $metadata[$className] = $classMetadata; + })) ; - $refl + + $metadataFactory ->expects($this->any()) - ->method('getValue') - ->will($this->returnValue(true)) + ->method('isTransient') + ->will($this->returnCallback(function($className) { + return false !== strpos($className, 'Transient'); + })) ; - $classMetadata->reflFields = array('name' => $refl); + + $em->expects($this->any()) + ->method('getMetadataFactory') + ->will($this->returnValue($metadataFactory)) + ; + $em->expects($this->any()) ->method('getClassMetadata') - ->will($this->returnValue($classMetadata)) + ->will($this->returnCallback([$metadataFactory, 'getMetadataFor'])) ; return $em; From 6c4b3ae987eace8c007a0939723e7b89a7a6ea58 Mon Sep 17 00:00:00 2001 From: Mathieu Lemoine Date: Fri, 11 Dec 2015 19:25:50 -0500 Subject: [PATCH 8/9] [DoctrineBridge] Fix UniqueEntity interaction with inheritance Fixes #16969 Fixes #4087 Fixes #12573 Incompatible with #15002 Incompatible with #12977 --- .../Doctrine/Tests/Fixtures/ChildEntity.php | 21 ++++ .../Fixtures/HighestEntity/GoodEntity.php | 21 ++++ .../Fixtures/HighestEntity/InstanceEntity.php | 21 ++++ .../Fixtures/HighestEntity/LowerEntity.php | 21 ++++ .../HighestEntity/LowerMappedSuperClass.php | 19 ++++ .../Fixtures/HighestEntity/LowerTransient.php | 16 +++ .../Fixtures/HighestEntity/SideEntity.php | 21 ++++ .../HighestEntity/SideMappedSuperClass.php | 19 ++++ .../Fixtures/HighestEntity/SideTransient.php | 16 +++ .../HighestEntity/TargetMappedSuperClass.php | 19 ++++ .../HighestEntity/TargetTransient.php | 16 +++ .../Fixtures/HighestEntity/TopEntity.php | 57 +++++++++++ .../HighestEntity/UpperMappedSuperClass.php | 19 ++++ .../Fixtures/HighestEntity/UpperTransient.php | 16 +++ .../Tests/Fixtures/SingleIntIdEntity.php | 10 +- .../Constraints/UniqueEntityValidatorTest.php | 97 ++++++++++++++++--- .../Validator/Constraints/UniqueEntity.php | 14 +-- .../Constraints/UniqueEntityValidator.php | 49 +++++++++- 18 files changed, 446 insertions(+), 26 deletions(-) create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Fixtures/ChildEntity.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/GoodEntity.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/InstanceEntity.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/LowerEntity.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/LowerMappedSuperClass.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/LowerTransient.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/SideEntity.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/SideMappedSuperClass.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/SideTransient.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/TargetMappedSuperClass.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/TargetTransient.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/TopEntity.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/UpperMappedSuperClass.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/UpperTransient.php diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/ChildEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/ChildEntity.php new file mode 100644 index 0000000000000..268e7f74e4cc1 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/ChildEntity.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures; + +use Doctrine\ORM\Mapping\Id; +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Entity; + +/** @Entity */ +class ChildEntity extends SingleIntIdEntity +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/GoodEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/GoodEntity.php new file mode 100644 index 0000000000000..92dd372e61664 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/GoodEntity.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity; + +use Doctrine\ORM\Mapping\Entity; + +/** + * @Entity + */ +class GoodEntity extends UpperTransient +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/InstanceEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/InstanceEntity.php new file mode 100644 index 0000000000000..bd3b6ee9baf2e --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/InstanceEntity.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity; + +use Doctrine\ORM\Mapping\Entity; + +/** + * @Entity + */ +class InstanceEntity extends LowerEntity +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/LowerEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/LowerEntity.php new file mode 100644 index 0000000000000..c334ef40c963d --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/LowerEntity.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity; + +use Doctrine\ORM\Mapping\Entity; + +/** + * @Entity + */ +class LowerEntity extends LowerTransient +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/LowerMappedSuperClass.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/LowerMappedSuperClass.php new file mode 100644 index 0000000000000..f4bc192952b07 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/LowerMappedSuperClass.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity; + +use Doctrine\ORM\Mapping\MappedSuperclass; + +/** @MappedSuperclass */ +class LowerMappedSuperClass extends GoodEntity +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/LowerTransient.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/LowerTransient.php new file mode 100644 index 0000000000000..ed51a46cdf1fb --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/LowerTransient.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity; + +class LowerTransient extends LowerMappedSuperClass +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/SideEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/SideEntity.php new file mode 100644 index 0000000000000..bd31392b40f04 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/SideEntity.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity; + +use Doctrine\ORM\Mapping\Entity; + +/** + * @Entity + */ +class SideEntity extends SideTransient +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/SideMappedSuperClass.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/SideMappedSuperClass.php new file mode 100644 index 0000000000000..d00a010684439 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/SideMappedSuperClass.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity; + +use Doctrine\ORM\Mapping\MappedSuperclass; + +/** @MappedSuperclass */ +class SideMappedSuperClass extends GoodEntity +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/SideTransient.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/SideTransient.php new file mode 100644 index 0000000000000..94a0764bcedbf --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/SideTransient.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity; + +class SideTransient extends SideMappedSuperClass +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/TargetMappedSuperClass.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/TargetMappedSuperClass.php new file mode 100644 index 0000000000000..507230fdf8595 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/TargetMappedSuperClass.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity; + +use Doctrine\ORM\Mapping\MappedSuperclass; + +/** @MappedSuperclass */ +class TargetMappedSuperClass extends TopEntity +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/TargetTransient.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/TargetTransient.php new file mode 100644 index 0000000000000..dc8aa20b60e6f --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/TargetTransient.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity; + +class TargetTransient extends TargetMappedSuperClass +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/TopEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/TopEntity.php new file mode 100644 index 0000000000000..1c1934d25ebc0 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/TopEntity.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity; + +use Doctrine\ORM\Mapping\Id; +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\DiscriminatorColumn; +use Doctrine\ORM\Mapping\DiscriminatorMap; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\InheritanceType; + +/** + * @Entity + * @InheritanceType("SINGLE_TABLE") + * @DiscriminatorColumn(name="discr", type="string") + * @DiscriminatorMap({ + * "top" = "TopEntity", + * "good" = "GoodEntity", + * "low" = "LowEntity", + * "side" = "SideEntity", + * "inst" = "InstanceEntity", + * }) + * + * The point of this hierarchy is to test the selection of the repository + * by the UniqueEntityValidator. This hierarchy provides: + * - A top entity which is too high in the inheritance chain to be selected + * - Both a transient and a mapped super class targets + * - Upper mapped super and transient classes which should not be selected + * - A good Entity which is intended to provide the repo + * - A side branch including one of each Entity, Mapped Super and Transient + * - A lower part including one of each Entity, Mapped Super and Transient + * which are not intended to be selected since they lower than the GoodEntity + * - An InstanceEntity which is intended to be validated + */ +class TopEntity +{ + /** @Id @Column(type="integer") */ + protected $id; + + /** @Column(type="string", nullable=true) */ + public $name; + + public function __construct($id, $name) + { + $this->id = $id; + $this->name = $name; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/UpperMappedSuperClass.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/UpperMappedSuperClass.php new file mode 100644 index 0000000000000..433693b11e943 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/UpperMappedSuperClass.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity; + +use Doctrine\ORM\Mapping\MappedSuperclass; + +/** @MappedSuperclass */ +class UpperMappedSuperClass extends TargetTransient +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/UpperTransient.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/UpperTransient.php new file mode 100644 index 0000000000000..293a64ddd2fed --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/HighestEntity/UpperTransient.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity; + +class UpperTransient extends UpperMappedSuperClass +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleIntIdEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleIntIdEntity.php index 44630a1fc51f1..1c35cf7a4b300 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleIntIdEntity.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleIntIdEntity.php @@ -13,9 +13,17 @@ use Doctrine\ORM\Mapping\Id; use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\DiscriminatorColumn; +use Doctrine\ORM\Mapping\DiscriminatorMap; use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\InheritanceType; -/** @Entity */ +/** + * @Entity + * @InheritanceType("SINGLE_TABLE") + * @DiscriminatorColumn(name="discr", type="string") + * @DiscriminatorMap({"parent" = "SingleIntIdEntity", "child" = "ChildEntity"}) + */ class SingleIntIdEntity { /** @Id @Column(type="integer") */ diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php index 7f846cccad010..3ee283a1907ef 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php @@ -16,10 +16,12 @@ use Doctrine\Common\Persistence\ObjectManager; use Doctrine\Common\Persistence\ObjectRepository; use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; +use Symfony\Bridge\Doctrine\Tests\Fixtures\ChildEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\DoubleNameEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity; +use Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity\InstanceEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator; use Symfony\Component\Validator\Tests\Constraints\AbstractConstraintValidatorTest; @@ -82,10 +84,13 @@ protected function createEntityManagerMock($repositoryMock) $em = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager') ->getMock() ; - $em->expects($this->any()) - ->method('getRepository') - ->will($this->returnValue($repositoryMock)) - ; + + if ($repositoryMock) { + $em->expects($this->any()) + ->method('getRepository') + ->will($this->returnValue($repositoryMock)) + ; + } $metadataFactory = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadataFactory'); @@ -180,6 +185,7 @@ public function testValidateUniqueness() 'message' => 'myMessage', 'fields' => array('name'), 'em' => self::EM_NAME, + 'target' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity', )); $entity1 = new SingleIntIdEntity(1, 'Foo'); @@ -212,6 +218,7 @@ public function testValidateCustomErrorPath() 'fields' => array('name'), 'em' => self::EM_NAME, 'errorPath' => 'bar', + 'target' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity', )); $entity1 = new SingleIntIdEntity(1, 'Foo'); @@ -235,6 +242,7 @@ public function testValidateUniquenessWithNull() 'message' => 'myMessage', 'fields' => array('name'), 'em' => self::EM_NAME, + 'target' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity', )); $entity1 = new SingleIntIdEntity(1, null); @@ -256,6 +264,7 @@ public function testValidateUniquenessWithIgnoreNull() 'fields' => array('name', 'name2'), 'em' => self::EM_NAME, 'ignoreNull' => false, + 'target' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\DoubleNameEntity', )); $entity1 = new DoubleNameEntity(1, 'Foo', null); @@ -288,6 +297,7 @@ public function testValidateUniquenessWithValidCustomErrorPath() 'fields' => array('name', 'name2'), 'em' => self::EM_NAME, 'errorPath' => 'name2', + 'target' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\DoubleNameEntity', )); $entity1 = new DoubleNameEntity(1, 'Foo', 'Bar'); @@ -320,6 +330,7 @@ public function testValidateUniquenessUsingCustomRepositoryMethod() 'fields' => array('name'), 'em' => self::EM_NAME, 'repositoryMethod' => 'findByCustom', + 'target' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity', )); $repository = $this->createRepositoryMock(); @@ -346,6 +357,7 @@ public function testValidateUniquenessWithUnrewoundArray() 'fields' => array('name'), 'em' => self::EM_NAME, 'repositoryMethod' => 'findByCustom', + 'target' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity', )); $entity = new SingleIntIdEntity(1, 'foo'); @@ -384,6 +396,7 @@ public function testValidateResultTypes($entity1, $result) 'fields' => array('name'), 'em' => self::EM_NAME, 'repositoryMethod' => 'findByCustom', + 'target' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity', )); $repository = $this->createRepositoryMock(); @@ -418,6 +431,7 @@ public function testAssociatedEntity() 'message' => 'myMessage', 'fields' => array('single'), 'em' => self::EM_NAME, + 'target' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity', )); $entity1 = new SingleIntIdEntity(1, 'foo'); @@ -453,6 +467,7 @@ public function testAssociatedEntityWithNull() 'fields' => array('single'), 'em' => self::EM_NAME, 'ignoreNull' => false, + 'target' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity', )); $associated = new AssociationEntity(); @@ -476,6 +491,7 @@ public function testAssociatedCompositeEntity() 'message' => 'myMessage', 'fields' => array('composite'), 'em' => self::EM_NAME, + 'target' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity', )); $composite = new CompositeIntIdEntity(1, 1, 'test'); @@ -499,6 +515,7 @@ public function testDedicatedEntityManagerNullObject() 'message' => 'myMessage', 'fields' => array('name'), 'em' => self::EM_NAME, + 'target' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity', )); $this->em = null; @@ -511,24 +528,80 @@ public function testDedicatedEntityManagerNullObject() $this->validator->validate($entity, $constraint); } - /** - * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException - * @expectedExceptionMessage Unable to find the object manager associated with an entity of class "Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity" - */ - public function testEntityManagerNullObject() + public function testTargetsRepoIsUsedForChildren() { $constraint = new UniqueEntity(array( 'message' => 'myMessage', 'fields' => array('name'), - // no "em" option set + 'em' => self::EM_NAME, + 'target' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity', )); - $this->em = null; + $repository = $this->createRepositoryMock(); + $this->em = $this->createEntityManagerMock(null); + $this->em->expects($this->atLeastOnce()) + ->method('getRepository') + ->with('Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity') + ->will($this->returnValue($repository)) + ; $this->registry = $this->createRegistryMock($this->em); + $this->validator = $this->createValidator(); $this->validator->initialize($this->context); - $entity = new SingleIntIdEntity(1, null); + $entity = new ChildEntity(1, 'Foo'); + + $this->validator->validate($entity, $constraint); + } + + public function testTargetRepoIsHighestEntityFromTransient() + { + $constraint = new UniqueEntity(array( + 'message' => 'myMessage', + 'fields' => array('name'), + 'em' => self::EM_NAME, + 'target' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity\TargetTransient', + )); + + $repository = $this->createRepositoryMock(); + $this->em = $this->createEntityManagerMock(null); + $this->em->expects($this->atLeastOnce()) + ->method('getRepository') + ->with('Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity\GoodEntity') + ->will($this->returnValue($repository)) + ; + $this->registry = $this->createRegistryMock($this->em); + + $this->validator = $this->createValidator(); + $this->validator->initialize($this->context); + + $entity = new InstanceEntity(1, 'Foo'); + + $this->validator->validate($entity, $constraint); + } + + public function testTargetRepoIsHighestEntityFromMappedSuperClass() + { + $constraint = new UniqueEntity(array( + 'message' => 'myMessage', + 'fields' => array('name'), + 'em' => self::EM_NAME, + 'target' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity\TargetMappedSuperClass', + )); + + $repository = $this->createRepositoryMock(); + $this->em = $this->createEntityManagerMock(null); + $this->em->expects($this->atLeastOnce()) + ->method('getRepository') + ->with('Symfony\Bridge\Doctrine\Tests\Fixtures\HighestEntity\GoodEntity') + ->will($this->returnValue($repository)) + ; + $this->registry = $this->createRegistryMock($this->em); + + $this->validator = $this->createValidator(); + $this->validator->initialize($this->context); + + $entity = new InstanceEntity(1, 'Foo'); $this->validator->validate($entity, $constraint); } diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php index fc6e213bd772e..d35fb0332f2a7 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php @@ -12,6 +12,8 @@ namespace Symfony\Bridge\Doctrine\Validator\Constraints; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\TargetAwareConstraintInterface; +use Symfony\Component\Validator\Constraints\TargetAwareConstraintTrait; /** * Constraint for the Unique Entity validator. @@ -21,8 +23,10 @@ * * @author Benjamin Eberlei */ -class UniqueEntity extends Constraint +class UniqueEntity extends Constraint implements TargetAwareConstraintInterface { + use TargetAwareConstraintTrait; + public $message = 'This value is already used.'; public $service = 'doctrine.orm.validator.unique'; public $em = null; @@ -46,14 +50,6 @@ public function validatedBy() return $this->service; } - /** - * {@inheritdoc} - */ - public function getTargets() - { - return self::CLASS_CONSTRAINT; - } - public function getDefaultOption() { return 'fields'; diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php index 91c62a4de5d53..6187dc6e55b20 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\Doctrine\Validator\Constraints; +use Doctrine\Common\Persistence\Mapping\ClassMetadataFactory; use Doctrine\Common\Persistence\ManagerRegistry; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\UnexpectedTypeException; @@ -34,6 +35,40 @@ public function __construct(ManagerRegistry $registry) $this->registry = $registry; } + /** + * @param ClassMetadataFactory $metadataFactory + * @param string $target + * @param string $entityClass + * + * @return \Doctrine\Common\Persistence\Mapping\ClassMetadata + */ + private function findHighestEntityMetadata(ClassMetadataFactory $metadataFactory, $target, $entityClass) + { + if (!$metadataFactory->isTransient($target)) { + $metadata = $metadataFactory->getMetadataFor($target); + + if (!$metadata->isMappedSuperclass) { + return $metadata; + } + } + + $highestEntity = null; + + while (is_subclass_of($entityClass, $target)) { + if (!$metadataFactory->isTransient($entityClass)) { + $metadata = $metadataFactory->getMetadataFor($entityClass); + + if (!$metadata->isMappedSuperclass) { + $highestEntity = $metadata; + } + } + + $entityClass = get_parent_class($entityClass); + } + + return $highestEntity; + } + /** * @param object $entity * @param Constraint $constraint @@ -61,6 +96,13 @@ public function validate($entity, Constraint $constraint) throw new ConstraintDefinitionException('At least one field has to be specified.'); } + $target = $constraint->target; + /* @var $target string */ + if (!$target) { + @trigger_error('Not providing a target for a UniqueEntity constraint is deprecated since version 3.1 and will be removed in 4.0.', E_USER_DEPRECATED); + $target = get_class($entity); + } + if ($constraint->em) { $em = $this->registry->getManager($constraint->em); @@ -68,15 +110,14 @@ public function validate($entity, Constraint $constraint) throw new ConstraintDefinitionException(sprintf('Object manager "%s" does not exist.', $constraint->em)); } } else { - $em = $this->registry->getManagerForClass(get_class($entity)); + $em = $this->registry->getManagerForClass($target); if (!$em) { throw new ConstraintDefinitionException(sprintf('Unable to find the object manager associated with an entity of class "%s".', get_class($entity))); } } - $class = $em->getClassMetadata(get_class($entity)); - /* @var $class \Doctrine\Common\Persistence\Mapping\ClassMetadata */ + $class = $this->findHighestEntityMetadata($em->getMetadataFactory(), $target, get_class($entity)); $criteria = array(); foreach ($fields as $fieldName) { @@ -110,7 +151,7 @@ public function validate($entity, Constraint $constraint) } } - $repository = $em->getRepository(get_class($entity)); + $repository = $em->getRepository($class->getName()); $result = $repository->{$constraint->repositoryMethod}($criteria); if ($result instanceof \IteratorAggregate) { From 3868657a46eee357963db70edae5cca64df4b286 Mon Sep 17 00:00:00 2001 From: Mathieu Lemoine Date: Fri, 11 Dec 2015 19:25:52 -0500 Subject: [PATCH 9/9] [DoctrineBridge][Validator] CHANGELOG & UPGRADE --- UPGRADE-4.0.md | 6 ++++++ src/Symfony/Bridge/Doctrine/CHANGELOG.md | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md index 2d402d75fbb88..6592c8197ab1f 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -26,3 +26,9 @@ Yaml * The `!!php/object` tag to indicate dumped PHP objects was removed in favor of the `!php/object` tag. + +Doctrine Bridge +--- + + * The `UniqueEntity` constraints now requires a `target` property when loaded + with the `StaticMethodLoader`. diff --git a/src/Symfony/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Bridge/Doctrine/CHANGELOG.md index 913868ef50f42..58234e2f9945c 100644 --- a/src/Symfony/Bridge/Doctrine/CHANGELOG.md +++ b/src/Symfony/Bridge/Doctrine/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +3.2.0 +----- + + * fixed `UniqueEntity` interaction with inheritance + * deprecated `UniqueEntity` constraints without a target (possible only if + `StaticMethodLoader` or a custom constraint loader is used) + 3.1.0 -----