[Validator] Removed traverser implementation

The traverser is too slow compared to the current, recursive approach.
Testing showed a performance decrease of about 70% without a lot of
optimization potential.
This commit is contained in:
Bernhard Schussek 2014-03-18 10:02:15 +01:00
parent 3183aed7cd
commit 90c27bb1e7
18 changed files with 3 additions and 1938 deletions

View File

@ -1,74 +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\Node;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
/**
* Represents an object and its class metadata in the validation graph.
*
* If the object is a collection which should be traversed, a new
* {@link CollectionNode} instance will be created for that object:
*
* (TagList:ClassNode)
* \
* (TagList:CollectionNode)
*
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ClassNode extends Node
{
/**
* @var ClassMetadataInterface
*/
public $metadata;
/**
* Creates a new class node.
*
* @param object $object The validated object
* @param ClassMetadataInterface $metadata The class metadata of
* that object
* @param string $propertyPath The property path leading
* to this node
* @param string[] $groups The groups in which this
* node should be validated
* @param string[]|null $cascadedGroups The groups in which
* cascaded objects should
* be validated
* @param integer $traversalStrategy The strategy used for
* traversing the object
*
* @throws UnexpectedTypeException If $object is not an object
*
* @see \Symfony\Component\Validator\Mapping\TraversalStrategy
*/
public function __construct($object, $cacheKey, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT)
{
if (!is_object($object)) {
throw new UnexpectedTypeException($object, 'object');
}
parent::__construct(
$object,
$cacheKey,
$metadata,
$propertyPath,
$groups,
$cascadedGroups,
$traversalStrategy
);
}
}

View File

@ -1,66 +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\Node;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
/**
* Represents a traversable value in the validation graph.
*
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CollectionNode extends Node
{
/**
* Creates a new collection node.
*
* @param array|\Traversable $collection The validated collection
* @param string $propertyPath The property path leading
* to this node
* @param string[] $groups The groups in which this
* node should be validated
* @param string[]|null $cascadedGroups The groups in which
* cascaded objects should be
* validated
* @param integer $traversalStrategy The strategy used for
* traversing the collection
*
* @throws ConstraintDefinitionException If $collection is not an array or a
* \Traversable
*
* @see \Symfony\Component\Validator\Mapping\TraversalStrategy
*/
public function __construct($collection, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::TRAVERSE)
{
if (!is_array($collection) && !$collection instanceof \Traversable) {
// Must throw a ConstraintDefinitionException for backwards
// compatibility reasons with Symfony < 2.5
throw new ConstraintDefinitionException(sprintf(
'Traversal was enabled for "%s", but this class '.
'does not implement "\Traversable".',
get_class($collection)
));
}
parent::__construct(
$collection,
null,
null,
$propertyPath,
$groups,
$cascadedGroups,
$traversalStrategy
);
}
}

View File

@ -1,26 +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\Node;
/**
* Represents a value that has neither class metadata nor property metadata
* attached to it.
*
* Together with {@link \Symfony\Component\Validator\Mapping\GenericMetadata},
* this node type can be used to validate a value against some constraints.
*
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class GenericNode extends Node
{
}

View File

@ -1,101 +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\Node;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Mapping\MetadataInterface;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
/**
* A node in the validation graph.
*
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
abstract class Node
{
/**
* The validated value.
*
* @var mixed
*/
public $value;
public $cacheKey;
/**
* The metadata specifying how the value should be validated.
*
* @var MetadataInterface|null
*/
public $metadata;
/**
* The property path leading to this node.
*
* @var string
*/
public $propertyPath;
/**
* The groups in which the value should be validated.
*
* @var string[]
*/
public $groups;
/**
* The groups in which cascaded values should be validated.
*
* @var string[]
*/
public $cascadedGroups;
/**
* The strategy used for traversing the validated value.
*
* @var integer
*
* @see \Symfony\Component\Validator\Mapping\TraversalStrategy
*/
public $traversalStrategy;
/**
* Creates a new property node.
*
* @param mixed $value The property value
* @param MetadataInterface|null $metadata The property's metadata
* @param string $propertyPath The property path leading to
* this node
* @param string[] $groups The groups in which this node
* should be validated
* @param string[]|null $cascadedGroups The groups in which cascaded
* objects should be validated
* @param integer $traversalStrategy
*
* @throws UnexpectedTypeException If $cascadedGroups is invalid
*/
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;
$this->cascadedGroups = $cascadedGroups;
$this->traversalStrategy = $traversalStrategy;
}
}

View File

@ -1,82 +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\Node;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Mapping\PropertyMetadataInterface;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
/**
* Represents the value of a property and its associated metadata.
*
* If the property contains an object and should be cascaded, a new
* {@link ClassNode} instance will be created for that object:
*
* (Article:ClassNode)
* \
* (->author:PropertyNode)
* \
* (Author:ClassNode)
*
* If the property contains a collection which should be traversed, a new
* {@link CollectionNode} instance will be created for that collection:
*
* (Article:ClassNode)
* \
* (->tags:PropertyNode)
* \
* (array:CollectionNode)
*
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyNode extends Node
{
/**
* @var PropertyMetadataInterface
*/
public $metadata;
/**
* Creates a new property node.
*
* @param object $object The object the property
* belongs to
* @param mixed $value The property value
* @param PropertyMetadataInterface $metadata The property's metadata
* @param string $propertyPath The property path leading
* to this node
* @param string[] $groups The groups in which this
* node should be validated
* @param string[]|null $cascadedGroups The groups in which
* cascaded objects should
* be validated
* @param integer $traversalStrategy
*
* @throws UnexpectedTypeException If $object is not an object
*
* @see \Symfony\Component\Validator\Mapping\TraversalStrategy
*/
public function __construct($value, $cacheKey, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT)
{
parent::__construct(
$value,
$cacheKey,
$metadata,
$propertyPath,
$groups,
$cascadedGroups,
$traversalStrategy
);
}
}

View File

@ -1,99 +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\NodeTraverser;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Node\Node;
use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface;
/**
* Traverses the nodes of the validation graph.
*
* You can attach visitors to the traverser that are invoked during the
* traversal. Before starting the traversal, the
* {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::beforeTraversal()}
* method of each visitor is called. For each node in the graph, the
* {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::visit()}
* of each visitor is called. At the end of the traversal, the traverser invokes
* {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::afterTraversal()}
* on each visitor. The visitors are called in the same order in which they are
* added to the traverser.
*
* If the {@link traverse()} method is called recursively, the
* {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::beforeTraversal()}
* and {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::afterTraversal()}
* methods of the visitors will be invoked for each call.
*
* The validation graph typically contains nodes of the following types:
*
* - {@link \Symfony\Component\Validator\Node\ClassNode}:
* An object with associated class metadata
* - {@link \Symfony\Component\Validator\Node\PropertyNode}:
* A property value with associated property metadata
* - {@link \Symfony\Component\Validator\Node\GenericNode}:
* A generic value with associated constraints
* - {@link \Symfony\Component\Validator\Node\CollectionNode}:
* A traversable collection
*
* Generic nodes are mostly useful when you want to validate a value that has
* neither associated class nor property metadata. Generic nodes usually come
* with {@link \Symfony\Component\Validator\Mapping\GenericMetadata}, that
* contains the constraints that the value should be validated against.
*
* Whenever a class, property or generic node is validated that contains a
* traversable value which should be traversed (according to the
* {@link \Symfony\Component\Validator\Mapping\TraversalStrategy} specified
* in the node or its metadata), a new
* {@link \Symfony\Component\Validator\Node\CollectionNode} will be attached
* to the node graph.
*
* For example:
*
* (TagList:ClassNode)
* \
* (TagList:CollectionNode)
*
* When writing custom visitors, be aware that collection nodes usually contain
* values that have already been passed to the visitor before through a class
* node, a property node or a generic node.
*
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface NodeTraverserInterface
{
/**
* Adds a new visitor to the traverser.
*
* Visitors that have already been added before are ignored.
*
* @param NodeVisitorInterface $visitor The visitor to add
*/
public function addVisitor(NodeVisitorInterface $visitor);
/**
* Removes a visitor from the traverser.
*
* Non-existing visitors are ignored.
*
* @param NodeVisitorInterface $visitor The visitor to remove
*/
public function removeVisitor(NodeVisitorInterface $visitor);
/**
* Traverses the given nodes in the given context.
*
* @param Node[] $nodes The nodes to traverse
* @param ExecutionContextInterface $context The validation context
*/
public function traverse($nodes, ExecutionContextInterface $context);
}

View File

@ -1,560 +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\NodeTraverser;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\NoSuchMetadataException;
use Symfony\Component\Validator\Exception\UnsupportedMetadataException;
use Symfony\Component\Validator\Mapping\CascadingStrategy;
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
use Symfony\Component\Validator\Mapping\PropertyMetadataInterface;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
use Symfony\Component\Validator\MetadataFactoryInterface;
use Symfony\Component\Validator\Node\ClassNode;
use Symfony\Component\Validator\Node\CollectionNode;
use Symfony\Component\Validator\Node\Node;
use Symfony\Component\Validator\Node\PropertyNode;
use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface;
/**
* Non-recursive implementation of {@link NodeTraverserInterface}.
*
* This implementation uses a Depth First algorithm to traverse the node
* graph. Instead of loading the complete node graph into memory before the
* traversal, the traverser only expands the successor nodes of a node once
* that node is traversed. For example, when traversing a class node, the
* nodes for all constrained properties of that class are loaded into memory.
* When the traversal of the class node is over, the node is discarded.
*
* Next, one of the class' property nodes is traversed. At that point, the
* successor nodes of that property node (a class node, if the property should
* be cascaded, or a collection node, if the property should be traversed) are
* loaded into memory. As soon as the traversal of the property node is over,
* it is discarded as well.
*
* This leads to an average memory consumption of O(log N * B), where N is the
* number of nodes in the graph and B is the average number of successor nodes
* of a node.
*
* In order to maintain a small execution stack, nodes are not validated
* recursively, but iteratively. Internally, a stack is used to store all the
* nodes that should be processed. Whenever a node is traversed, its successor
* nodes are put on the stack. The traverser keeps fetching and traversing nodes
* from the stack until the stack is empty and all nodes have been traversed.
*
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @see NodeTraverserInterface
* @see CascadingStrategy
* @see TraversalStrategy
*/
class NonRecursiveNodeTraverser implements NodeTraverserInterface
{
/**
* @var NodeVisitorInterface[]
*/
private $visitors;
/**
* @var MetadataFactoryInterface
*/
private $metadataFactory;
/**
* Creates a new traverser.
*
* @param MetadataFactoryInterface $metadataFactory The metadata factory
*/
public function __construct(MetadataFactoryInterface $metadataFactory)
{
$this->visitors = new \SplObjectStorage();
$this->nodeStack = new \SplStack();
$this->metadataFactory = $metadataFactory;
}
/**
* {@inheritdoc}
*/
public function addVisitor(NodeVisitorInterface $visitor)
{
$this->visitors->attach($visitor);
}
/**
* {@inheritdoc}
*/
public function removeVisitor(NodeVisitorInterface $visitor)
{
$this->visitors->detach($visitor);
}
/**
* {@inheritdoc}
*/
public function traverse($nodes, ExecutionContextInterface $context)
{
if (!is_array($nodes)) {
$nodes = array($nodes);
}
$numberOfInitializedVisitors = $this->beforeTraversal($nodes, $context);
// If any of the visitors requested to abort the traversal, do so, but
// clean up before
if ($numberOfInitializedVisitors < count($this->visitors)) {
$this->afterTraversal($nodes, $context, $numberOfInitializedVisitors);
return;
}
// This stack contains all the nodes that should be traversed
// A stack is used rather than a queue in order to traverse the graph
// in a Depth First approach (the last added node is processed first).
// In this way, the order in which the nodes are passed to the visitors
// is similar to a recursive implementation (except that the successor
// nodes of a node are traversed right-to-left instead of left-to-right).
$nodeStack = new \SplStack();
foreach ($nodes as $node) {
// Push a node to the stack and immediately process it. This way,
// the successor nodes are traversed before the next node in $nodes
$nodeStack->push($node);
// Fetch nodes from the stack and traverse them until no more nodes
// are left. Then continue with the next node in $nodes.
while (!$nodeStack->isEmpty()) {
$node = $nodeStack->pop();
if ($node instanceof ClassNode) {
$this->traverseClassNode($node, $context, $nodeStack);
} elseif ($node instanceof CollectionNode) {
$this->traverseCollectionNode($node, $context, $nodeStack);
} else {
$this->traverseNode($node, $context, $nodeStack);
}
}
}
$this->afterTraversal($nodes, $context);
}
/**
* Executes the {@link NodeVisitorInterface::beforeTraversal()} method of
* each visitor.
*
* @param Node[] $nodes The traversed nodes
* @param ExecutionContextInterface $context The current execution context
*
* @return integer The number of successful calls. This is lower than
* the number of visitors if any of the visitors'
* beforeTraversal() methods returned false
*/
private function beforeTraversal($nodes, ExecutionContextInterface $context)
{
$numberOfCalls = 1;
foreach ($this->visitors as $visitor) {
if (false === $visitor->beforeTraversal($nodes, $context)) {
break;
}
++$numberOfCalls;
}
return $numberOfCalls;
}
/**
* Executes the {@link NodeVisitorInterface::beforeTraversal()} method of
* each visitor.
*
* @param Node[] $nodes The traversed nodes
* @param ExecutionContextInterface $context The current execution context
* @param integer|null $limit Limits the number of visitors
* on which beforeTraversal()
* should be called. All visitors
* will be called by default
*/
private function afterTraversal($nodes, ExecutionContextInterface $context, $limit = null)
{
if (null === $limit) {
$limit = count($this->visitors);
}
$numberOfCalls = 0;
foreach ($this->visitors as $visitor) {
$visitor->afterTraversal($nodes, $context);
if (++$numberOfCalls === $limit) {
return;
}
}
}
/**
* Executes the {@link NodeVisitorInterface::visit()} method of each
* visitor.
*
* @param Node $node The visited node
* @param ExecutionContextInterface $context The current execution context
*
* @return Boolean Whether to traverse the node's successor nodes
*/
private function visit(Node $node, ExecutionContextInterface $context)
{
foreach ($this->visitors as $visitor) {
if (false === $visitor->visit($node, $context)) {
return false;
}
}
return true;
}
/**
* 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
* @param \SplStack $nodeStack The stack for storing the
* successor nodes
*
* @throws UnsupportedMetadataException If a property metadata does not
* implement {@link PropertyMetadataInterface}
*
* @see ClassNode
* @see PropertyNode
* @see CollectionNode
* @see TraversalStrategy
*/
private function traverseClassNode(ClassNode $node, ExecutionContextInterface $context, \SplStack $nodeStack)
{
// Visitors have two possibilities to influence the traversal:
//
// 1. If a visitor's visit() method returns false, the traversal is
// skipped entirely.
// 2. If a visitor's visit() method removes a group from the node,
// that group will be skipped in the subtree of that node.
if (false === $this->visit($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)
));
}
$nodeStack->push(new PropertyNode(
$propertyMetadata->getPropertyValue($node->value),
$node->cacheKey.':'.$propertyName,
$propertyMetadata,
$node->propertyPath
? $node->propertyPath.'.'.$propertyName
: $propertyName,
$node->groups,
$node->cascadedGroups
));
}
}
$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
$nodeStack->push(new CollectionNode(
$node->value,
$node->propertyPath,
$node->groups,
$node->cascadedGroups,
$traversalStrategy
));
}
/**
* 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
* @param \SplStack $nodeStack The stack for storing the
* successor nodes
*
* @see ClassNode
* @see CollectionNode
*/
private function traverseCollectionNode(CollectionNode $node, ExecutionContextInterface $context, \SplStack $nodeStack)
{
// Visitors have two possibilities to influence the traversal:
//
// 1. If a visitor's visit() method returns false, the traversal is
// skipped entirely.
// 2. If a visitor's visit() method removes a group from the node,
// that group will be skipped in the subtree of that node.
if (false === $this->visit($node, $context)) {
return;
}
if (0 === count($node->groups)) {
return;
}
$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)
$nodeStack->push(new CollectionNode(
$value,
$node->propertyPath.'['.$key.']',
$node->groups,
null,
$traversalStrategy
));
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,
$nodeStack
);
}
}
}
/**
* 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
* @param \SplStack $nodeStack The stack for storing the
* successor nodes
*/
private function traverseNode(Node $node, ExecutionContextInterface $context, \SplStack $nodeStack)
{
// Visitors have two possibilities to influence the traversal:
//
// 1. If a visitor's visit() method returns false, the traversal is
// skipped entirely.
// 2. If a visitor's visit() method removes a group from the node,
// that group will be skipped in the subtree of that node.
if (false === $this->visit($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)
$nodeStack->push(new CollectionNode(
$node->value,
$node->propertyPath,
$cascadedGroups,
null,
$traversalStrategy
));
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,
$nodeStack
);
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 \SplStack $nodeStack The stack for storing the successor
* nodes
*
* @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, \SplStack $nodeStack)
{
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)
));
}
$nodeStack->push(new ClassNode(
$object,
spl_object_hash($object),
$classMetadata,
$propertyPath,
$groups,
null,
$traversalStrategy
));
} 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;
}
$nodeStack->push(new CollectionNode(
$object,
$propertyPath,
$groups,
null,
$traversalStrategy
));
}
}
}

View File

@ -1,47 +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\NodeVisitor;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Node\Node;
/**
* Base visitor with empty method stubs.
*
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @see NodeVisitorInterface
*/
abstract class AbstractVisitor implements NodeVisitorInterface
{
/**
* {@inheritdoc}
*/
public function beforeTraversal($nodes, ExecutionContextInterface $context)
{
}
/**
* {@inheritdoc}
*/
public function afterTraversal($nodes, ExecutionContextInterface $context)
{
}
/**
* {@inheritdoc}
*/
public function visit(Node $node, ExecutionContextInterface $context)
{
}
}

View File

@ -1,207 +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\NodeVisitor;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\GroupSequence;
use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Node\ClassNode;
use Symfony\Component\Validator\Node\CollectionNode;
use Symfony\Component\Validator\Node\Node;
use Symfony\Component\Validator\Node\PropertyNode;
use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface;
/**
* Validates a node's value against the constraints defined in it's metadata.
*
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class NodeValidationVisitor extends AbstractVisitor
{
/**
* @var ConstraintValidatorFactoryInterface
*/
private $validatorFactory;
/**
* @var NodeTraverserInterface
*/
private $nodeTraverser;
/**
* Creates a new visitor.
*
* @param NodeTraverserInterface $nodeTraverser The node traverser
* @param ConstraintValidatorFactoryInterface $validatorFactory The validator factory
*/
public function __construct(NodeTraverserInterface $nodeTraverser, ConstraintValidatorFactoryInterface $validatorFactory)
{
$this->validatorFactory = $validatorFactory;
$this->nodeTraverser = $nodeTraverser;
}
/**
* 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 visit(Node $node, ExecutionContextInterface $context)
{
if ($node instanceof CollectionNode) {
return true;
}
$context->setNode($node->value, $node->metadata, $node->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 ($node->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 ($node instanceof ClassNode) {
// Use the object hash for group sequences
$groupHash = is_object($group) ? spl_object_hash($group) : $group;
if ($context->isGroupValidated($node->cacheKey, $groupHash)) {
// Skip this group when validating the successor nodes
// (property and/or collection nodes)
unset($node->groups[$key]);
continue;
}
$context->markGroupAsValidated($node->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 ($node->metadata->hasGroupSequence()) {
// The group sequence is statically defined for the class
$group = $node->metadata->getGroupSequence();
$cascadedGroup = Constraint::DEFAULT_GROUP;
} elseif ($node->metadata->isGroupSequenceProvider()) {
// The group sequence is dynamically obtained from the validated
// object
/** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */
$group = $node->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->traverseGroupSequence($node, $group, $cascadedGroup, $context);
// Skip the group sequence when validating successor nodes
unset($node->groups[$key]);
continue;
}
// Validate normal group
$this->validateInGroup($node, $group, $context);
}
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 traverseGroupSequence(Node $node, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context)
{
$violationCount = count($context->getViolations());
$cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null;
foreach ($groupSequence->groups as $groupInSequence) {
$node = clone $node;
$node->groups = array($groupInSequence);
$node->cascadedGroups = $cascadedGroups;
$this->nodeTraverser->traverse(array($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 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 !== $node->cacheKey) {
$constraintHash = spl_object_hash($constraint);
if ($context->isConstraintValidated($node->cacheKey, $constraintHash)) {
continue;
}
$context->markConstraintAsValidated($node->cacheKey, $constraintHash);
}
$validator = $this->validatorFactory->getInstance($constraint);
$validator->initialize($context);
$validator->validate($node->value, $constraint);
}
}
}

View File

@ -1,59 +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\NodeVisitor;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Node\Node;
/**
* A node visitor invoked by the node traverser.
*
* At the beginning of the traversal, the method {@link beforeTraversal()} is
* called. For each traversed node, the method {@link visit()} is called. At
* last, the method {@link afterTraversal()} is called when the traversal is
* complete.
*
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @see \Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface
*/
interface NodeVisitorInterface
{
/**
* Called at the beginning of a traversal.
*
* @param Node[] $nodes A list of Node instances
* @param ExecutionContextInterface $context The execution context
*
* @return Boolean Whether to continue the traversal
*/
public function beforeTraversal($nodes, ExecutionContextInterface $context);
/**
* Called at the end of a traversal.
*
* @param Node[] $nodes A list of Node instances
* @param ExecutionContextInterface $context The execution context
*/
public function afterTraversal($nodes, ExecutionContextInterface $context);
/**
* Called for each node during a traversal.
*
* @param Node $node The current node
* @param ExecutionContextInterface $context The execution context
*
* @return Boolean Whether to traverse the node's successor nodes
*/
public function visit(Node $node, ExecutionContextInterface $context);
}

View File

@ -1,83 +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\NodeVisitor;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\InvalidArgumentException;
use Symfony\Component\Validator\Node\ClassNode;
use Symfony\Component\Validator\Node\Node;
use Symfony\Component\Validator\ObjectInitializerInterface;
/**
* Initializes the objects of all class nodes.
*
* You have to pass at least one instance of {@link ObjectInitializerInterface}
* to the constructor of this visitor.
*
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ObjectInitializationVisitor extends AbstractVisitor
{
/**
* @var ObjectInitializerInterface[]
*/
private $initializers;
/**
* Creates a new visitor.
*
* @param ObjectInitializerInterface[] $initializers The object initializers
*
* @throws InvalidArgumentException
*/
public function __construct(array $initializers)
{
foreach ($initializers as $initializer) {
if (!$initializer instanceof ObjectInitializerInterface) {
throw new InvalidArgumentException(sprintf(
'Validator initializers must implement '.
'"Symfony\Component\Validator\ObjectInitializerInterface". '.
'Got: "%s"',
is_object($initializer) ? get_class($initializer) : gettype($initializer)
));
}
}
// If no initializer is present, this visitor should not even be created
if (0 === count($initializers)) {
throw new InvalidArgumentException('Please pass at least one initializer.');
}
$this->initializers = $initializers;
}
/**
* Calls the {@link ObjectInitializerInterface::initialize()} method for
* the object of each class node.
*
* @param Node $node The current node
* @param ExecutionContextInterface $context The execution context
*
* @return Boolean Always returns true
*/
public function visit(Node $node, ExecutionContextInterface $context)
{
if ($node instanceof ClassNode) {
foreach ($this->initializers as $initializer) {
$initializer->initialize($node->value);
}
}
return true;
}
}

View File

@ -1,31 +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\Node;
use Symfony\Component\Validator\Node\ClassNode;
/**
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ClassNodeTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException \Symfony\Component\Validator\Exception\UnexpectedTypeException
*/
public function testConstructorExpectsObject()
{
$metadata = $this->getMock('Symfony\Component\Validator\Mapping\ClassMetadataInterface');
new ClassNode('foobar', null, $metadata, '', array(), array());
}
}

View File

@ -1,91 +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\NodeTraverser;
use Symfony\Component\Validator\Mapping\GenericMetadata;
use Symfony\Component\Validator\Node\GenericNode;
use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser;
use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory;
/**
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class NonRecursiveNodeTraverserTest extends \PHPUnit_Framework_TestCase
{
/**
* @var FakeMetadataFactory
*/
private $metadataFactory;
/**
* @var NonRecursiveNodeTraverser
*/
private $traverser;
protected function setUp()
{
$this->metadataFactory = new FakeMetadataFactory();
$this->traverser = new NonRecursiveNodeTraverser($this->metadataFactory);
}
public function testVisitorsMayPreventTraversal()
{
$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');
$visitor2 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface');
$visitor3 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface');
$visitor1->expects($this->once())
->method('beforeTraversal')
->with($nodes, $context);
// abort traversal
$visitor2->expects($this->once())
->method('beforeTraversal')
->with($nodes, $context)
->will($this->returnValue(false));
// never called
$visitor3->expects($this->never())
->method('beforeTraversal');
$visitor1->expects($this->never())
->method('visit');
$visitor2->expects($this->never())
->method('visit');
$visitor2->expects($this->never())
->method('visit');
// called in order to clean up
$visitor1->expects($this->once())
->method('afterTraversal')
->with($nodes, $context);
// abort traversal
$visitor2->expects($this->once())
->method('afterTraversal')
->with($nodes, $context);
// never called, because beforeTraversal() wasn't called either
$visitor3->expects($this->never())
->method('afterTraversal');
$this->traverser->addVisitor($visitor1);
$this->traverser->addVisitor($visitor2);
$this->traverser->addVisitor($visitor3);
$this->traverser->traverse($nodes, $context);
}
}

View File

@ -1,34 +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\Validator;
use Symfony\Component\Validator\ConstraintValidatorFactory;
use Symfony\Component\Validator\Context\ExecutionContextFactory;
use Symfony\Component\Validator\DefaultTranslator;
use Symfony\Component\Validator\MetadataFactoryInterface;
use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser;
use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor;
use Symfony\Component\Validator\Validator\TraversingValidator;
class TraversingValidator2Dot5ApiTest extends Abstract2Dot5ApiTest
{
protected function createValidator(MetadataFactoryInterface $metadataFactory)
{
$nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory);
$contextFactory = new ExecutionContextFactory(new DefaultTranslator());
$validator = new TraversingValidator($contextFactory, $nodeTraverser, $metadataFactory);
$nodeTraverser->addVisitor(new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()));
return $validator;
}
}

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\TraversingValidator', $this->builder->getValidator());
$this->assertInstanceOf('Symfony\Component\Validator\Validator\RecursiveValidator', $this->builder->getValidator());
}
public function testSetApiVersion24And25()

View File

@ -1,242 +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\Validator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\RuntimeException;
use Symfony\Component\Validator\Exception\UnsupportedMetadataException;
use Symfony\Component\Validator\Exception\ValidatorException;
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
use Symfony\Component\Validator\Mapping\GenericMetadata;
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\PropertyNode;
use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface;
use Symfony\Component\Validator\Util\PropertyPath;
/**
* Default implementation of {@link ContextualValidatorInterface}.
*
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class TraversingContextualValidator implements ContextualValidatorInterface
{
/**
* @var ExecutionContextInterface
*/
private $context;
/**
* @var NodeTraverserInterface
*/
private $nodeTraverser;
/**
* @var MetadataFactoryInterface
*/
private $metadataFactory;
/**
* Creates a validator for the given context.
*
* @param ExecutionContextInterface $context The execution context
* @param NodeTraverserInterface $nodeTraverser The node traverser
* @param MetadataFactoryInterface $metadataFactory The factory for fetching
* the metadata of validated
* objects
*/
public function __construct(ExecutionContextInterface $context, NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory)
{
$this->context = $context;
$this->defaultPropertyPath = $context->getPropertyPath();
$this->defaultGroups = array($context->getGroup() ?: Constraint::DEFAULT_GROUP);
$this->nodeTraverser = $nodeTraverser;
$this->metadataFactory = $metadataFactory;
}
/**
* {@inheritdoc}
*/
public function atPath($path)
{
$this->defaultPropertyPath = $this->context->getPropertyPath($path);
return $this;
}
/**
* {@inheritdoc}
*/
public function validate($value, $constraints = null, $groups = null)
{
$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
if (null !== $constraints) {
if (!is_array($constraints)) {
$constraints = array($constraints);
}
$metadata = new GenericMetadata();
$metadata->addConstraints($constraints);
$node = new GenericNode(
$value,
is_object($value) ? spl_object_hash($value) : null,
$metadata,
$this->defaultPropertyPath,
$groups
);
} elseif (is_array($value) || $value instanceof \Traversable && !$this->metadataFactory->hasMetadataFor($value)) {
$node = new CollectionNode(
$value,
$this->defaultPropertyPath,
$groups
);
} elseif (is_object($value)) {
$metadata = $this->metadataFactory->getMetadataFor($value);
if (!$metadata instanceof ClassMetadataInterface) {
throw new UnsupportedMetadataException(sprintf(
'The metadata factory should return instances of '.
'"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '.
'got: "%s".',
is_object($metadata) ? get_class($metadata) : gettype($metadata)
));
}
$node = new ClassNode(
$value,
spl_object_hash($value),
$metadata,
$this->defaultPropertyPath,
$groups
);
} else {
throw new RuntimeException(sprintf(
'Cannot validate values of type "%s" automatically. Please '.
'provide a constraint.',
gettype($value)
));
}
$this->nodeTraverser->traverse(array($node), $this->context);
return $this;
}
/**
* {@inheritdoc}
*/
public function validateProperty($object, $propertyName, $groups = null)
{
$classMetadata = $this->metadataFactory->getMetadataFor($object);
if (!$classMetadata instanceof ClassMetadataInterface) {
// Cannot be UnsupportedMetadataException because of BC with
// Symfony < 2.5
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;
$cacheKey = spl_object_hash($object);
$nodes = array();
foreach ($propertyMetadatas as $propertyMetadata) {
$propertyValue = $propertyMetadata->getPropertyValue($object);
$nodes[] = new PropertyNode(
$propertyValue,
$cacheKey.':'.$propertyName,
$propertyMetadata,
PropertyPath::append($this->defaultPropertyPath, $propertyName),
$groups
);
}
$this->nodeTraverser->traverse($nodes, $this->context);
return $this;
}
/**
* {@inheritdoc}
*/
public function validatePropertyValue($object, $propertyName, $value, $groups = null)
{
$classMetadata = $this->metadataFactory->getMetadataFor($object);
if (!$classMetadata instanceof ClassMetadataInterface) {
// Cannot be UnsupportedMetadataException because of BC with
// Symfony < 2.5
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;
$cacheKey = spl_object_hash($object);
$nodes = array();
foreach ($propertyMetadatas as $propertyMetadata) {
$nodes[] = new PropertyNode(
$value,
$cacheKey.':'.$propertyName,
$propertyMetadata,
PropertyPath::append($this->defaultPropertyPath, $propertyName),
$groups,
$groups
);
}
$this->nodeTraverser->traverse($nodes, $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);
}
}

View File

@ -1,128 +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\Validator;
use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\MetadataFactoryInterface;
use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface;
/**
* Default implementation of {@link ValidatorInterface}.
*
* @since 2.5
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class TraversingValidator implements ValidatorInterface
{
/**
* @var ExecutionContextFactoryInterface
*/
protected $contextFactory;
/**
* @var NodeTraverserInterface
*/
protected $nodeTraverser;
/**
* @var MetadataFactoryInterface
*/
protected $metadataFactory;
/**
* Creates a new validator.
*
* @param ExecutionContextFactoryInterface $contextFactory The factory for
* creating new contexts
* @param NodeTraverserInterface $nodeTraverser The node traverser
* @param MetadataFactoryInterface $metadataFactory The factory for
* fetching the metadata
* of validated objects
*/
public function __construct(ExecutionContextFactoryInterface $contextFactory, NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory)
{
$this->contextFactory = $contextFactory;
$this->nodeTraverser = $nodeTraverser;
$this->metadataFactory = $metadataFactory;
}
/**
* {@inheritdoc}
*/
public function startContext($root = null)
{
return new TraversingContextualValidator(
$this->contextFactory->createContext($this, $root),
$this->nodeTraverser,
$this->metadataFactory
);
}
/**
* {@inheritdoc}
*/
public function inContext(ExecutionContextInterface $context)
{
return new TraversingContextualValidator(
$context,
$this->nodeTraverser,
$this->metadataFactory
);
}
/**
* {@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

@ -35,6 +35,7 @@ use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser;
use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor;
use Symfony\Component\Validator\NodeVisitor\ObjectInitializationVisitor;
use Symfony\Component\Validator\Validator\LegacyValidator;
use Symfony\Component\Validator\Validator\RecursiveValidator;
use Symfony\Component\Validator\Validator\TraversingValidator;
use Symfony\Component\Validator\Validator as ValidatorV24;
@ -416,13 +417,7 @@ class ValidatorBuilder implements ValidatorBuilderInterface
$contextFactory = new LegacyExecutionContextFactory($metadataFactory, $translator, $this->translationDomain);
if (Validation::API_VERSION_2_5 === $apiVersion) {
$nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory);
if (count($this->initializers) > 0) {
$nodeTraverser->addVisitor(new ObjectInitializationVisitor($this->initializers));
}
$nodeTraverser->addVisitor(new NodeValidationVisitor($nodeTraverser, $validatorFactory));
return new TraversingValidator($contextFactory, $nodeTraverser, $metadataFactory);
return new RecursiveValidator($contextFactory, $metadataFactory, $validatorFactory);
}
return new LegacyValidator($contextFactory, $metadataFactory, $validatorFactory);