From 3183aed7cdd669ee6ad6fc4715e820dad1ae46c6 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Mar 2014 20:33:59 +0100 Subject: [PATCH] [Validator] Improved performance of cache key generation --- .../Validator/Context/ExecutionContext.php | 50 +- .../Context/ExecutionContextInterface.php | 43 +- .../Component/Validator/Node/ClassNode.php | 8 +- .../Validator/Node/CollectionNode.php | 1 + src/Symfony/Component/Validator/Node/Node.php | 5 +- .../Component/Validator/Node/PropertyNode.php | 14 +- .../NonRecursiveNodeTraverser.php | 3 +- .../NodeVisitor/NodeValidationVisitor.php | 36 +- .../Validator/Tests/Node/ClassNodeTest.php | 2 +- .../NonRecursiveNodeTraverserTest.php | 2 +- .../RecursiveContextualValidator.php | 534 ++++++++---------- .../TraversingContextualValidator.php | 8 +- 12 files changed, 299 insertions(+), 407 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 718de5eb25..75b7e2c3bf 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -104,7 +104,7 @@ class ExecutionContext implements ExecutionContextInterface * * @var array */ - private $validatedClassConstraints = array(); + private $validatedConstraints = array(); /** * Stores which property constraint has been validated for which property. @@ -319,64 +319,36 @@ class ExecutionContext implements ExecutionContextInterface /** * {@inheritdoc} */ - public function markObjectAsValidatedForGroup($objectHash, $groupHash) + public function markGroupAsValidated($cacheKey, $groupHash) { - if (!isset($this->validatedObjects[$objectHash])) { - $this->validatedObjects[$objectHash] = array(); + if (!isset($this->validatedObjects[$cacheKey])) { + $this->validatedObjects[$cacheKey] = array(); } - $this->validatedObjects[$objectHash][$groupHash] = true; + $this->validatedObjects[$cacheKey][$groupHash] = true; } /** * {@inheritdoc} */ - public function isObjectValidatedForGroup($objectHash, $groupHash) + public function isGroupValidated($cacheKey, $groupHash) { - return isset($this->validatedObjects[$objectHash][$groupHash]); + return isset($this->validatedObjects[$cacheKey][$groupHash]); } /** * {@inheritdoc} */ - public function markClassConstraintAsValidated($objectHash, $constraintHash) + public function markConstraintAsValidated($cacheKey, $constraintHash) { - if (!isset($this->validatedClassConstraints[$objectHash])) { - $this->validatedClassConstraints[$objectHash] = array(); - } - - $this->validatedClassConstraints[$objectHash][$constraintHash] = true; + $this->validatedConstraints[$cacheKey.':'.$constraintHash] = true; } /** * {@inheritdoc} */ - public function isClassConstraintValidated($objectHash, $constraintHash) + public function isConstraintValidated($cacheKey, $constraintHash) { - return isset($this->validatedClassConstraints[$objectHash][$constraintHash]); - } - - /** - * {@inheritdoc} - */ - public function markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash) - { - if (!isset($this->validatedPropertyConstraints[$objectHash])) { - $this->validatedPropertyConstraints[$objectHash] = array(); - } - - if (!isset($this->validatedPropertyConstraints[$objectHash][$propertyName])) { - $this->validatedPropertyConstraints[$objectHash][$propertyName] = array(); - } - - $this->validatedPropertyConstraints[$objectHash][$propertyName][$constraintHash] = true; - } - - /** - * {@inheritdoc} - */ - public function isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash) - { - return isset($this->validatedPropertyConstraints[$objectHash][$propertyName][$constraintHash]); + return isset($this->validatedConstraints[$cacheKey.':'.$constraintHash]); } } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index beafe75433..2e778fbec8 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -124,19 +124,19 @@ interface ExecutionContextInterface extends LegacyExecutionContextInterface /** * Marks an object as validated in a specific validation group. * - * @param string $objectHash The hash of the object + * @param string $cacheKey The hash of the object * @param string $groupHash The group's name or hash, if it is group * sequence * * @internal Used by the validator engine. Should not be called by user * code. */ - public function markObjectAsValidatedForGroup($objectHash, $groupHash); + public function markGroupAsValidated($cacheKey, $groupHash); /** * Returns whether an object was validated in a specific validation group. * - * @param string $objectHash The hash of the object + * @param string $cacheKey The hash of the object * @param string $groupHash The group's name or hash, if it is group * sequence * @@ -146,23 +146,23 @@ interface ExecutionContextInterface extends LegacyExecutionContextInterface * @internal Used by the validator engine. Should not be called by user * code. */ - public function isObjectValidatedForGroup($objectHash, $groupHash); + public function isGroupValidated($cacheKey, $groupHash); /** * Marks a constraint as validated for an object. * - * @param string $objectHash The hash of the object + * @param string $cacheKey The hash of the object * @param string $constraintHash The hash of the constraint * * @internal Used by the validator engine. Should not be called by user * code. */ - public function markClassConstraintAsValidated($objectHash, $constraintHash); + public function markConstraintAsValidated($cacheKey, $constraintHash); /** * Returns whether a constraint was validated for an object. * - * @param string $objectHash The hash of the object + * @param string $cacheKey The hash of the object * @param string $constraintHash The hash of the constraint * * @return Boolean Whether the constraint was already validated @@ -170,32 +170,5 @@ interface ExecutionContextInterface extends LegacyExecutionContextInterface * @internal Used by the validator engine. Should not be called by user * code. */ - public function isClassConstraintValidated($objectHash, $constraintHash); - - /** - * Marks a constraint as validated for an object and a property name. - * - * @param string $objectHash The hash of the object - * @param string $propertyName The property name - * @param string $constraintHash The hash of the constraint - * - * @internal Used by the validator engine. Should not be called by user - * code. - */ - public function markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); - - /** - * Returns whether a constraint was validated for an object and a property - * name. - * - * @param string $objectHash The hash of the object - * @param string $propertyName The property name - * @param string $constraintHash The hash of the constraint - * - * @return Boolean Whether the constraint was already validated - * - * @internal Used by the validator engine. Should not be called by user - * code. - */ - public function isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash); + public function isConstraintValidated($cacheKey, $constraintHash); } diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php index 54e22e2d97..f52a68366b 100644 --- a/src/Symfony/Component/Validator/Node/ClassNode.php +++ b/src/Symfony/Component/Validator/Node/ClassNode.php @@ -55,7 +55,7 @@ class ClassNode extends Node * * @see \Symfony\Component\Validator\Mapping\TraversalStrategy */ - public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) + public function __construct($object, $cacheKey, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { if (!is_object($object)) { throw new UnexpectedTypeException($object, 'object'); @@ -63,12 +63,12 @@ class ClassNode extends Node parent::__construct( $object, + $cacheKey, $metadata, $propertyPath, $groups, - $cascadedGroups + $cascadedGroups, + $traversalStrategy ); - - $this->traversalStrategy = $traversalStrategy; } } diff --git a/src/Symfony/Component/Validator/Node/CollectionNode.php b/src/Symfony/Component/Validator/Node/CollectionNode.php index ddca97a5c7..79a49ff6e3 100644 --- a/src/Symfony/Component/Validator/Node/CollectionNode.php +++ b/src/Symfony/Component/Validator/Node/CollectionNode.php @@ -56,6 +56,7 @@ class CollectionNode extends Node parent::__construct( $collection, null, + null, $propertyPath, $groups, $cascadedGroups, diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php index 56c8145c45..93099844be 100644 --- a/src/Symfony/Component/Validator/Node/Node.php +++ b/src/Symfony/Component/Validator/Node/Node.php @@ -30,6 +30,8 @@ abstract class Node */ public $value; + public $cacheKey; + /** * The metadata specifying how the value should be validated. * @@ -82,13 +84,14 @@ abstract class Node * * @throws UnexpectedTypeException If $cascadedGroups is invalid */ - public function __construct($value, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) + public function __construct($value, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { if (null !== $cascadedGroups && !is_array($cascadedGroups)) { throw new UnexpectedTypeException($cascadedGroups, 'null or array'); } $this->value = $value; + $this->cacheKey = $cacheKey; $this->metadata = $metadata; $this->propertyPath = $propertyPath; $this->groups = $groups; diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php index 8934bf1d73..4ee7ac5918 100644 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -41,11 +41,6 @@ use Symfony\Component\Validator\Mapping\TraversalStrategy; */ class PropertyNode extends Node { - /** - * @var object - */ - public $object; - /** * @var PropertyMetadataInterface */ @@ -71,22 +66,17 @@ class PropertyNode extends Node * * @see \Symfony\Component\Validator\Mapping\TraversalStrategy */ - public function __construct($object, $value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) + public function __construct($value, $cacheKey, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { - if (!is_object($object)) { - throw new UnexpectedTypeException($object, 'object'); - } - parent::__construct( $value, + $cacheKey, $metadata, $propertyPath, $groups, $cascadedGroups, $traversalStrategy ); - - $this->object = $object; } } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php index 5c904f01d4..c29bac71e2 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php @@ -274,8 +274,8 @@ class NonRecursiveNodeTraverser implements NodeTraverserInterface } $nodeStack->push(new PropertyNode( - $node->value, $propertyMetadata->getPropertyValue($node->value), + $node->cacheKey.':'.$propertyName, $propertyMetadata, $node->propertyPath ? $node->propertyPath.'.'.$propertyName @@ -530,6 +530,7 @@ class NonRecursiveNodeTraverser implements NodeTraverserInterface $nodeStack->push(new ClassNode( $object, + spl_object_hash($object), $classMetadata, $propertyPath, $groups, diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index d3d7937ad9..5eee760c8a 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -71,14 +71,6 @@ class NodeValidationVisitor extends AbstractVisitor $context->setNode($node->value, $node->metadata, $node->propertyPath); - 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 (=[,G3,G4]) contains group sequence (=) // then call traverse() with each entry of the group sequence and abort // if necessary (G1, G2) @@ -97,7 +89,7 @@ class NodeValidationVisitor extends AbstractVisitor // Use the object hash for group sequences $groupHash = is_object($group) ? spl_object_hash($group) : $group; - if ($context->isObjectValidatedForGroup($objectHash, $groupHash)) { + if ($context->isGroupValidated($node->cacheKey, $groupHash)) { // Skip this group when validating the successor nodes // (property and/or collection nodes) unset($node->groups[$key]); @@ -105,7 +97,7 @@ class NodeValidationVisitor extends AbstractVisitor continue; } - $context->markObjectAsValidatedForGroup($objectHash, $groupHash); + $context->markGroupAsValidated($node->cacheKey, $groupHash); // Replace the "Default" group by the group sequence defined // for the class, if applicable @@ -144,7 +136,7 @@ class NodeValidationVisitor extends AbstractVisitor } // Validate normal group - $this->validateNodeForGroup($node, $group, $context, $objectHash); + $this->validateInGroup($node, $group, $context); } return true; @@ -190,31 +182,21 @@ class NodeValidationVisitor extends AbstractVisitor * * @throws \Exception */ - private function validateNodeForGroup(Node $node, $group, ExecutionContextInterface $context, $objectHash) + private function validateInGroup(Node $node, $group, ExecutionContextInterface $context) { $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) { + if (null !== $node->cacheKey) { $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); + if ($context->isConstraintValidated($node->cacheKey, $constraintHash)) { + continue; } + + $context->markConstraintAsValidated($node->cacheKey, $constraintHash); } $validator = $this->validatorFactory->getInstance($constraint); diff --git a/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php b/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php index 1241d1bb5b..c79f4c838f 100644 --- a/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php +++ b/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php @@ -26,6 +26,6 @@ class ClassNodeTest extends \PHPUnit_Framework_TestCase { $metadata = $this->getMock('Symfony\Component\Validator\Mapping\ClassMetadataInterface'); - new ClassNode('foobar', $metadata, '', array(), array()); + new ClassNode('foobar', null, $metadata, '', array(), array()); } } diff --git a/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php b/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php index 4dfc707124..09e26bcaf9 100644 --- a/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php +++ b/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php @@ -40,7 +40,7 @@ class NonRecursiveNodeTraverserTest extends \PHPUnit_Framework_TestCase public function testVisitorsMayPreventTraversal() { - $nodes = array(new GenericNode('value', new GenericMetadata(), '', array('Default'))); + $nodes = array(new GenericNode('value', null, new GenericMetadata(), '', array('Default'))); $context = $this->getMock('Symfony\Component\Validator\Context\ExecutionContextInterface'); $visitor1 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface'); diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index bb5a0fff5e..0e083c05cf 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -100,11 +100,9 @@ class RecursiveContextualValidator implements ContextualValidatorInterface $metadata = new GenericMetadata(); $metadata->addConstraints($constraints); - $this->traverseGenericNode( + $this->validateGenericNode( $value, is_object($value) ? spl_object_hash($value) : null, - null, - null, $metadata, $this->defaultPropertyPath, $groups, @@ -119,7 +117,6 @@ class RecursiveContextualValidator implements ContextualValidatorInterface if (is_object($value)) { $this->cascadeObject( $value, - spl_object_hash($value), $this->defaultPropertyPath, $groups, TraversalStrategy::IMPLICIT, @@ -168,16 +165,14 @@ class RecursiveContextualValidator implements ContextualValidatorInterface $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $containerHash = spl_object_hash($container); + $cacheKey = spl_object_hash($container); foreach ($propertyMetadatas as $propertyMetadata) { $propertyValue = $propertyMetadata->getPropertyValue($container); - $this->traverseGenericNode( + $this->validateGenericNode( $propertyValue, - is_object($propertyValue) ? spl_object_hash($propertyValue) : null, - $container, - $containerHash, + $cacheKey.':'.$propertyName, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups, @@ -210,14 +205,12 @@ class RecursiveContextualValidator implements ContextualValidatorInterface $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $containerHash = spl_object_hash($container); + $cacheKey = spl_object_hash($container); foreach ($propertyMetadatas as $propertyMetadata) { - $this->traverseGenericNode( + $this->validateGenericNode( $value, - is_object($value) ? spl_object_hash($value) : null, - $container, - $containerHash, + $cacheKey.':'.$propertyName, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups, @@ -275,9 +268,74 @@ class RecursiveContextualValidator implements ContextualValidatorInterface * @see CollectionNode * @see TraversalStrategy */ - private function traverseClassNode($value, $valueHash, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) + private function validateClassNode($value, $cacheKey, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { - $groups = $this->validateNode($value, $valueHash, null, null, $metadata, $propertyPath, $groups, $traversalStrategy, $context); + $context->setNode($value, $metadata, $propertyPath); + + // if group (=[,G3,G4]) contains group sequence (=) + // 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 ($groups as $key => $group) { + $cascadedGroup = null; + + // 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. + + // Use the object hash for group sequences + $groupHash = is_object($group) ? spl_object_hash($group) : $group; + + if ($context->isGroupValidated($cacheKey, $groupHash)) { + // Skip this group when validating the successor nodes + // (property and/or collection nodes) + unset($groups[$key]); + + continue; + } + + $context->markGroupAsValidated($cacheKey, $groupHash); + + // Replace the "Default" group by the group sequence defined + // for the class, if applicable + // This is done after checking the cache, so that + // spl_object_hash() isn't called for this sequence and + // "Default" is used instead in the cache. This is useful + // if the getters below return different group sequences in + // every call. + if (Constraint::DEFAULT_GROUP === $group) { + if ($metadata->hasGroupSequence()) { + // The group sequence is statically defined for the class + $group = $metadata->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + } elseif ($metadata->isGroupSequenceProvider()) { + // The group sequence is dynamically obtained from the validated + // object + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $group = $value->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + + if (!$group instanceof GroupSequence) { + $group = new GroupSequence($group); + } + } + } + + if ($group instanceof GroupSequence) { + $this->stepThroughGroupSequence($value, $cacheKey, $metadata, $propertyPath, $traversalStrategy, $group, $cascadedGroup, $context); + + // Skip the group sequence when validating successor nodes + unset($groups[$key]); + + continue; + } + + $this->validateInGroup($value, $cacheKey, $metadata, $group, $context); + } if (0 === count($groups)) { return; @@ -296,11 +354,9 @@ class RecursiveContextualValidator implements ContextualValidatorInterface $propertyValue = $propertyMetadata->getPropertyValue($value); - $this->traverseGenericNode( + $this->validateGenericNode( $propertyValue, - is_object($propertyValue) ? spl_object_hash($propertyValue) : null, - $value, - $valueHash, + $cacheKey.':'.$propertyName, $propertyMetadata, $propertyPath ? $propertyPath.'.'.$propertyName @@ -351,6 +407,171 @@ class RecursiveContextualValidator implements ContextualValidatorInterface ); } + /** + * 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 validateGenericNode($value, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) + { + $context->setNode($value, $metadata, $propertyPath); + + foreach ($groups as $key => $group) { + if ($group instanceof GroupSequence) { + $this->stepThroughGroupSequence($value, $cacheKey, $metadata, $propertyPath, $traversalStrategy, $group, null, $context); + + // Skip the group sequence when validating successor nodes + unset($groups[$key]); + + continue; + } + + $this->validateInGroup($value, $cacheKey, $metadata, $group, $context); + } + + if (0 === count($groups)) { + return; + } + + if (null === $value) { + return; + } + + $cascadingStrategy = $metadata->getCascadingStrategy(); + + // Quit unless we have an array or a cascaded object + if (!is_array($value) && !($cascadingStrategy & CascadingStrategy::CASCADE)) { + return; + } + + // 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 = $metadata->getTraversalStrategy() + | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); + } + + // The "cascadedGroups" property is set by the NodeValidationVisitor when + // traversing group sequences + $cascadedGroups = count($cascadedGroups) > 0 + ? $cascadedGroups + : $groups; + + if (is_array($value)) { + // Arrays are always traversed, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $this->cascadeCollection( + $value, + $propertyPath, + $cascadedGroups, + $traversalStrategy, + $context + ); + + return; + } + + // 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( + $value, + $propertyPath, + $cascadedGroups, + $traversalStrategy, + $context + ); + + // 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 $container 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($container, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) + { + try { + $classMetadata = $this->metadataFactory->getMetadataFor($container); + + 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->validateClassNode( + $container, + spl_object_hash($container), + $classMetadata, + $propertyPath, + $groups, + null, + $traversalStrategy, + $context + ); + } catch (NoSuchMetadataException $e) { + // Rethrow if not Traversable + if (!$container instanceof \Traversable) { + throw $e; + } + + // Rethrow unless IMPLICIT or TRAVERSE + if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { + throw $e; + } + + $this->cascadeCollection( + $container, + $propertyPath, + $groups, + $traversalStrategy, + $context + ); + } + } + /** * Traverses a collection node. * @@ -402,7 +623,6 @@ class RecursiveContextualValidator implements ContextualValidatorInterface if (is_object($value)) { $this->cascadeObject( $value, - spl_object_hash($value), $propertyPath.'['.$key.']', $groups, $traversalStrategy, @@ -412,246 +632,6 @@ class RecursiveContextualValidator implements ContextualValidatorInterface } } - /** - * 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($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) - { - $groups = $this->validateNode($value, $valueHash, $container, $containerHash, $metadata, $propertyPath, $groups, $traversalStrategy, $context); - - if (0 === count($groups)) { - return; - } - - if (null === $value) { - return; - } - - $cascadingStrategy = $metadata->getCascadingStrategy(); - - // Quit unless we have an array or a cascaded object - if (!is_array($value) && !($cascadingStrategy & CascadingStrategy::CASCADE)) { - return; - } - - // 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 = $metadata->getTraversalStrategy() - | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); - } - - // The "cascadedGroups" property is set by the NodeValidationVisitor when - // traversing group sequences - $cascadedGroups = count($cascadedGroups) > 0 - ? $cascadedGroups - : $groups; - - if (is_array($value)) { - // Arrays are always traversed, independent of the specified - // traversal strategy - // (BC with Symfony < 2.5) - $this->cascadeCollection( - $value, - $propertyPath, - $cascadedGroups, - $traversalStrategy, - $context - ); - - return; - } - - // 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( - $value, - $valueHash, - $propertyPath, - $cascadedGroups, - $traversalStrategy, - $context - ); - - // 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 $container 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($container, $containerHash, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) - { - try { - $classMetadata = $this->metadataFactory->getMetadataFor($container); - - 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( - $container, - $containerHash, - $classMetadata, - $propertyPath, - $groups, - null, - $traversalStrategy, - $context - ); - } catch (NoSuchMetadataException $e) { - // Rethrow if not Traversable - if (!$container instanceof \Traversable) { - throw $e; - } - - // Rethrow unless IMPLICIT or TRAVERSE - if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { - throw $e; - } - - $this->cascadeCollection( - $container, - $propertyPath, - $groups, - $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 array The groups in which the successor nodes should be validated - */ - public function validateNode($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) - { - $context->setNode($value, $metadata, $propertyPath); - - // if group (=[,G3,G4]) contains group sequence (=) - // 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 ($groups as $key => $group) { - $cascadedGroup = null; - - // 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 ($metadata instanceof ClassMetadataInterface) { - // Use the object hash for group sequences - $groupHash = is_object($group) ? spl_object_hash($group) : $group; - - if ($context->isObjectValidatedForGroup($valueHash, $groupHash)) { - // Skip this group when validating the successor nodes - // (property and/or collection nodes) - unset($groups[$key]); - - continue; - } - - $context->markObjectAsValidatedForGroup($valueHash, $groupHash); - - // Replace the "Default" group by the group sequence defined - // for the class, if applicable - // This is done after checking the cache, so that - // spl_object_hash() isn't called for this sequence and - // "Default" is used instead in the cache. This is useful - // if the getters below return different group sequences in - // every call. - if (Constraint::DEFAULT_GROUP === $group) { - if ($metadata->hasGroupSequence()) { - // The group sequence is statically defined for the class - $group = $metadata->getGroupSequence(); - $cascadedGroup = Constraint::DEFAULT_GROUP; - } elseif ($metadata->isGroupSequenceProvider()) { - // The group sequence is dynamically obtained from the validated - // object - /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $group = $value->getGroupSequence(); - $cascadedGroup = Constraint::DEFAULT_GROUP; - - if (!$group instanceof GroupSequence) { - $group = new GroupSequence($group); - } - } - } - } - - if ($group instanceof GroupSequence) { - // Traverse group sequence until a violation is generated - $this->stepThroughGroupSequence($value, $valueHash, $container, $containerHash, $metadata, $propertyPath, $traversalStrategy, $group, $cascadedGroup, $context); - - // Skip the group sequence when validating successor nodes - unset($groups[$key]); - - continue; - } - - // Validate normal group - $this->validateNodeForGroup($value, $valueHash, $containerHash, $metadata, $group, $context); - } - - return $groups; - } - /** * Validates a node's value in each group of a group sequence. * @@ -662,7 +642,7 @@ class RecursiveContextualValidator implements ContextualValidatorInterface * @param GroupSequence $groupSequence The group sequence * @param ExecutionContextInterface $context The execution context */ - private function stepThroughGroupSequence($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) + private function stepThroughGroupSequence($value, $cacheKey, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null; @@ -671,9 +651,9 @@ class RecursiveContextualValidator implements ContextualValidatorInterface $groups = array($groupInSequence); if ($metadata instanceof ClassMetadataInterface) { - $this->traverseClassNode( + $this->validateClassNode( $value, - $valueHash, + $cacheKey, $metadata, $propertyPath, $groups, @@ -682,11 +662,9 @@ class RecursiveContextualValidator implements ContextualValidatorInterface $context ); } else { - $this->traverseGenericNode( + $this->validateGenericNode( $value, - $valueHash, - $container, - $containerHash, + $cacheKey, $metadata, $propertyPath, $groups, @@ -714,33 +692,21 @@ class RecursiveContextualValidator implements ContextualValidatorInterface * * @throws \Exception */ - private function validateNodeForGroup($value, $valueHash, $containerHash, MetadataInterface $metadata = null, $group, ExecutionContextInterface $context) + private function validateInGroup($value, $cacheKey, MetadataInterface $metadata, $group, ExecutionContextInterface $context) { $context->setGroup($group); - $propertyName = $metadata instanceof PropertyMetadataInterface - ? $metadata->getPropertyName() - : null; - foreach ($metadata->findConstraints($group) as $constraint) { // Prevent duplicate validation of constraints, in the case // that constraints belong to multiple validated groups - if (null !== $propertyName) { + if (null !== $cacheKey) { $constraintHash = spl_object_hash($constraint); - if ($context->isPropertyConstraintValidated($containerHash, $propertyName, $constraintHash)) { + if ($context->isConstraintValidated($cacheKey, $constraintHash)) { continue; } - $context->markPropertyConstraintAsValidated($containerHash, $propertyName, $constraintHash); - } elseif (null !== $valueHash) { - $constraintHash = spl_object_hash($constraint); - - if ($context->isClassConstraintValidated($valueHash, $constraintHash)) { - continue; - } - - $context->markClassConstraintAsValidated($valueHash, $constraintHash); + $context->markConstraintAsValidated($cacheKey, $constraintHash); } $validator = $this->validatorFactory->getInstance($constraint); diff --git a/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php index 288fd8a66c..bd749eeb97 100644 --- a/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php @@ -94,6 +94,7 @@ class TraversingContextualValidator implements ContextualValidatorInterface $node = new GenericNode( $value, + is_object($value) ? spl_object_hash($value) : null, $metadata, $this->defaultPropertyPath, $groups @@ -118,6 +119,7 @@ class TraversingContextualValidator implements ContextualValidatorInterface $node = new ClassNode( $value, + spl_object_hash($value), $metadata, $this->defaultPropertyPath, $groups @@ -155,14 +157,15 @@ class TraversingContextualValidator implements ContextualValidatorInterface $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $cacheKey = spl_object_hash($object); $nodes = array(); foreach ($propertyMetadatas as $propertyMetadata) { $propertyValue = $propertyMetadata->getPropertyValue($object); $nodes[] = new PropertyNode( - $object, $propertyValue, + $cacheKey.':'.$propertyName, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups @@ -194,12 +197,13 @@ class TraversingContextualValidator implements ContextualValidatorInterface $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $cacheKey = spl_object_hash($object); $nodes = array(); foreach ($propertyMetadatas as $propertyMetadata) { $nodes[] = new PropertyNode( - $object, $value, + $cacheKey.':'.$propertyName, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups,