[Validator] Added a recursive clone of the new implementation for speed comparison

This commit is contained in:
Bernhard Schussek 2014-02-22 11:43:44 +01:00
parent f61d31e5fa
commit 23534ca6ab
21 changed files with 1004 additions and 225 deletions

View File

@ -17,6 +17,7 @@ use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Exception\BadMethodCallException;
use Symfony\Component\Validator\Group\GroupManagerInterface;
use Symfony\Component\Validator\Mapping\MetadataInterface;
use Symfony\Component\Validator\Mapping\PropertyMetadataInterface;
use Symfony\Component\Validator\Node\Node;
use Symfony\Component\Validator\Util\PropertyPath;
@ -48,11 +49,6 @@ class ExecutionContext implements ExecutionContextInterface
*/
private $root;
/**
* @var GroupManagerInterface
*/
private $groupManager;
/**
* @var TranslatorInterface
*/
@ -71,11 +67,32 @@ class ExecutionContext implements ExecutionContextInterface
private $violations;
/**
* The current node under validation.
* The currently validated value.
*
* @var Node
* @var mixed
*/
private $node;
private $value;
/**
* The property path leading to the current value.
*
* @var string
*/
private $propertyPath = '';
/**
* The current validation metadata.
*
* @var MetadataInterface
*/
private $metadata;
/**
* The currently validated group.
*
* @var string|null
*/
private $group;
/**
* Stores which objects have been validated in which group.
@ -104,9 +121,6 @@ class ExecutionContext implements ExecutionContextInterface
* @param ValidatorInterface $validator The validator
* @param mixed $root The root value of the
* validated object graph
* @param GroupManagerInterface $groupManager The manager for accessing
* the currently validated
* group
* @param TranslatorInterface $translator The translator
* @param string|null $translationDomain The translation domain to
* use for translating
@ -115,24 +129,45 @@ class ExecutionContext implements ExecutionContextInterface
* @internal Called by {@link ExecutionContextFactory}. Should not be used
* in user code.
*/
public function __construct(ValidatorInterface $validator, $root, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null)
public function __construct(ValidatorInterface $validator, $root, TranslatorInterface $translator, $translationDomain = null)
{
$this->validator = $validator;
$this->root = $root;
$this->groupManager = $groupManager;
$this->translator = $translator;
$this->translationDomain = $translationDomain;
$this->violations = new ConstraintViolationList();
}
/**
* Sets the values of the context to match the given node.
*
* @param Node $node The currently validated node
* {@inheritdoc}
*/
public function setCurrentNode(Node $node)
public function setValue($value)
{
$this->node = $node;
$this->value = $value;
}
/**
* {@inheritdoc}
*/
public function setMetadata(MetadataInterface $metadata = null)
{
$this->metadata = $metadata;
}
/**
* {@inheritdoc}
*/
public function setPropertyPath($propertyPath)
{
$this->propertyPath = (string) $propertyPath;
}
/**
* {@inheritdoc}
*/
public function setGroup($group)
{
$this->group = $group;
}
/**
@ -209,7 +244,7 @@ class ExecutionContext implements ExecutionContextInterface
*/
public function getValue()
{
return $this->node ? $this->node->value : null;
return $this->value;
}
/**
@ -217,7 +252,7 @@ class ExecutionContext implements ExecutionContextInterface
*/
public function getMetadata()
{
return $this->node ? $this->node->metadata : null;
return $this->metadata;
}
/**
@ -225,7 +260,7 @@ class ExecutionContext implements ExecutionContextInterface
*/
public function getGroup()
{
return $this->groupManager->getCurrentGroup();
return $this->group;
}
/**
@ -233,9 +268,7 @@ class ExecutionContext implements ExecutionContextInterface
*/
public function getClassName()
{
$metadata = $this->getMetadata();
return $metadata instanceof ClassBasedInterface ? $metadata->getClassName() : null;
return $this->metadata instanceof ClassBasedInterface ? $this->metadata->getClassName() : null;
}
/**
@ -243,9 +276,7 @@ class ExecutionContext implements ExecutionContextInterface
*/
public function getPropertyName()
{
$metadata = $this->getMetadata();
return $metadata instanceof PropertyMetadataInterface ? $metadata->getPropertyName() : null;
return $this->metadata instanceof PropertyMetadataInterface ? $this->metadata->getPropertyName() : null;
}
/**
@ -253,9 +284,7 @@ class ExecutionContext implements ExecutionContextInterface
*/
public function getPropertyPath($subPath = '')
{
$propertyPath = $this->node ? $this->node->propertyPath : '';
return PropertyPath::append($propertyPath, $subPath);
return PropertyPath::append($this->propertyPath, $subPath);
}
/**

View File

@ -26,11 +26,6 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
*/
class ExecutionContextFactory implements ExecutionContextFactoryInterface
{
/**
* @var GroupManagerInterface
*/
private $groupManager;
/**
* @var TranslatorInterface
*/
@ -44,17 +39,13 @@ class ExecutionContextFactory implements ExecutionContextFactoryInterface
/**
* Creates a new context factory.
*
* @param GroupManagerInterface $groupManager The manager for accessing
* the currently validated
* group
* @param TranslatorInterface $translator The translator
* @param string|null $translationDomain The translation domain to
* use for translating
* violation messages
*/
public function __construct(GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null)
public function __construct(TranslatorInterface $translator, $translationDomain = null)
{
$this->groupManager = $groupManager;
$this->translator = $translator;
$this->translationDomain = $translationDomain;
}
@ -67,7 +58,6 @@ class ExecutionContextFactory implements ExecutionContextFactoryInterface
return new ExecutionContext(
$validator,
$root,
$this->groupManager,
$this->translator,
$this->translationDomain
);

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Validator\Context;
use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface;
use Symfony\Component\Validator\Mapping\MetadataInterface;
use Symfony\Component\Validator\Node\Node;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface;
@ -100,14 +101,44 @@ interface ExecutionContextInterface extends LegacyExecutionContextInterface
public function getValidator();
/**
* Sets the currently traversed node.
* Sets the currently validated value.
*
* @param Node $node The current node
* @param mixed $value The validated value
*
* @internal Used by the validator engine. Should not be called by user
* code.
*/
public function setCurrentNode(Node $node);
public function setValue($value);
/**
* Sets the current validation metadata.
*
* @param MetadataInterface $metadata The validation metadata
*
* @internal Used by the validator engine. Should not be called by user
* code.
*/
public function setMetadata(MetadataInterface $metadata = null);
/**
* Sets the property path leading to the current value.
*
* @param string $propertyPath The property path to the current value
*
* @internal Used by the validator engine. Should not be called by user
* code.
*/
public function setPropertyPath($propertyPath);
/**
* Sets the currently validated group.
*
* @param string|null $group The validated group
*
* @internal Used by the validator engine. Should not be called by user
* code.
*/
public function setGroup($group);
/**
* Marks an object as validated in a specific validation group.

View File

@ -42,7 +42,7 @@ class LegacyExecutionContext extends ExecutionContext
* @internal Called by {@link LegacyExecutionContextFactory}. Should not be used
* in user code.
*/
public function __construct(ValidatorInterface $validator, $root, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null)
public function __construct(ValidatorInterface $validator, $root, TranslatorInterface $translator, $translationDomain = null)
{
if (!$validator instanceof LegacyValidatorInterface) {
throw new InvalidArgumentException(
@ -54,7 +54,6 @@ class LegacyExecutionContext extends ExecutionContext
parent::__construct(
$validator,
$root,
$groupManager,
$translator,
$translationDomain
);

View File

@ -26,11 +26,6 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
*/
class LegacyExecutionContextFactory implements ExecutionContextFactoryInterface
{
/**
* @var GroupManagerInterface
*/
private $groupManager;
/**
* @var TranslatorInterface
*/
@ -44,17 +39,13 @@ class LegacyExecutionContextFactory implements ExecutionContextFactoryInterface
/**
* Creates a new context factory.
*
* @param GroupManagerInterface $groupManager The manager for accessing
* the currently validated
* group
* @param TranslatorInterface $translator The translator
* @param string|null $translationDomain The translation domain to
* use for translating
* violation messages
*/
public function __construct(GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null)
public function __construct(TranslatorInterface $translator, $translationDomain = null)
{
$this->groupManager = $groupManager;
$this->translator = $translator;
$this->translationDomain = $translationDomain;
}
@ -67,7 +58,6 @@ class LegacyExecutionContextFactory implements ExecutionContextFactoryInterface
return new LegacyExecutionContext(
$validator,
$root,
$this->groupManager,
$this->translator,
$this->translationDomain
);

View File

@ -1,29 +0,0 @@
<?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\Group;
/**
* Returns the group that is currently being validated.
*
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface GroupManagerInterface
{
/**
* Returns the group that is currently being validated.
*
* @return string|null The current group or null, if no validation is
* active.
*/
public function getCurrentGroup();
}

View File

@ -30,6 +30,8 @@ class ContextUpdateVisitor extends AbstractVisitor
*/
public function visit(Node $node, ExecutionContextInterface $context)
{
$context->setCurrentNode($node);
$context->setValue($node->value);
$context->setMetadata($node->metadata);
$context->setPropertyPath($node->propertyPath);
}
}

View File

@ -14,7 +14,6 @@ namespace Symfony\Component\Validator\NodeVisitor;
use Symfony\Component\Validator\Constraints\GroupSequence;
use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Group\GroupManagerInterface;
use Symfony\Component\Validator\Node\ClassNode;
use Symfony\Component\Validator\Node\CollectionNode;
use Symfony\Component\Validator\Node\Node;
@ -27,7 +26,7 @@ use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface;
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInterface
class NodeValidationVisitor extends AbstractVisitor
{
/**
* @var ConstraintValidatorFactoryInterface
@ -39,13 +38,6 @@ class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInter
*/
private $nodeTraverser;
/**
* The currently validated group.
*
* @var string
*/
private $currentGroup;
/**
* Creates a new visitor.
*
@ -128,14 +120,6 @@ class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInter
return true;
}
/**
* {@inheritdoc}
*/
public function getCurrentGroup()
{
return $this->currentGroup;
}
/**
* Validates a node's value in each group of a group sequence.
*
@ -181,7 +165,7 @@ class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInter
private function validateNodeForGroup(Node $node, $group, ExecutionContextInterface $context, $objectHash)
{
try {
$this->currentGroup = $group;
$context->setGroup($group);
foreach ($node->metadata->findConstraints($group) as $constraint) {
// Prevent duplicate validation of constraints, in the case
@ -211,10 +195,10 @@ class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInter
$validator->validate($node->value, $constraint);
}
$this->currentGroup = null;
$context->setGroup(null);
} catch (\Exception $e) {
// Should be put into a finally block once we switch to PHP 5.5
$this->currentGroup = null;
$context->setGroup(null);
throw $e;
}

View File

@ -1,69 +0,0 @@
<?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\Context;
use Symfony\Component\Validator\Context\ExecutionContext;
/**
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ExecutionContextTest extends \PHPUnit_Framework_TestCase
{
const ROOT = '__ROOT__';
const TRANSLATION_DOMAIN = '__TRANSLATION_DOMAIN__';
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $validator;
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $groupManager;
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $translator;
/**
* @var ExecutionContext
*/
private $context;
protected function setUp()
{
if (version_compare(PHP_VERSION, '5.3.9', '<')) {
$this->markTestSkipped('Not supported prior to PHP 5.3.9');
}
$this->validator = $this->getMock('Symfony\Component\Validator\Validator\ValidatorInterface');
$this->groupManager = $this->getMock('Symfony\Component\Validator\Group\GroupManagerInterface');
$this->translator = $this->getMock('Symfony\Component\Translation\TranslatorInterface');
$this->context = new ExecutionContext(
$this->validator, self::ROOT, $this->groupManager, $this->translator, self::TRANSLATION_DOMAIN
);
}
public function testGetGroup()
{
$this->groupManager->expects($this->once())
->method('getCurrentGroup')
->will($this->returnValue('Current Group'));
$this->assertSame('Current Group', $this->context->getGroup());
}
}

View File

@ -34,17 +34,8 @@ class LegacyValidator2Dot5ApiTest extends Abstract2Dot5ApiTest
protected function createValidator(MetadataFactoryInterface $metadataFactory)
{
$nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory);
$nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory());
$contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator());
$validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory);
$groupSequenceResolver = new DefaultGroupReplacingVisitor();
$contextRefresher = new ContextUpdateVisitor();
$contextFactory = new LegacyExecutionContextFactory(new DefaultTranslator());
$nodeTraverser->addVisitor($groupSequenceResolver);
$nodeTraverser->addVisitor($contextRefresher);
$nodeTraverser->addVisitor($nodeValidator);
return $validator;
return new LegacyValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory());
}
}

View File

@ -34,17 +34,8 @@ class LegacyValidatorLegacyApiTest extends AbstractLegacyApiTest
protected function createValidator(MetadataFactoryInterface $metadataFactory)
{
$nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory);
$nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory());
$contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator());
$validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory);
$groupSequenceResolver = new DefaultGroupReplacingVisitor();
$contextRefresher = new ContextUpdateVisitor();
$contextFactory = new LegacyExecutionContextFactory(new DefaultTranslator());
$nodeTraverser->addVisitor($groupSequenceResolver);
$nodeTraverser->addVisitor($contextRefresher);
$nodeTraverser->addVisitor($nodeValidator);
return $validator;
return new LegacyValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory());
}
}

View File

@ -0,0 +1,33 @@
<?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\Validator;
use Symfony\Component\Validator\DefaultTranslator;
use Symfony\Component\Validator\ConstraintValidatorFactory;
use Symfony\Component\Validator\Context\ExecutionContextFactory;
use Symfony\Component\Validator\MetadataFactoryInterface;
use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor;
use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor;
use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor;
use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser;
use Symfony\Component\Validator\Validator\RecursiveValidator;
use Symfony\Component\Validator\Validator\TraversingValidator;
class RecursiveValidator2Dot5ApiTest extends Abstract2Dot5ApiTest
{
protected function createValidator(MetadataFactoryInterface $metadataFactory)
{
$contextFactory = new ExecutionContextFactory(new DefaultTranslator());
return new RecursiveValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory());
}
}

View File

@ -19,18 +19,19 @@ use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor;
use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor;
use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor;
use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser;
use Symfony\Component\Validator\Validator\Validator;
use Symfony\Component\Validator\Validator\TraversingValidator;
class Validator2Dot5ApiTest extends Abstract2Dot5ApiTest
class TraversingValidator2Dot5ApiTest extends Abstract2Dot5ApiTest
{
protected function createValidator(MetadataFactoryInterface $metadataFactory)
{
$nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory);
$nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory());
$contextFactory = new ExecutionContextFactory($nodeValidator, new DefaultTranslator());
$validator = new Validator($contextFactory, $nodeTraverser, $metadataFactory);
$contextFactory = new ExecutionContextFactory(new DefaultTranslator());
$validator = new TraversingValidator($contextFactory, $nodeTraverser, $metadataFactory);
$groupSequenceResolver = new DefaultGroupReplacingVisitor();
$contextRefresher = new ContextUpdateVisitor();
$nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory());
$nodeTraverser->addVisitor($groupSequenceResolver);
$nodeTraverser->addVisitor($contextRefresher);

View File

@ -130,7 +130,7 @@ class ValidatorBuilderTest extends \PHPUnit_Framework_TestCase
public function testSetApiVersion25()
{
$this->assertSame($this->builder, $this->builder->setApiVersion(Validation::API_VERSION_2_5));
$this->assertInstanceOf('Symfony\Component\Validator\Validator\Validator', $this->builder->getValidator());
$this->assertInstanceOf('Symfony\Component\Validator\Validator\TraversingValidator', $this->builder->getValidator());
}
public function testSetApiVersion24And25()

View File

@ -22,7 +22,7 @@ use Symfony\Component\Translation\TranslatorInterface;
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0.
* Use {@link Validator\Validator} instead.
* Use {@link Validator\TraversingValidator} instead.
*/
class Validator implements ValidatorInterface
{

View File

@ -28,7 +28,7 @@ use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface;
* @deprecated Implemented for backwards compatibility with Symfony < 2.5.
* To be removed in Symfony 3.0.
*/
class LegacyValidator extends Validator implements LegacyValidatorInterface
class LegacyValidator extends RecursiveValidator implements LegacyValidatorInterface
{
public function validate($value, $groups = null, $traverse = false, $deep = false)
{

View File

@ -0,0 +1,700 @@
<?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\Validator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\GroupSequence;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\NoSuchMetadataException;
use Symfony\Component\Validator\Exception\UnsupportedMetadataException;
use Symfony\Component\Validator\Exception\ValidatorException;
use Symfony\Component\Validator\Mapping\CascadingStrategy;
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
use Symfony\Component\Validator\Mapping\GenericMetadata;
use Symfony\Component\Validator\Mapping\PropertyMetadataInterface;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
use Symfony\Component\Validator\MetadataFactoryInterface;
use Symfony\Component\Validator\Node\ClassNode;
use Symfony\Component\Validator\Node\CollectionNode;
use Symfony\Component\Validator\Node\GenericNode;
use Symfony\Component\Validator\Node\Node;
use Symfony\Component\Validator\Node\PropertyNode;
use Symfony\Component\Validator\Util\PropertyPath;
/**
* Default implementation of {@link ContextualValidatorInterface}.
*
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class RecursiveContextualValidator implements ContextualValidatorInterface
{
/**
* @var ExecutionContextInterface
*/
private $context;
/**
* @var MetadataFactoryInterface
*/
private $metadataFactory;
private $validatorFactory;
private $currentGroup;
/**
* Creates a validator for the given context.
*
* @param ExecutionContextInterface $context The execution context
* @param MetadataFactoryInterface $metadataFactory The factory for fetching
* the metadata of validated
* objects
*/
public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory)
{
$this->context = $context;
$this->defaultPropertyPath = $context->getPropertyPath();
$this->defaultGroups = array($context->getGroup() ?: Constraint::DEFAULT_GROUP);
$this->metadataFactory = $metadataFactory;
$this->validatorFactory = $validatorFactory;
}
/**
* {@inheritdoc}
*/
public function atPath($path)
{
$this->defaultPropertyPath = $this->context->getPropertyPath($path);
return $this;
}
/**
* {@inheritdoc}
*/
public function validate($value, $constraints = null, $groups = null)
{
if (null === $constraints) {
$constraints = array(new Valid());
} elseif (!is_array($constraints)) {
$constraints = array($constraints);
}
$metadata = new GenericMetadata();
$metadata->addConstraints($constraints);
$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
$this->traverseGenericNode(new GenericNode(
$value,
$metadata,
$this->defaultPropertyPath,
$groups
), $this->context);
return $this;
}
/**
* {@inheritdoc}
*/
public function validateProperty($object, $propertyName, $groups = null)
{
$classMetadata = $this->metadataFactory->getMetadataFor($object);
if (!$classMetadata instanceof ClassMetadataInterface) {
throw new ValidatorException(sprintf(
'The metadata factory should return instances of '.
'"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '.
'got: "%s".',
is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata)
));
}
$propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
foreach ($propertyMetadatas as $propertyMetadata) {
$propertyValue = $propertyMetadata->getPropertyValue($object);
$this->traverseGenericNode(new PropertyNode(
$object,
$propertyValue,
$propertyMetadata,
PropertyPath::append($this->defaultPropertyPath, $propertyName),
$groups
), $this->context);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function validatePropertyValue($object, $propertyName, $value, $groups = null)
{
$classMetadata = $this->metadataFactory->getMetadataFor($object);
if (!$classMetadata instanceof ClassMetadataInterface) {
throw new ValidatorException(sprintf(
'The metadata factory should return instances of '.
'"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '.
'got: "%s".',
is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata)
));
}
$propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
foreach ($propertyMetadatas as $propertyMetadata) {
$this->traverseGenericNode(new PropertyNode(
$object,
$value,
$propertyMetadata,
PropertyPath::append($this->defaultPropertyPath, $propertyName),
$groups,
$groups
), $this->context);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function getViolations()
{
return $this->context->getViolations();
}
/**
* Normalizes the given group or list of groups to an array.
*
* @param mixed $groups The groups to normalize
*
* @return array A group array
*/
protected function normalizeGroups($groups)
{
if (is_array($groups)) {
return $groups;
}
return array($groups);
}
/**
* Traverses a class node.
*
* At first, each visitor is invoked for this node. Then, unless any
* of the visitors aborts the traversal by returning false, a property
* node is put on the node stack for each constrained property of the class.
* At last, if the class is traversable and should be traversed according
* to the selected traversal strategy, a new collection node is put on the
* stack.
*
* @param ClassNode $node The class node
* @param ExecutionContextInterface $context The current execution context
*
* @throws UnsupportedMetadataException If a property metadata does not
* implement {@link PropertyMetadataInterface}
*
* @see ClassNode
* @see PropertyNode
* @see CollectionNode
* @see TraversalStrategy
*/
private function traverseClassNode(ClassNode $node, ExecutionContextInterface $context)
{
if (false === $this->validateNode($node, $context)) {
return;
}
if (0 === count($node->groups)) {
return;
}
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)
));
}
$this->traverseGenericNode(new PropertyNode(
$node->value,
$propertyMetadata->getPropertyValue($node->value),
$propertyMetadata,
$node->propertyPath
? $node->propertyPath.'.'.$propertyName
: $propertyName,
$node->groups,
$node->cascadedGroups
), $context);
}
}
$traversalStrategy = $node->traversalStrategy;
// If no specific traversal strategy was requested when this method
// was called, use the traversal strategy of the class' metadata
if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
// Keep the STOP_RECURSION flag, if it was set
$traversalStrategy = $node->metadata->getTraversalStrategy()
| ($traversalStrategy & TraversalStrategy::STOP_RECURSION);
}
// Traverse only if IMPLICIT or TRAVERSE
if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
return;
}
// 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
$this->traverseCollectionNode(new CollectionNode(
$node->value,
$node->propertyPath,
$node->groups,
$node->cascadedGroups,
$traversalStrategy
), $context);
}
/**
* Traverses a collection node.
*
* At first, each visitor is invoked for this node. Then, unless any
* of the visitors aborts the traversal by returning false, the successor
* nodes of the collection node are put on the stack:
*
* - for each object in the collection with associated class metadata, a
* new class node is put on the stack;
* - if an object has no associated class metadata, but is traversable, and
* unless the {@link TraversalStrategy::STOP_RECURSION} flag is set for
* collection node, a new collection node is put on the stack for that
* object;
* - for each array in the collection, a new collection node is put on the
* stack.
*
* @param CollectionNode $node The collection node
* @param ExecutionContextInterface $context The current execution context
*
* @see ClassNode
* @see CollectionNode
*/
private function traverseCollectionNode(CollectionNode $node, ExecutionContextInterface $context)
{
$traversalStrategy = $node->traversalStrategy;
if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) {
$traversalStrategy = TraversalStrategy::NONE;
} else {
$traversalStrategy = TraversalStrategy::IMPLICIT;
}
foreach ($node->value as $key => $value) {
if (is_array($value)) {
// Arrays are always cascaded, independent of the specified
// traversal strategy
// (BC with Symfony < 2.5)
$this->traverseCollectionNode(new CollectionNode(
$value,
$node->propertyPath.'['.$key.']',
$node->groups,
null,
$traversalStrategy
), $context);
continue;
}
// Scalar and null values in the collection are ignored
// (BC with Symfony < 2.5)
if (is_object($value)) {
$this->cascadeObject(
$value,
$node->propertyPath.'['.$key.']',
$node->groups,
$traversalStrategy,
$context
);
}
}
}
/**
* Traverses a node that is neither a class nor a collection node.
*
* At first, each visitor is invoked for this node. Then, unless any
* of the visitors aborts the traversal by returning false, the successor
* nodes of the collection node are put on the stack:
*
* - if the node contains an object with associated class metadata, a new
* class node is put on the stack;
* - if the node contains a traversable object without associated class
* metadata and traversal is enabled according to the selected traversal
* strategy, a collection node is put on the stack;
* - if the node contains an array, a collection node is put on the stack.
*
* @param Node $node The node
* @param ExecutionContextInterface $context The current execution context
*/
private function traverseGenericNode(Node $node, ExecutionContextInterface $context)
{
if (false === $this->validateNode($node, $context)) {
return;
}
if (null === $node->value) {
return;
}
// The "cascadedGroups" property is set by the NodeValidationVisitor when
// traversing group sequences
$cascadedGroups = null !== $node->cascadedGroups
? $node->cascadedGroups
: $node->groups;
if (0 === count($cascadedGroups)) {
return;
}
$cascadingStrategy = $node->metadata->getCascadingStrategy();
$traversalStrategy = $node->traversalStrategy;
// If no specific traversal strategy was requested when this method
// was called, use the traversal strategy of the node's metadata
if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
// Keep the STOP_RECURSION flag, if it was set
$traversalStrategy = $node->metadata->getTraversalStrategy()
| ($traversalStrategy & TraversalStrategy::STOP_RECURSION);
}
if (is_array($node->value)) {
// Arrays are always traversed, independent of the specified
// traversal strategy
// (BC with Symfony < 2.5)
$this->traverseCollectionNode(new CollectionNode(
$node->value,
$node->propertyPath,
$cascadedGroups,
null,
$traversalStrategy
), $context);
return;
}
if ($cascadingStrategy & CascadingStrategy::CASCADE) {
// If the value is a scalar, pass it anyway, because we want
// a NoSuchMetadataException to be thrown in that case
// (BC with Symfony < 2.5)
$this->cascadeObject(
$node->value,
$node->propertyPath,
$cascadedGroups,
$traversalStrategy,
$context
);
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.
// see GenericMetadata::addConstraint()
}
/**
* Executes the cascading logic for an object.
*
* If class metadata is available for the object, a class node is put on
* the node stack. Otherwise, if the selected traversal strategy allows
* traversal of the object, a new collection node is put on the stack.
* Otherwise, an exception is thrown.
*
* @param object $object The object to cascade
* @param string $propertyPath The current property path
* @param string[] $groups The validated groups
* @param integer $traversalStrategy The strategy for traversing the
* cascaded object
* @param ExecutionContextInterface $context The current execution context
*
* @throws NoSuchMetadataException If the object has no associated metadata
* 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, ExecutionContextInterface $context)
{
try {
$classMetadata = $this->metadataFactory->getMetadataFor($object);
if (!$classMetadata instanceof ClassMetadataInterface) {
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)
));
}
$this->traverseClassNode(new ClassNode(
$object,
$classMetadata,
$propertyPath,
$groups,
null,
$traversalStrategy
), $context);
} catch (NoSuchMetadataException $e) {
// Rethrow if not Traversable
if (!$object instanceof \Traversable) {
throw $e;
}
// Rethrow unless IMPLICIT or TRAVERSE
if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
throw $e;
}
$this->traverseCollectionNode(new CollectionNode(
$object,
$propertyPath,
$groups,
null,
$traversalStrategy
), $context);
}
}
/**
* Validates a node's value against the constraints defined in the node's
* metadata.
*
* Objects and constraints that were validated before in the same context
* will be skipped.
*
* @param Node $node The current node
* @param ExecutionContextInterface $context The execution context
*
* @return Boolean Whether to traverse the successor nodes
*/
public function validateNode(Node $node, ExecutionContextInterface $context)
{
if ($node instanceof CollectionNode) {
return true;
}
$context->setValue($node->value);
$context->setMetadata($node->metadata);
$context->setPropertyPath($node->propertyPath);
if ($node instanceof ClassNode) {
$groupSequence = null;
if ($node->metadata->hasGroupSequence()) {
// The group sequence is statically defined for the class
$groupSequence = $node->metadata->getGroupSequence();
} elseif ($node->metadata->isGroupSequenceProvider()) {
// The group sequence is dynamically obtained from the validated
// object
/** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */
$groupSequence = $node->value->getGroupSequence();
if (!$groupSequence instanceof GroupSequence) {
$groupSequence = new GroupSequence($groupSequence);
}
}
if (null !== $groupSequence) {
$key = array_search(Constraint::DEFAULT_GROUP, $node->groups);
if (false !== $key) {
// Replace the "Default" group by the group sequence
$node->groups[$key] = $groupSequence;
// Cascade the "Default" group when validating the sequence
$groupSequence->cascadedGroup = Constraint::DEFAULT_GROUP;
}
}
}
if ($node instanceof ClassNode) {
$objectHash = spl_object_hash($node->value);
} elseif ($node instanceof PropertyNode) {
$objectHash = spl_object_hash($node->object);
} else {
$objectHash = null;
}
// if group (=[<G1,G2>,G3,G4]) contains group sequence (=<G1,G2>)
// then call traverse() with each entry of the group sequence and abort
// if necessary (G1, G2)
// finally call traverse() with remaining entries ([G3,G4]) or
// simply continue traversal (if possible)
foreach ($node->groups as $key => $group) {
// Even if we remove the following clause, the constraints on an
// object won't be validated again due to the measures taken in
// validateNodeForGroup().
// The following shortcut, however, prevents validatedNodeForGroup()
// from being called at all and enhances performance a bit.
if ($node instanceof ClassNode) {
// Use the object hash for group sequences
$groupHash = is_object($group) ? spl_object_hash($group) : $group;
if ($context->isObjectValidatedForGroup($objectHash, $groupHash)) {
// Skip this group when validating the successor nodes
// (property and/or collection nodes)
unset($node->groups[$key]);
continue;
}
$context->markObjectAsValidatedForGroup($objectHash, $groupHash);
}
// Validate normal group
if (!$group instanceof GroupSequence) {
$this->validateNodeForGroup($node, $group, $context, $objectHash);
continue;
}
// Traverse group sequence until a violation is generated
$this->stepThroughGroupSequence($node, $group, $context);
// Skip the group sequence when validating successor nodes
unset($node->groups[$key]);
}
return true;
}
/**
* Validates a node's value in each group of a group sequence.
*
* If any of the groups' constraints generates a violation, subsequent
* groups are not validated anymore.
*
* @param Node $node The validated node
* @param GroupSequence $groupSequence The group sequence
* @param ExecutionContextInterface $context The execution context
*/
private function stepThroughGroupSequence(Node $node, GroupSequence $groupSequence, ExecutionContextInterface $context)
{
$violationCount = count($context->getViolations());
foreach ($groupSequence->groups as $groupInSequence) {
$node = clone $node;
$node->groups = array($groupInSequence);
if (null !== $groupSequence->cascadedGroup) {
$node->cascadedGroups = array($groupSequence->cascadedGroup);
}
if ($node instanceof ClassNode) {
$this->traverseClassNode($node, $context);
} elseif ($node instanceof CollectionNode) {
$this->traverseCollectionNode($node, $context);
} else {
$this->traverseGenericNode($node, $context);
}
// Abort sequence validation if a violation was generated
if (count($context->getViolations()) > $violationCount) {
break;
}
}
}
/**
* Validates a node's value against all constraints in the given group.
*
* @param Node $node The validated node
* @param string $group The group to validate
* @param ExecutionContextInterface $context The execution context
* @param string $objectHash The hash of the node's
* object (if any)
*
* @throws \Exception
*/
private function validateNodeForGroup(Node $node, $group, ExecutionContextInterface $context, $objectHash)
{
try {
$context->setGroup($group);
foreach ($node->metadata->findConstraints($group) as $constraint) {
// Prevent duplicate validation of constraints, in the case
// that constraints belong to multiple validated groups
if (null !== $objectHash) {
$constraintHash = spl_object_hash($constraint);
if ($node instanceof ClassNode) {
if ($context->isClassConstraintValidated($objectHash, $constraintHash)) {
continue;
}
$context->markClassConstraintAsValidated($objectHash, $constraintHash);
} elseif ($node instanceof PropertyNode) {
$propertyName = $node->metadata->getPropertyName();
if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) {
continue;
}
$context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash);
}
}
$validator = $this->validatorFactory->getInstance($constraint);
$validator->initialize($context);
$validator->validate($node->value, $constraint);
}
$context->setGroup(null);
} catch (\Exception $e) {
// Should be put into a finally block once we switch to PHP 5.5
$context->setGroup(null);
throw $e;
}
}
/**
* {@inheritdoc}
*/
public function getCurrentGroup()
{
return $this->currentGroup;
}
}

View File

@ -0,0 +1,124 @@
<?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\Validator;
use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\MetadataFactoryInterface;
/**
* Default implementation of {@link ValidatorInterface}.
*
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class RecursiveValidator implements ValidatorInterface
{
/**
* @var ExecutionContextFactoryInterface
*/
protected $contextFactory;
/**
* @var MetadataFactoryInterface
*/
protected $metadataFactory;
protected $validatorFactory;
/**
* Creates a new validator.
*
* @param ExecutionContextFactoryInterface $contextFactory The factory for
* creating new contexts
* @param MetadataFactoryInterface $metadataFactory The factory for
* fetching the metadata
* of validated objects
*/
public function __construct(ExecutionContextFactoryInterface $contextFactory, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory)
{
$this->contextFactory = $contextFactory;
$this->metadataFactory = $metadataFactory;
$this->validatorFactory = $validatorFactory;
}
/**
* {@inheritdoc}
*/
public function startContext($root = null)
{
return new RecursiveContextualValidator(
$this->contextFactory->createContext($this, $root),
$this->metadataFactory,
$this->validatorFactory
);
}
/**
* {@inheritdoc}
*/
public function inContext(ExecutionContextInterface $context)
{
return new RecursiveContextualValidator(
$context,
$this->metadataFactory,
$this->validatorFactory
);
}
/**
* {@inheritdoc}
*/
public function getMetadataFor($object)
{
return $this->metadataFactory->getMetadataFor($object);
}
/**
* {@inheritdoc}
*/
public function hasMetadataFor($object)
{
return $this->metadataFactory->hasMetadataFor($object);
}
/**
* {@inheritdoc}
*/
public function validate($value, $constraints = null, $groups = null)
{
return $this->startContext($value)
->validate($value, $constraints, $groups)
->getViolations();
}
/**
* {@inheritdoc}
*/
public function validateProperty($object, $propertyName, $groups = null)
{
return $this->startContext($object)
->validateProperty($object, $propertyName, $groups)
->getViolations();
}
/**
* {@inheritdoc}
*/
public function validatePropertyValue($object, $propertyName, $value, $groups = null)
{
return $this->startContext($object)
->validatePropertyValue($object, $propertyName, $value, $groups)
->getViolations();
}
}

View File

@ -29,7 +29,7 @@ use Symfony\Component\Validator\Util\PropertyPath;
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ContextualValidator implements ContextualValidatorInterface
class TraversingContextualValidator implements ContextualValidatorInterface
{
/**
* @var ExecutionContextInterface

View File

@ -22,7 +22,7 @@ use Symfony\Component\Validator\MetadataFactoryInterface;
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class Validator implements ValidatorInterface
class TraversingValidator implements ValidatorInterface
{
/**
* @var ExecutionContextFactoryInterface
@ -61,7 +61,7 @@ class Validator implements ValidatorInterface
*/
public function startContext($root = null)
{
return new ContextualValidator(
return new TraversingContextualValidator(
$this->contextFactory->createContext($this, $root),
$this->nodeTraverser,
$this->metadataFactory
@ -73,7 +73,7 @@ class Validator implements ValidatorInterface
*/
public function inContext(ExecutionContextInterface $context)
{
return new ContextualValidator(
return new TraversingContextualValidator(
$context,
$this->nodeTraverser,
$this->metadataFactory

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Validator;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Validator\Context\ExecutionContextFactory;
@ -37,7 +38,7 @@ use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor;
use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor;
use Symfony\Component\Validator\NodeVisitor\ObjectInitializationVisitor;
use Symfony\Component\Validator\Validator as ValidatorV24;
use Symfony\Component\Validator\Validator\Validator;
use Symfony\Component\Validator\Validator\TraversingValidator;
use Symfony\Component\Validator\Validator\LegacyValidator;
/**
@ -373,6 +374,19 @@ class ValidatorBuilder implements ValidatorBuilderInterface
if ($this->annotationReader) {
$loaders[] = new AnnotationLoader($this->annotationReader);
AnnotationRegistry::registerLoader(function ($class) {
if (0 === strpos($class, __NAMESPACE__.'\\Constraints\\')) {
$file = str_replace(__NAMESPACE__.'\\Constraints\\', __DIR__.'/Constraints/', $class).'.php';
if (is_file($file)) {
require_once $file;
return true;
}
}
return false;
});
}
$loader = null;
@ -401,26 +415,24 @@ class ValidatorBuilder implements ValidatorBuilderInterface
return new ValidatorV24($metadataFactory, $validatorFactory, $translator, $this->translationDomain, $this->initializers);
}
$nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory);
$nodeValidator = new NodeValidationVisitor($nodeTraverser, $validatorFactory);
if (Validation::API_VERSION_2_5 === $apiVersion) {
$contextFactory = new ExecutionContextFactory($nodeValidator, $translator, $this->translationDomain);
$contextFactory = new ExecutionContextFactory($translator, $this->translationDomain);
} else {
$contextFactory = new LegacyExecutionContextFactory($nodeValidator, $translator, $this->translationDomain);
$contextFactory = new LegacyExecutionContextFactory($translator, $this->translationDomain);
}
$nodeTraverser->addVisitor(new ContextUpdateVisitor());
if (count($this->initializers) > 0) {
$nodeTraverser->addVisitor(new ObjectInitializationVisitor($this->initializers));
}
$nodeTraverser->addVisitor(new DefaultGroupReplacingVisitor());
$nodeTraverser->addVisitor($nodeValidator);
if (Validation::API_VERSION_2_5 === $apiVersion) {
return new Validator($contextFactory, $nodeTraverser, $metadataFactory);
$nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory);
if (count($this->initializers) > 0) {
$nodeTraverser->addVisitor(new ObjectInitializationVisitor($this->initializers));
}
$nodeTraverser->addVisitor(new ContextUpdateVisitor());
$nodeTraverser->addVisitor(new DefaultGroupReplacingVisitor());
$nodeTraverser->addVisitor(new NodeValidationVisitor($nodeTraverser, $validatorFactory));
return new TraversingValidator($contextFactory, $nodeTraverser, $metadataFactory);
}
return new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory);
return new LegacyValidator($contextFactory, $metadataFactory, $validatorFactory);
}
}