[Validator] Improved performance of cache key generation

This commit is contained in:
Bernhard Schussek 2014-03-17 20:33:59 +01:00
parent 029a71638e
commit 3183aed7cd
12 changed files with 299 additions and 407 deletions

View File

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

View File

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

View File

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

View File

@ -56,6 +56,7 @@ class CollectionNode extends Node
parent::__construct(
$collection,
null,
null,
$propertyPath,
$groups,
$cascadedGroups,

View File

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

View File

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

View File

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

View File

@ -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 (=[<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)
@ -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);

View File

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

View File

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

View File

@ -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 (=[<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 ($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 (=[<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 ($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);

View File

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