From 186c115894737c0934b830910b3ec222f94bb789 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 14:44:01 +0100 Subject: [PATCH] [Validator] Improved test coverage of NonRecursiveNodeTraverser --- .../UnsupportedMetadataException.php | 20 +++++ .../Validator/Mapping/ClassMetadata.php | 11 +++ .../Validator/Mapping/GenericMetadata.php | 78 ++++++++++++++++--- .../NonRecursiveNodeTraverser.php | 47 ++++++----- .../Tests/Fixtures/FakeClassMetadata.php | 29 +++++++ .../Tests/Fixtures/FakeMetadataFactory.php | 17 +++- .../Tests/Fixtures/LegacyClassMetadata.php | 20 +++++ .../Tests/Validator/Abstract2Dot5ApiTest.php | 55 +++++++++++++ 8 files changed, 244 insertions(+), 33 deletions(-) create mode 100644 src/Symfony/Component/Validator/Exception/UnsupportedMetadataException.php create mode 100644 src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php create mode 100644 src/Symfony/Component/Validator/Tests/Fixtures/LegacyClassMetadata.php diff --git a/src/Symfony/Component/Validator/Exception/UnsupportedMetadataException.php b/src/Symfony/Component/Validator/Exception/UnsupportedMetadataException.php new file mode 100644 index 0000000000..c6ece50b70 --- /dev/null +++ b/src/Symfony/Component/Validator/Exception/UnsupportedMetadataException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Exception; + +/** + * @since 2.5 + * @author Bernhard Schussek + */ +class UnsupportedMetadataException extends InvalidArgumentException +{ +} diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 5a2782f854..30c7b1ad3b 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -64,6 +64,15 @@ class ClassMetadata extends ElementMetadata implements LegacyMetadataInterface, */ public $groupSequenceProvider = false; + /** + * The strategy for traversing traversable objects. + * + * By default, only instances of {@link \Traversable} are traversed. + * + * @var integer + */ + public $traversalStrategy = TraversalStrategy::IMPLICIT; + /** * @var \ReflectionClass */ @@ -215,6 +224,8 @@ class ClassMetadata extends ElementMetadata implements LegacyMetadataInterface, $constraint->addImplicitGroupName($this->getDefaultGroup()); parent::addConstraint($constraint); + + return $this; } /** diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index 47eda213a4..f77e11736a 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -13,10 +13,13 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\Exception\BadMethodCallException; use Symfony\Component\Validator\ValidationVisitorInterface; /** - * @since %%NextVersion%% + * A generic container of {@link Constraint} objects. + * + * @since 2.5 * @author Bernhard Schussek */ class GenericMetadata implements MetadataInterface @@ -31,9 +34,27 @@ class GenericMetadata implements MetadataInterface */ public $constraintsByGroup = array(); + /** + * The strategy for cascading objects. + * + * By default, objects are not cascaded. + * + * @var integer + * + * @see CascadingStrategy + */ public $cascadingStrategy = CascadingStrategy::NONE; - public $traversalStrategy = TraversalStrategy::IMPLICIT; + /** + * The strategy for traversing traversable objects. + * + * By default, traversable objects are not traversed. + * + * @var integer + * + * @see TraversalStrategy + */ + public $traversalStrategy = TraversalStrategy::NONE; /** * Returns the names of the properties that should be serialized. @@ -66,11 +87,22 @@ class GenericMetadata implements MetadataInterface } /** - * Adds a constraint to this element. + * Adds a constraint. * - * @param Constraint $constraint + * If the constraint {@link Valid} is added, the cascading strategy will be + * changed to {@link CascadingStrategy::CASCADE}. Depending on the + * properties $traverse and $deep of that constraint, the traversal strategy + * will be set to one of the following: * - * @return ElementMetadata + * - {@link TraversalStrategy::IMPLICIT} if $traverse is enabled and $deep + * is enabled + * - {@link TraversalStrategy::IMPLICIT} | {@link TraversalStrategy::STOP_RECURSION} + * if $traverse is enabled, but $deep is disabled + * - {@link TraversalStrategy::NONE} if $traverse is disabled + * + * @param Constraint $constraint The constraint to add + * + * @return GenericMetadata This object */ public function addConstraint(Constraint $constraint) { @@ -100,17 +132,26 @@ class GenericMetadata implements MetadataInterface return $this; } + /** + * Adds an list of constraints. + * + * @param Constraint[] $constraints The constraints to add + * + * @return GenericMetadata This object + */ public function addConstraints(array $constraints) { foreach ($constraints as $constraint) { $this->addConstraint($constraint); } + + return $this; } /** * Returns all constraints of this element. * - * @return Constraint[] An array of Constraint instances + * @return Constraint[] A list of Constraint instances */ public function getConstraints() { @@ -132,30 +173,45 @@ class GenericMetadata implements MetadataInterface * * @param string $group The group name * - * @return array An array with all Constraint instances belonging to the group + * @return Constraint[] An list of all the Constraint instances belonging + * to the group */ public function findConstraints($group) { return isset($this->constraintsByGroup[$group]) - ? $this->constraintsByGroup[$group] - : array(); + ? $this->constraintsByGroup[$group] + : array(); } + /** + * {@inheritdoc} + */ public function getCascadingStrategy() { return $this->cascadingStrategy; } + /** + * {@inheritdoc} + */ public function getTraversalStrategy() { return $this->traversalStrategy; } /** - * {@inheritdoc} + * Exists for compatibility with the deprecated + * {@link Symfony\Component\Validator\MetadataInterface}. + * + * Should not be used. + * + * @throws BadMethodCallException + * + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * Will be removed in Symfony 3.0. */ public function accept(ValidationVisitorInterface $visitor, $value, $group, $propertyPath) { - // Thanks PHP < 5.3.9 + throw new BadMethodCallException('Not supported.'); } } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php index ca1e2a9db5..b931b96943 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php @@ -13,8 +13,10 @@ namespace Symfony\Component\Validator\NodeTraverser; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Exception\NoSuchMetadataException; +use Symfony\Component\Validator\Exception\UnsupportedMetadataException; use Symfony\Component\Validator\Mapping\CascadingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; +use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; @@ -191,6 +193,9 @@ class NonRecursiveNodeTraverser implements NodeTraverserInterface * @param \SplStack $nodeStack The stack for storing the * successor nodes * + * @throws UnsupportedMetadataException If a property metadata does not + * implement {@link PropertyMetadataInterface} + * * @see ClassNode * @see PropertyNode * @see CollectionNode @@ -215,6 +220,15 @@ class NonRecursiveNodeTraverser implements NodeTraverserInterface foreach ($node->metadata->getConstrainedProperties() as $propertyName) { foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { + if (!$propertyMetadata instanceof PropertyMetadataInterface) { + throw new UnsupportedMetadataException(sprintf( + 'The property metadata instances should implement '. + '"Symfony\Component\Validator\Mapping\PropertyMetadataInterface", '. + 'got: "%s".', + is_object($propertyMetadata) ? get_class($propertyMetadata) : gettype($propertyMetadata) + )); + } + $nodeStack->push(new PropertyNode( $node->value, $propertyMetadata->getPropertyValue($node->value), @@ -424,24 +438,12 @@ class NonRecursiveNodeTraverser implements NodeTraverserInterface return; } - // Traverse only if IMPLICIT or TRAVERSE - if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { - return; - } + // Currently, the traversal strategy can only be TRAVERSE for a + // generic node if the cascading strategy is CASCADE. Thus, traversable + // objects will always be handled within cascadeObject() and there's + // nothing more to do here. - // If IMPLICIT, stop unless we deal with a Traversable - if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) { - return; - } - - // If TRAVERSE, the constructor will fail if we have no Traversable - $nodeStack->push(new CollectionNode( - $node->value, - $node->propertyPath, - $cascadedGroups, - null, - $traversalStrategy - )); + // see GenericMetadata::addConstraint() } /** @@ -464,7 +466,9 @@ class NonRecursiveNodeTraverser implements NodeTraverserInterface * and does not implement {@link \Traversable} * or if traversal is disabled via the * $traversalStrategy argument - * + * @throws UnsupportedMetadataException If the metadata returned by the + * metadata factory does not implement + * {@link ClassMetadataInterface} */ private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, \SplStack $nodeStack) { @@ -472,7 +476,12 @@ class NonRecursiveNodeTraverser implements NodeTraverserInterface $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { - // error + throw new UnsupportedMetadataException(sprintf( + 'The metadata factory should return instances of '. + '"Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) + )); } $nodeStack->push(new ClassNode( diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php b/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php new file mode 100644 index 0000000000..c6b79f66f3 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php @@ -0,0 +1,29 @@ + + * + * 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\ClassBasedInterface; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\MetadataInterface; +use Symfony\Component\Validator\PropertyMetadataContainerInterface; + +class FakeClassMetadata extends ClassMetadata +{ + public function addPropertyMetadata($propertyName, $metadata) + { + if (!isset($this->members[$propertyName])) { + $this->members[$propertyName] = array(); + } + + $this->members[$propertyName][] = $metadata; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php index b03eacf71e..09b0ca63be 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php @@ -22,7 +22,10 @@ class FakeMetadataFactory implements MetadataFactoryInterface public function getMetadataFor($class) { + $hash = null; + if (is_object($class)) { + $hash = spl_object_hash($class); $class = get_class($class); } @@ -31,6 +34,10 @@ class FakeMetadataFactory implements MetadataFactoryInterface } if (!isset($this->metadatas[$class])) { + if (isset($this->metadatas[$hash])) { + return $this->metadatas[$hash]; + } + throw new NoSuchMetadataException(sprintf('No metadata for "%s"', $class)); } @@ -39,24 +46,28 @@ class FakeMetadataFactory implements MetadataFactoryInterface public function hasMetadataFor($class) { + $hash = null; + if (is_object($class)) { $class = get_class($class); + $hash = spl_object_hash($hash); } if (!is_string($class)) { return false; } - return isset($this->metadatas[$class]); + return isset($this->metadatas[$class]) || isset($this->metadatas[$hash]); } - public function addMetadata(ClassMetadata $metadata) + public function addMetadata($metadata) { $this->metadatas[$metadata->getClassName()] = $metadata; } public function addMetadataForValue($value, MetadataInterface $metadata) { - $this->metadatas[$value] = $metadata; + $key = is_object($value) ? spl_object_hash($value) : $value; + $this->metadatas[$key] = $metadata; } } diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/LegacyClassMetadata.php b/src/Symfony/Component/Validator/Tests/Fixtures/LegacyClassMetadata.php new file mode 100644 index 0000000000..6a832a109f --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/LegacyClassMetadata.php @@ -0,0 +1,20 @@ + + * + * 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\ClassBasedInterface; +use Symfony\Component\Validator\MetadataInterface; +use Symfony\Component\Validator\PropertyMetadataContainerInterface; + +interface LegacyClassMetadata extends MetadataInterface, PropertyMetadataContainerInterface, ClassBasedInterface +{ +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 973283c84e..63b8c6a551 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -20,6 +20,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Tests\Fixtures\Entity; +use Symfony\Component\Validator\Tests\Fixtures\FakeClassMetadata; use Symfony\Component\Validator\Tests\Fixtures\Reference; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -386,4 +387,58 @@ abstract class Abstract2Dot5ApiTest extends AbstractValidatorTest $this->assertSame(2, $violations[0]->getMessagePluralization()); $this->assertSame('Code', $violations[0]->getCode()); } + + /** + * @expectedException \Symfony\Component\Validator\Exception\UnsupportedMetadataException + */ + public function testMetadataMustImplementClassMetadataInterface() + { + $entity = new Entity(); + + $metadata = $this->getMock('Symfony\Component\Validator\Tests\Fixtures\LegacyClassMetadata'); + $metadata->expects($this->any()) + ->method('getClassName') + ->will($this->returnValue(get_class($entity))); + + $this->metadataFactory->addMetadata($metadata); + + $this->validator->validate($entity); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\UnsupportedMetadataException + */ + public function testReferenceMetadataMustImplementClassMetadataInterface() + { + $entity = new Entity(); + $entity->reference = new Reference(); + + $metadata = $this->getMock('Symfony\Component\Validator\Tests\Fixtures\LegacyClassMetadata'); + $metadata->expects($this->any()) + ->method('getClassName') + ->will($this->returnValue(get_class($entity->reference))); + + $this->metadataFactory->addMetadata($metadata); + + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $this->validator->validate($entity); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\UnsupportedMetadataException + */ + public function testPropertyMetadataMustImplementPropertyMetadataInterface() + { + $entity = new Entity(); + + // Legacy interface + $propertyMetadata = $this->getMock('Symfony\Component\Validator\MetadataInterface'); + $metadata = new FakeClassMetadata(get_class($entity)); + $metadata->addPropertyMetadata('firstName', $propertyMetadata); + + $this->metadataFactory->addMetadata($metadata); + + $this->validator->validate($entity); + } }