[Validator] Improved test coverage of NonRecursiveNodeTraverser

This commit is contained in:
Bernhard Schussek 2014-02-21 14:44:01 +01:00
parent 822fe4712f
commit 186c115894
8 changed files with 244 additions and 33 deletions

View File

@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bschussek@gmail.com>
*/
class UnsupportedMetadataException extends InvalidArgumentException
{
}

View File

@ -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;
}
/**

View File

@ -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 <bschussek@gmail.com>
*/
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.');
}
}

View File

@ -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(

View File

@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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
{
}

View File

@ -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);
}
}