diff --git a/CHANGELOG-2.1.md b/CHANGELOG-2.1.md index 398d0c7d15..8caeaacbb8 100644 --- a/CHANGELOG-2.1.md +++ b/CHANGELOG-2.1.md @@ -305,6 +305,7 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c * made ExecutionContext immutable * deprecated Constraint methods `setMessage`, `getMessageTemplate` and `getMessageParameters` + * added support for dynamic group sequences with the GroupSequenceProvider pattern ### Yaml diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequenceProvider.php b/src/Symfony/Component/Validator/Constraints/GroupSequenceProvider.php new file mode 100644 index 0000000000..457155981a --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/GroupSequenceProvider.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\Constraints; + +/** + * Annotation to define a group sequence provider + * + * @Annotation + */ +class GroupSequenceProvider +{ + +} diff --git a/src/Symfony/Component/Validator/GraphWalker.php b/src/Symfony/Component/Validator/GraphWalker.php index dbdcfdb70e..0f4b99e634 100644 --- a/src/Symfony/Component/Validator/GraphWalker.php +++ b/src/Symfony/Component/Validator/GraphWalker.php @@ -17,6 +17,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\MemberMetadata; +use Symfony\Component\Validator\GroupSequenceProviderInterface; /** * Responsible for walking over and initializing validation on different @@ -67,8 +68,13 @@ class GraphWalker $initializer->initialize($object); } - if ($group === Constraint::DEFAULT_GROUP && $metadata->hasGroupSequence()) { - $groups = $metadata->getGroupSequence(); + if ($group === Constraint::DEFAULT_GROUP && ($metadata->hasGroupSequence() || $metadata->isGroupSequenceProvider())) { + if ($metadata->hasGroupSequence()) { + $groups = $metadata->getGroupSequence(); + } else { + $groups = $object->getGroupSequence(); + } + foreach ($groups as $group) { $this->walkObjectForGroup($metadata, $object, $group, $propertyPath, Constraint::DEFAULT_GROUP); diff --git a/src/Symfony/Component/Validator/GroupSequenceProviderInterface.php b/src/Symfony/Component/Validator/GroupSequenceProviderInterface.php new file mode 100644 index 0000000000..676a3b399f --- /dev/null +++ b/src/Symfony/Component/Validator/GroupSequenceProviderInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator; + +/** + * Defines the interface for a group sequence provider. + */ +interface GroupSequenceProviderInterface +{ + /** + * Returns which validation groups should be used for a certain state + * of the object. + * + * @return array An array of validation groups + */ + function getGroupSequence(); +} diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 237d854cbb..2d8c60f098 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -29,6 +29,7 @@ class ClassMetadata extends ElementMetadata public $properties = array(); public $getters = array(); public $groupSequence = array(); + public $groupSequenceProvider = false; private $reflClass; /** @@ -57,6 +58,7 @@ class ClassMetadata extends ElementMetadata return array_merge(parent::__sleep(), array( 'getters', 'groupSequence', + 'groupSequenceProvider', 'members', 'name', 'properties', @@ -247,6 +249,10 @@ class ClassMetadata extends ElementMetadata */ public function setGroupSequence(array $groups) { + if ($this->isGroupSequenceProvider()) { + throw new GroupDefinitionException('Defining a static group sequence is not allowed with a group sequence provider'); + } + if (in_array(Constraint::DEFAULT_GROUP, $groups, true)) { throw new GroupDefinitionException(sprintf('The group "%s" is not allowed in group sequences', Constraint::DEFAULT_GROUP)); } @@ -293,4 +299,32 @@ class ClassMetadata extends ElementMetadata return $this->reflClass; } + + /** + * Sets whether a group sequence provider should be used + * + * @param boolean $active + */ + public function setGroupSequenceProvider($active) + { + if ($this->hasGroupSequence()) { + throw new GroupDefinitionException('Defining a group sequence provider is not allowed with a static group sequence'); + } + + if (!$this->getReflectionClass()->implementsInterface('Symfony\Component\Validator\GroupSequenceProviderInterface')) { + throw new GroupDefinitionException(sprintf('Class "%s" must implement GroupSequenceProviderInterface', $this->name)); + } + + $this->groupSequenceProvider = $active; + } + + /** + * Returns whether the class is a group sequence provider. + * + * @return boolean + */ + public function isGroupSequenceProvider() + { + return $this->groupSequenceProvider; + } } diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php index a16dfdc3b6..691f830c88 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php @@ -58,6 +58,9 @@ class ClassMetadataFactory implements ClassMetadataFactoryInterface // Include constraints from all implemented interfaces foreach ($metadata->getReflectionClass()->getInterfaces() as $interface) { + if ('Symfony\Component\Validator\GroupSequenceProviderInterface' === $interface->getName()) { + continue; + } $metadata->mergeConstraints($this->getClassMetadata($interface->getName())); } diff --git a/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php index 2474dba79f..30642a0474 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php @@ -15,6 +15,7 @@ use Doctrine\Common\Annotations\Reader; use Symfony\Component\Validator\Exception\MappingException; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\GroupSequenceProvider; use Symfony\Component\Validator\Constraint; class AnnotationLoader implements LoaderInterface @@ -38,6 +39,8 @@ class AnnotationLoader implements LoaderInterface foreach ($this->reader->getClassAnnotations($reflClass) as $constraint) { if ($constraint instanceof GroupSequence) { $metadata->setGroupSequence($constraint->groups); + } elseif ($constraint instanceof GroupSequenceProvider) { + $metadata->setGroupSequenceProvider(true); } elseif ($constraint instanceof Constraint) { $metadata->addConstraint($constraint); } diff --git a/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php index ef130d4b42..4c185c604d 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php @@ -43,6 +43,10 @@ class XmlFileLoader extends FileLoader if (isset($this->classes[$metadata->getClassName()])) { $xml = $this->classes[$metadata->getClassName()]; + foreach ($xml->{'group-sequence-provider'} as $provider) { + $metadata->setGroupSequenceProvider(true); + } + foreach ($this->parseConstraints($xml->constraint) as $constraint) { $metadata->addConstraint($constraint); } diff --git a/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php index c25e49cab3..db65f684bb 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php @@ -54,6 +54,10 @@ class YamlFileLoader extends FileLoader if (isset($this->classes[$metadata->getClassName()])) { $yaml = $this->classes[$metadata->getClassName()]; + if (isset($yaml['group_sequence_provider'])) { + $metadata->setGroupSequenceProvider((bool)$yaml['group_sequence_provider']); + } + if (isset($yaml['constraints'])) { foreach ($this->parseNodes($yaml['constraints']) as $constraint) { $metadata->addConstraint($constraint); diff --git a/src/Symfony/Component/Validator/Mapping/Loader/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd b/src/Symfony/Component/Validator/Mapping/Loader/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd index 61232da570..6d5d3b5f95 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd +++ b/src/Symfony/Component/Validator/Mapping/Loader/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd @@ -52,6 +52,7 @@ ]]> + @@ -59,6 +60,14 @@ + + + + + + groups = $groups; + } + + public function getGroupSequence() + { + return $this->groups; + } +} diff --git a/tests/Symfony/Tests/Component/Validator/Mapping/ClassMetadataTest.php b/tests/Symfony/Tests/Component/Validator/Mapping/ClassMetadataTest.php index 5ec7abff8b..7da676aba8 100644 --- a/tests/Symfony/Tests/Component/Validator/Mapping/ClassMetadataTest.php +++ b/tests/Symfony/Tests/Component/Validator/Mapping/ClassMetadataTest.php @@ -14,10 +14,12 @@ namespace Symfony\Tests\Component\Validator\Mapping; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Exception\GroupDefinitionException; use Symfony\Tests\Component\Validator\Fixtures\Entity; use Symfony\Tests\Component\Validator\Fixtures\ConstraintA; use Symfony\Tests\Component\Validator\Fixtures\ConstraintB; use Symfony\Tests\Component\Validator\Fixtures\PropertyConstraint; +use Symfony\Tests\Component\Validator\Fixtures\GroupSequenceProvider; require_once __DIR__.'/../Fixtures/Entity.php'; require_once __DIR__.'/../Fixtures/ConstraintA.php'; @@ -28,6 +30,7 @@ class ClassMetadataTest extends \PHPUnit_Framework_TestCase { const CLASSNAME = 'Symfony\Tests\Component\Validator\Fixtures\Entity'; const PARENTCLASS = 'Symfony\Tests\Component\Validator\Fixtures\EntityParent'; + const PROVIDERCLASS = 'Symfony\Tests\Component\Validator\Fixtures\GroupSequenceProviderEntity'; protected $metadata; @@ -189,5 +192,40 @@ class ClassMetadataTest extends \PHPUnit_Framework_TestCase $this->metadata->setGroupSequence(array('Foo', $this->metadata->getDefaultGroup(), Constraint::DEFAULT_GROUP)); } -} + /** + * @expectedException Symfony\Component\Validator\Exception\GroupDefinitionException + */ + public function testGroupSequenceFailsIfGroupSequenceProviderIsSet() + { + $metadata = new ClassMetadata(self::PROVIDERCLASS); + $metadata->setGroupSequenceProvider(true); + $metadata->setGroupSequence(array('GroupSequenceProviderEntity', 'Foo')); + } + + /** + * @expectedException Symfony\Component\Validator\Exception\GroupDefinitionException + */ + public function testGroupSequenceProviderFailsIfGroupSequenceIsSet() + { + $metadata = new ClassMetadata(self::PROVIDERCLASS); + $metadata->setGroupSequence(array('GroupSequenceProviderEntity', 'Foo')); + $metadata->setGroupSequenceProvider(true); + } + + /** + * @expectedException Symfony\Component\Validator\Exception\GroupDefinitionException + */ + public function testGroupSequenceProviderFailsIfDomainClassIsInvalid() + { + $metadata = new ClassMetadata('stdClass'); + $metadata->setGroupSequenceProvider(true); + } + + public function testGroupSequenceProvider() + { + $metadata = new ClassMetadata(self::PROVIDERCLASS); + $metadata->setGroupSequenceProvider(true); + $this->assertTrue($metadata->isGroupSequenceProvider()); + } +} diff --git a/tests/Symfony/Tests/Component/Validator/Mapping/Loader/AnnotationLoaderTest.php b/tests/Symfony/Tests/Component/Validator/Mapping/Loader/AnnotationLoaderTest.php index 58f215c9fa..2a6e5dbf54 100644 --- a/tests/Symfony/Tests/Component/Validator/Mapping/Loader/AnnotationLoaderTest.php +++ b/tests/Symfony/Tests/Component/Validator/Mapping/Loader/AnnotationLoaderTest.php @@ -144,4 +144,17 @@ class AnnotationLoaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, $metadata); } + public function testLoadGroupSequenceProviderAnnotation() + { + $loader = new AnnotationLoader(new AnnotationReader()); + + $metadata = new ClassMetadata('Symfony\Tests\Component\Validator\Fixtures\GroupSequenceProviderEntity'); + $loader->loadClassMetadata($metadata); + + $expected = new ClassMetadata('Symfony\Tests\Component\Validator\Fixtures\GroupSequenceProviderEntity'); + $expected->setGroupSequenceProvider(true); + $expected->getReflectionClass(); + + $this->assertEquals($expected, $metadata); + } } diff --git a/tests/Symfony/Tests/Component/Validator/Mapping/Loader/XmlFileLoaderTest.php b/tests/Symfony/Tests/Component/Validator/Mapping/Loader/XmlFileLoaderTest.php index 8746422aac..8afae6c1ef 100644 --- a/tests/Symfony/Tests/Component/Validator/Mapping/Loader/XmlFileLoaderTest.php +++ b/tests/Symfony/Tests/Component/Validator/Mapping/Loader/XmlFileLoaderTest.php @@ -70,4 +70,17 @@ class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, $metadata); } + + public function testLoadGroupSequenceProvider() + { + $loader = new XmlFileLoader(__DIR__.'/constraint-mapping.xml'); + $metadata = new ClassMetadata('Symfony\Tests\Component\Validator\Fixtures\GroupSequenceProviderEntity'); + + $loader->loadClassMetadata($metadata); + + $expected = new ClassMetadata('Symfony\Tests\Component\Validator\Fixtures\GroupSequenceProviderEntity'); + $expected->setGroupSequenceProvider(true); + + $this->assertEquals($expected, $metadata); + } } diff --git a/tests/Symfony/Tests/Component/Validator/Mapping/Loader/YamlFileLoaderTest.php b/tests/Symfony/Tests/Component/Validator/Mapping/Loader/YamlFileLoaderTest.php index b661efbf94..2d1d458efe 100644 --- a/tests/Symfony/Tests/Component/Validator/Mapping/Loader/YamlFileLoaderTest.php +++ b/tests/Symfony/Tests/Component/Validator/Mapping/Loader/YamlFileLoaderTest.php @@ -88,4 +88,17 @@ class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, $metadata); } + + public function testLoadGroupSequenceProvider() + { + $loader = new YamlFileLoader(__DIR__.'/constraint-mapping.yml'); + $metadata = new ClassMetadata('Symfony\Tests\Component\Validator\Fixtures\GroupSequenceProviderEntity'); + + $loader->loadClassMetadata($metadata); + + $expected = new ClassMetadata('Symfony\Tests\Component\Validator\Fixtures\GroupSequenceProviderEntity'); + $expected->setGroupSequenceProvider(true); + + $this->assertEquals($expected, $metadata); + } } diff --git a/tests/Symfony/Tests/Component/Validator/Mapping/Loader/constraint-mapping.xml b/tests/Symfony/Tests/Component/Validator/Mapping/Loader/constraint-mapping.xml index 03c2467951..916394b1bd 100644 --- a/tests/Symfony/Tests/Component/Validator/Mapping/Loader/constraint-mapping.xml +++ b/tests/Symfony/Tests/Component/Validator/Mapping/Loader/constraint-mapping.xml @@ -77,4 +77,11 @@ + + + + + + + diff --git a/tests/Symfony/Tests/Component/Validator/Mapping/Loader/constraint-mapping.yml b/tests/Symfony/Tests/Component/Validator/Mapping/Loader/constraint-mapping.yml index ea1ba6fec9..cd4b9355e1 100644 --- a/tests/Symfony/Tests/Component/Validator/Mapping/Loader/constraint-mapping.yml +++ b/tests/Symfony/Tests/Component/Validator/Mapping/Loader/constraint-mapping.yml @@ -39,3 +39,6 @@ Symfony\Tests\Component\Validator\Fixtures\Entity: getters: lastName: - NotNull: ~ + +Symfony\Tests\Component\Validator\Fixtures\GroupSequenceProviderEntity: + group_sequence_provider: true diff --git a/tests/Symfony/Tests/Component/Validator/ValidatorTest.php b/tests/Symfony/Tests/Component/Validator/ValidatorTest.php index b07e596a10..19c3f7647d 100644 --- a/tests/Symfony/Tests/Component/Validator/ValidatorTest.php +++ b/tests/Symfony/Tests/Component/Validator/ValidatorTest.php @@ -17,6 +17,7 @@ require_once __DIR__.'/Fixtures/FailingConstraintValidator.php'; require_once __DIR__.'/Fixtures/FakeClassMetadataFactory.php'; use Symfony\Tests\Component\Validator\Fixtures\Entity; +use Symfony\Tests\Component\Validator\Fixtures\GroupSequenceProviderEntity; use Symfony\Tests\Component\Validator\Fixtures\FakeClassMetadataFactory; use Symfony\Tests\Component\Validator\Fixtures\FailingConstraint; use Symfony\Component\Validator\Validator; @@ -122,6 +123,50 @@ class ValidatorTest extends \PHPUnit_Framework_TestCase $this->assertEquals($violations, $result); } + public function testValidate_groupSequenceProvider() + { + $entity = new GroupSequenceProviderEntity(); + $metadata = new ClassMetadata(get_class($entity)); + $metadata->addPropertyConstraint('firstName', new FailingConstraint(array( + 'groups' => 'First', + ))); + $metadata->addPropertyConstraint('lastName', new FailingConstraint(array( + 'groups' => 'Second', + ))); + $metadata->setGroupSequenceProvider(true); + $this->factory->addClassMetadata($metadata); + + $violations = new ConstraintViolationList(); + $violations->add(new ConstraintViolation( + 'Failed', + array(), + $entity, + 'firstName', + '' + )); + + $entity->setGroups(array('First')); + $result = $this->validator->validate($entity); + $this->assertEquals($violations, $result); + + $violations = new ConstraintViolationList(); + $violations->add(new ConstraintViolation( + 'Failed', + array(), + $entity, + 'lastName', + '' + )); + + $entity->setGroups(array('Second')); + $result = $this->validator->validate($entity); + $this->assertEquals($violations, $result); + + $entity->setGroups(array()); + $result = $this->validator->validate($entity); + $this->assertEquals(new ConstraintViolationList(), $result); + } + public function testValidateProperty() { $entity = new Entity();