diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index ba4b0cb801..80af8392c5 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -11,12 +11,16 @@ namespace Symfony\Component\Validator\Context; +use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\ClassBasedInterface; +use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; /** * @since %%NextVersion%% @@ -48,18 +52,30 @@ class ExecutionContext implements ExecutionContextInterface */ private $groupManager; - public function __construct(ValidatorInterface $validator, GroupManagerInterface $groupManager) + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var string + */ + private $translationDomain; + + public function __construct($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) { + $this->root = $root; $this->validator = $validator; $this->groupManager = $groupManager; + $this->translator = $translator; + $this->translationDomain = $translationDomain; $this->violations = new ConstraintViolationList(); + $this->nodeStack = new \SplStack(); } public function pushNode(Node $node) { - if (null === $this->node) { - $this->root = $node->value; - } else { + if (null !== $this->node) { $this->nodeStack->push($this->node); } @@ -89,13 +105,32 @@ class ExecutionContext implements ExecutionContextInterface return $poppedNode; } - public function addViolation($message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) + public function addViolation($message, array $parameters = array()) { + $this->violations->add(new ConstraintViolation( + $this->translator->trans($message, $parameters, $this->translationDomain), + $message, + $parameters, + $this->root, + $this->getPropertyPath(), + $this->getValue(), + null, + null + )); } - public function buildViolation($message) + public function buildViolation($message, array $parameters = array()) { - + return new ConstraintViolationBuilder( + $this->violations, + $message, + $parameters, + $this->root, + $this->getPropertyPath(), + $this->getValue(), + $this->translator, + $this->translationDomain + ); } public function getViolations() @@ -141,15 +176,7 @@ class ExecutionContext implements ExecutionContextInterface { $propertyPath = $this->node ? $this->node->propertyPath : ''; - if (strlen($subPath) > 0) { - if ('[' === $subPath{1}) { - return $propertyPath.$subPath; - } - - return $propertyPath ? $propertyPath.'.'.$subPath : $subPath; - } - - return $propertyPath; + return PropertyPath::append($propertyPath, $subPath); } /** diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index 4a8765f27b..b6eeb73d35 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -30,11 +30,11 @@ interface ExecutionContextInterface * Adds a violation at the current node of the validation graph. * * @param string $message The error message. - * @param array $params The parameters substituted in the error message. + * @param array $parameters The parameters substituted in the error message. * * @api */ - public function addViolation($message, array $params = array()); + public function addViolation($message, array $parameters = array()); public function buildViolation($message); diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php index cfb93831f8..369b6a062b 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Context; +use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\NodeVisitor\AbstractVisitor; @@ -42,9 +43,21 @@ class ExecutionContextManager extends AbstractVisitor implements ExecutionContex */ private $contextStack; - public function __construct(GroupManagerInterface $groupManager) + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var string|null + */ + private $translationDomain; + + public function __construct(GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) { $this->groupManager = $groupManager; + $this->translator = $translator; + $this->translationDomain = $translationDomain; $this->contextStack = new \SplStack(); } @@ -53,13 +66,23 @@ class ExecutionContextManager extends AbstractVisitor implements ExecutionContex $this->validator = $validator; } - public function startContext() + public function startContext($root) { + if (null === $this->validator) { + // TODO error, call initialize() first + } + if (null !== $this->currentContext) { $this->contextStack->push($this->currentContext); } - $this->currentContext = new ExecutionContext($this->validator, $this->groupManager); + $this->currentContext = new LegacyExecutionContext( + $root, + $this->validator, + $this->groupManager, + $this->translator, + $this->translationDomain + ); return $this->currentContext; } @@ -100,7 +123,7 @@ class ExecutionContextManager extends AbstractVisitor implements ExecutionContex public function enterNode(Node $node) { if (null === $this->currentContext) { - // error no context started + // TODO error call startContext() first } $this->currentContext->pushNode($node); diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php index 0d79eb43bb..b805c12e43 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php @@ -18,9 +18,11 @@ namespace Symfony\Component\Validator\Context; interface ExecutionContextManagerInterface { /** + * @param mixed $root + * * @return ExecutionContextInterface The started context */ - public function startContext(); + public function startContext($root); /** * @return ExecutionContextInterface The stopped context diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index 1981e0f00e..8cf17f5ee3 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -11,7 +11,12 @@ namespace Symfony\Component\Validator\Context; +use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; +use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; /** * @since %%NextVersion%% @@ -19,23 +24,84 @@ use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionCont */ class LegacyExecutionContext extends ExecutionContext implements LegacyExecutionContextInterface { - public function addViolationAt($subPath, $message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) + public function __construct($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) { + if (!$validator instanceof LegacyValidatorInterface) { + throw new InvalidArgumentException( + 'The validator passed to LegacyExecutionContext must implement '. + '"Symfony\Component\Validator\ValidatorInterface".' + ); + } + parent::__construct($root, $validator, $groupManager, $translator, $translationDomain); + } + + /** + * {@inheritdoc} + */ + public function addViolation($message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) + { + if (func_num_args() >= 3) { + $this + ->buildViolation($message, $parameters) + ->setInvalidValue($invalidValue) + ->setPluralization($pluralization) + ->setCode($code) + ->addViolation() + ; + + return; + } + + parent::addViolation($message, $parameters); + } + + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) + { + if (func_num_args() >= 3) { + $this + ->buildViolation($message, $parameters) + ->atPath($subPath) + ->setInvalidValue($invalidValue) + ->setPluralization($pluralization) + ->setCode($code) + ->addViolation() + ; + + return; + } + + $this + ->buildViolation($message, $parameters) + ->atPath($subPath) + ->addViolation() + ; } public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false) { + // TODO handle $traverse and $deep + return $this + ->getValidator() + ->inContext($this) + ->atPath($subPath) + ->validateObject($value, $groups) + ; } public function validateValue($value, $constraints, $subPath = '', $groups = null) { - + return $this + ->getValidator() + ->inContext($this) + ->atPath($subPath) + ->validateValue($value, $constraints, $groups) + ; } public function getMetadataFactory() { - + return $this->getValidator()->getMetadataFactory(); } } diff --git a/src/Symfony/Component/Validator/Mapping/CascadingStrategy.php b/src/Symfony/Component/Validator/Mapping/CascadingStrategy.php new file mode 100644 index 0000000000..1218c2d484 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/CascadingStrategy.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class CascadingStrategy +{ + const NONE = 0; + + const CASCADE = 1; + + private function __construct() + { + } +} diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 8bba73a01f..16c42ccd08 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -26,7 +26,7 @@ use Symfony\Component\Validator\Exception\GroupDefinitionException; * @author Bernhard Schussek * @author Fabien Potencier */ -class ClassMetadata extends ElementMetadata implements MetadataInterface, ClassBasedInterface, PropertyMetadataContainerInterface +class ClassMetadata extends ElementMetadata implements MetadataInterface, ClassBasedInterface, PropertyMetadataContainerInterface, ClassMetadataInterface { /** * @var string @@ -63,6 +63,8 @@ class ClassMetadata extends ElementMetadata implements MetadataInterface, ClassB */ public $groupSequenceProvider = false; + public $traversalStrategy = TraversalStrategy::IMPLICIT; + /** * @var \ReflectionClass */ @@ -423,4 +425,19 @@ class ClassMetadata extends ElementMetadata implements MetadataInterface, ClassB { return $this->groupSequenceProvider; } + + /** + * Class nodes are never cascaded. + * + * @return Boolean Always returns false. + */ + public function getCascadingStrategy() + { + return CascadingStrategy::NONE; + } + + public function getTraversalStrategy() + { + return $this->traversalStrategy; + } } diff --git a/src/Symfony/Component/Validator/Mapping/ElementMetadata.php b/src/Symfony/Component/Validator/Mapping/ElementMetadata.php index 9dedb79fd9..cfe34e3985 100644 --- a/src/Symfony/Component/Validator/Mapping/ElementMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ElementMetadata.php @@ -13,7 +13,7 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraint; -abstract class ElementMetadata +abstract class ElementMetadata implements MetadataInterface { /** * @var Constraint[] diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index c30a87ee06..f3b3e3cf8f 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -12,20 +12,18 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\ValidationVisitorInterface; -use Symfony\Component\Validator\ClassBasedInterface; -use Symfony\Component\Validator\PropertyMetadataInterface; +use Symfony\Component\Validator\PropertyMetadataInterface as LegacyPropertyMetadataInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; -abstract class MemberMetadata extends ElementMetadata implements PropertyMetadataInterface, ClassBasedInterface +abstract class MemberMetadata extends ElementMetadata implements PropertyMetadataInterface, LegacyPropertyMetadataInterface { public $class; public $name; public $property; - public $cascaded = false; - public $collectionCascaded = false; - public $collectionCascadedDeeply = false; + public $cascadingStrategy = CascadingStrategy::NONE; + public $traversalStrategy = TraversalStrategy::IMPLICIT; private $reflMember = array(); /** @@ -64,10 +62,15 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat } if ($constraint instanceof Valid) { - $this->cascaded = true; - /* @var Valid $constraint */ - $this->collectionCascaded = $constraint->traverse; - $this->collectionCascadedDeeply = $constraint->deep; + $this->cascadingStrategy = CascadingStrategy::CASCADE; + + if ($constraint->traverse) { + $this->traversalStrategy = TraversalStrategy::TRAVERSE; + } + + if ($constraint->deep) { + $this->traversalStrategy |= TraversalStrategy::RECURSIVE; + } } else { parent::addConstraint($constraint); } @@ -86,9 +89,8 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat 'class', 'name', 'property', - 'cascaded', - 'collectionCascaded', - 'collectionCascadedDeeply', + 'cascadingStrategy', + 'traversalStrategy', )); } @@ -158,6 +160,16 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat return $this->getReflectionMember($objectOrClassName)->isPrivate(); } + public function getCascadingStrategy() + { + return $this->cascadingStrategy; + } + + public function getTraversalStrategy() + { + return $this->traversalStrategy; + } + /** * Returns whether objects stored in this member should be validated * @@ -165,7 +177,7 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat */ public function isCascaded() { - return $this->cascaded; + return $this->cascadingStrategy & CascadingStrategy::CASCADE; } /** @@ -176,7 +188,7 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat */ public function isCollectionCascaded() { - return $this->collectionCascaded; + return $this->traversalStrategy & TraversalStrategy::TRAVERSE; } /** @@ -187,7 +199,7 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat */ public function isCollectionCascadedDeeply() { - return $this->collectionCascadedDeeply; + return $this->traversalStrategy & TraversalStrategy::RECURSIVE; } /** diff --git a/src/Symfony/Component/Validator/Mapping/MetadataInterface.php b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php index 3df0d9bc0d..540c4c9d60 100644 --- a/src/Symfony/Component/Validator/Mapping/MetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php @@ -53,5 +53,7 @@ interface MetadataInterface */ public function findConstraints($group); - public function supportsCascading(); + public function getCascadingStrategy(); + + public function getTraversalStrategy(); } diff --git a/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php new file mode 100644 index 0000000000..4a9d8c8aa1 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class TraversalStrategy +{ + const IMPLICIT = 0; + + const NONE = 1; + + const TRAVERSE = 2; + + const RECURSIVE = 4; + + private function __construct() + { + } +} diff --git a/src/Symfony/Component/Validator/Mapping/ValueMetadata.php b/src/Symfony/Component/Validator/Mapping/ValueMetadata.php index c51a6fa575..230e5dd947 100644 --- a/src/Symfony/Component/Validator/Mapping/ValueMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ValueMetadata.php @@ -11,35 +11,48 @@ namespace Symfony\Component\Validator\Mapping; +use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\Exception\ValidatorException; + /** * @since %%NextVersion%% * @author Bernhard Schussek */ -class ValueMetadata implements MetadataInterface +class ValueMetadata extends ElementMetadata { - /** - * Returns all constraints for a given validation group. - * - * @param string $group The validation group. - * - * @return \Symfony\Component\Validator\Constraint[] A list of constraint instances. - */ - public function findConstraints($group) + public function __construct(array $constraints) + { + foreach ($constraints as $constraint) { + if ($constraint instanceof Valid) { + // Why can't the Valid constraint be executed directly? + // + // It cannot be executed like regular other constraints, because regular + // constraints are only executed *if they belong to the validated group*. + // The Valid constraint, on the other hand, is always executed and propagates + // the group to the cascaded object. The propagated group depends on + // + // * Whether a group sequence is currently being executed. Then the default + // group is propagated. + // + // * Otherwise the validated group is propagated. + + throw new ValidatorException(sprintf( + 'The constraint "%s" cannot be validated. Use the method '. + 'validate() instead.', + get_class($constraint) + )); + } + + $this->addConstraint($constraint); + } + } + + public function getCascadingStrategy() { } - public function supportsCascading() - { - - } - - public function supportsIteration() - { - - } - - public function supportsRecursiveIteration() + public function getTraversalStrategy() { } diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php index dfb06dbc03..d49bf81c77 100644 --- a/src/Symfony/Component/Validator/Node/ClassNode.php +++ b/src/Symfony/Component/Validator/Node/ClassNode.php @@ -24,7 +24,7 @@ class ClassNode extends Node */ public $metadata; - public function __construct($value, ClassMetadataInterface $metadata, $propertyPath, array $groups) + public function __construct($value, ClassMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) { if (!is_object($value)) { // error @@ -34,7 +34,8 @@ class ClassNode extends Node $value, $metadata, $propertyPath, - $groups + $groups, + $cascadedGroups ); } diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php index 3dead5623d..08b2e4da78 100644 --- a/src/Symfony/Component/Validator/Node/Node.php +++ b/src/Symfony/Component/Validator/Node/Node.php @@ -27,11 +27,14 @@ abstract class Node public $groups; - public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups) + public $cascadedGroups; + + public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) { $this->value = $value; $this->metadata = $metadata; $this->propertyPath = $propertyPath; $this->groups = $groups; + $this->cascadedGroups = $cascadedGroups; } } diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php index 9424acb59f..76cfcb3531 100644 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -24,13 +24,14 @@ class PropertyNode extends Node */ public $metadata; - public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups) + public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) { parent::__construct( $value, $metadata, $propertyPath, - $groups + $groups, + $cascadedGroups ); } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index 159f7fdd5a..ce1c6029b5 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -12,6 +12,9 @@ namespace Symfony\Component\Validator\NodeTraverser; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\NoSuchMetadataException; +use Symfony\Component\Validator\Mapping\CascadingStrategy; +use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; @@ -98,15 +101,25 @@ class NodeTraverser implements NodeTraverserInterface // Stop the traversal, but execute the leaveNode() methods anyway to // perform possible cleanups - if (!$stopTraversal && is_object($node->value) && $node->metadata->supportsCascading()) { - $classMetadata = $this->metadataFactory->getMetadataFor($node->value); + if (!$stopTraversal && null !== $node->value) { + $cascadingStrategy = $node->metadata->getCascadingStrategy(); + $traversalStrategy = $node->metadata->getTraversalStrategy(); - $this->traverseClassNode(new ClassNode( - $node->value, - $classMetadata, - $node->propertyPath, - $node->groups - )); + if (is_array($node->value)) { + $this->cascadeCollection( + $node->value, + $node->propertyPath, + $node->cascadedGroups, + $traversalStrategy + ); + } elseif ($cascadingStrategy & CascadingStrategy::CASCADE) { + $this->cascadeObject( + $node->value, + $node->propertyPath, + $node->cascadedGroups, + $traversalStrategy + ); + } } foreach ($this->visitors as $visitor) { @@ -114,7 +127,7 @@ class NodeTraverser implements NodeTraverserInterface } } - private function traverseClassNode(ClassNode $node) + private function traverseClassNode(ClassNode $node, $traversalStrategy = TraversalStrategy::IMPLICIT) { $stopTraversal = false; @@ -135,14 +148,89 @@ class NodeTraverser implements NodeTraverserInterface $node->propertyPath ? $node->propertyPath.'.'.$propertyName : $propertyName, - $node->groups + $node->groups, + $node->cascadedGroups )); } } + + if ($traversalStrategy & TraversalStrategy::IMPLICIT) { + $traversalStrategy = $node->metadata->getTraversalStrategy(); + } + + if ($traversalStrategy & TraversalStrategy::TRAVERSE) { + $this->cascadeCollection( + $node->value, + $node->propertyPath, + $node->groups, + $traversalStrategy + ); + } } foreach ($this->visitors as $visitor) { $visitor->leaveNode($node); } } + + private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy) + { + try { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + $classNode = new ClassNode( + $object, + $classMetadata, + $propertyPath, + $groups, + $groups + ); + + $this->traverseClassNode($classNode, $traversalStrategy); + } catch (NoSuchMetadataException $e) { + if (!$object instanceof \Traversable || !($traversalStrategy & TraversalStrategy::TRAVERSE)) { + throw $e; + } + + // Metadata doesn't necessarily have to exist for + // traversable objects, because we know how to validate + // them anyway. + $this->cascadeCollection( + $object, + $propertyPath, + $groups, + $traversalStrategy + ); + } + } + + private function cascadeCollection($collection, $propertyPath, array $groups, $traversalStrategy) + { + if (!($traversalStrategy & TraversalStrategy::RECURSIVE)) { + $traversalStrategy = TraversalStrategy::IMPLICIT; + } + + foreach ($collection as $key => $value) { + if (is_array($value)) { + $this->cascadeCollection( + $value, + $propertyPath.'['.$key.']', + $groups, + $traversalStrategy + ); + + continue; + } + + // Scalar and null values in the collection are ignored + if (is_object($value)) { + $this->cascadeObject( + $value, + $propertyPath.'['.$key.']', + $groups, + $traversalStrategy + ); + } + } + } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php index 8b9741cecc..047f2ad60e 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php +++ b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\NodeVisitor; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; @@ -31,7 +32,11 @@ class GroupSequenceResolver extends AbstractVisitor $groupSequence = $node->metadata->getGroupSequence(); } elseif ($node->metadata->isGroupSequenceProvider()) { /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $groupSequence = $value->getGroupSequence(); + $groupSequence = $node->value->getGroupSequence(); + + if (!$groupSequence instanceof GroupSequence) { + $groupSequence = new GroupSequence($groupSequence); + } } else { return; } @@ -43,7 +48,7 @@ class GroupSequenceResolver extends AbstractVisitor $node->groups[$key] = $groupSequence; // Cascade the "Default" group when validating the sequence - $node->groups[$key]->cascadedGroup = Constraint::DEFAULT_GROUP; + $groupSequence->cascadedGroup = Constraint::DEFAULT_GROUP; } } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php index 12fad3cf0e..662262a4a5 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php @@ -75,7 +75,7 @@ class NodeValidator extends AbstractVisitor implements GroupManagerInterface if ($node instanceof ClassNode) { $objectHash = spl_object_hash($node->value); $this->objectHashStack->push($objectHash); - } elseif ($node instanceof PropertyNode) { + } elseif ($node instanceof PropertyNode && count($this->objectHashStack) > 0) { $objectHash = $this->objectHashStack->top(); } else { $objectHash = null; @@ -112,10 +112,8 @@ class NodeValidator extends AbstractVisitor implements GroupManagerInterface continue; } - // Only traverse group sequences at class, not at property level - if (!$node instanceof ClassNode) { - continue; - } + // Skip the group sequence when validating properties + unset($node->groups[$key]); // Traverse group sequence until a violation is generated $this->traverseGroupSequence($node, $group); @@ -130,24 +128,29 @@ class NodeValidator extends AbstractVisitor implements GroupManagerInterface return true; } + public function leaveNode(Node $node) + { + if ($node instanceof ClassNode) { + $this->objectHashStack->pop(); + } + } + public function getCurrentGroup() { return $this->currentGroup; } - private function traverseGroupSequence(ClassNode $node, GroupSequence $groupSequence) + private function traverseGroupSequence(Node $node, GroupSequence $groupSequence) { $context = $this->contextManager->getCurrentContext(); $violationCount = count($context->getViolations()); foreach ($groupSequence->groups as $groupInSequence) { - $this->nodeTraverser->traverse(array(new ClassNode( - $node->value, - $node->metadata, - $node->propertyPath, - array($groupInSequence), - array($groupSequence->cascadedGroup ?: $groupInSequence) - ))); + $node = clone $node; + $node->groups = array($groupInSequence); + $node->cascadedGroups = array($groupSequence->cascadedGroup ?: $groupInSequence); + + $this->nodeTraverser->traverse(array($node)); // Abort sequence validation if a violation was generated if (count($context->getViolations()) > $violationCount) { diff --git a/src/Symfony/Component/Validator/Tests/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php similarity index 99% rename from src/Symfony/Component/Validator/Tests/AbstractValidatorTest.php rename to src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index 33e39c5345..c314881320 100644 --- a/src/Symfony/Component/Validator/Tests/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Validator\Tests; +namespace Symfony\Component\Validator\Tests\Validator; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Callback; @@ -788,7 +788,6 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase $test->assertNull($context->getPropertyName()); $test->assertSame('', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); - $test->assertNull($context->getMetadata()); $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame('Bernhard', $context->getRoot()); $test->assertSame('Bernhard', $context->getValue()); @@ -942,8 +941,6 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase public function testNoDuplicateValidationIfConstraintInMultipleGroups() { - $this->markTestSkipped('Currently not supported'); - $entity = new Entity(); $callback = function ($value, ExecutionContextInterface $context) { @@ -963,8 +960,6 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase public function testGroupSequenceAbortsAfterFailedGroup() { - $this->markTestSkipped('Currently not supported'); - $entity = new Entity(); $callback1 = function ($value, ExecutionContextInterface $context) { @@ -997,8 +992,6 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase public function testGroupSequenceIncludesReferences() { - $this->markTestSkipped('Currently not supported'); - $entity = new Entity(); $entity->reference = new Reference(); diff --git a/src/Symfony/Component/Validator/Tests/ValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php similarity index 55% rename from src/Symfony/Component/Validator/Tests/ValidatorTest.php rename to src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php index 52bdbea519..327194d751 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php @@ -9,17 +9,32 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Validator\Tests; +namespace Symfony\Component\Validator\Tests\Validator; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Validator; use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; -class ValidatorTest extends AbstractValidatorTest +class LegacyValidatorTest extends AbstractValidatorTest { protected function createValidator(MetadataFactoryInterface $metadataFactory) { return new Validator($metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); } + + public function testNoDuplicateValidationIfConstraintInMultipleGroups() + { + $this->markTestSkipped('Currently not supported'); + } + + public function testGroupSequenceAbortsAfterFailedGroup() + { + $this->markTestSkipped('Currently not supported'); + } + + public function testGroupSequenceIncludesReferences() + { + $this->markTestSkipped('Currently not supported'); + } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php similarity index 78% rename from src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php rename to src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php index 968fc7a79a..f990f10e04 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php @@ -15,19 +15,20 @@ use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\ExecutionContextManager; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; use Symfony\Component\Validator\NodeVisitor\NodeValidator; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; -use Symfony\Component\Validator\Tests\AbstractValidatorTest; -use Symfony\Component\Validator\Validator\Validator; +use Symfony\Component\Validator\Validator\LegacyValidator; -class TraversingValidatorTest extends AbstractValidatorTest +class ValidatorTest extends AbstractValidatorTest { protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); $contextManager = new ExecutionContextManager($nodeValidator, new DefaultTranslator()); - $validator = new Validator($nodeTraverser, $metadataFactory, $contextManager); + $validator = new LegacyValidator($nodeTraverser, $metadataFactory, $contextManager); + $groupSequenceResolver = new GroupSequenceResolver(); // The context manager needs the validator for passing it to created // contexts @@ -37,6 +38,7 @@ class TraversingValidatorTest extends AbstractValidatorTest // context to the constraint validators $nodeValidator->initialize($contextManager); + $nodeTraverser->addVisitor($groupSequenceResolver); $nodeTraverser->addVisitor($contextManager); $nodeTraverser->addVisitor($nodeValidator); diff --git a/src/Symfony/Component/Validator/Util/PropertyPath.php b/src/Symfony/Component/Validator/Util/PropertyPath.php new file mode 100644 index 0000000000..bf33b50b5e --- /dev/null +++ b/src/Symfony/Component/Validator/Util/PropertyPath.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Util; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class PropertyPath +{ + public static function append($basePath, $subPath) + { + if ('' !== (string) $subPath) { + if ('[' === $subPath{1}) { + return $basePath.$subPath; + } + + return $basePath ? $basePath.'.'.$subPath : $subPath; + } + + return $basePath; + } + + private function __construct() + { + } +} diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php index 5401fc6dbe..69f48ca7d0 100644 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -13,6 +13,8 @@ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\ValueMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; @@ -20,6 +22,7 @@ use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\Node\ValueNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; +use Symfony\Component\Validator\Util\PropertyPath; /** * @since %%NextVersion%% @@ -75,15 +78,22 @@ abstract class AbstractValidator implements ValidatorInterface $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { - // error + 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) + )); } + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $this->nodeTraverser->traverse(array(new ClassNode( $object, $classMetadata, $this->defaultPropertyPath, - // TODO use cascade group here - $groups ? $this->normalizeGroups($groups) : $this->defaultGroups + $groups, + $groups ))); } @@ -92,7 +102,12 @@ abstract class AbstractValidator implements ValidatorInterface $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { - // error + 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); @@ -105,7 +120,8 @@ abstract class AbstractValidator implements ValidatorInterface $nodes[] = new PropertyNode( $propertyValue, $propertyMetadata, - $this->defaultPropertyPath, + PropertyPath::append($this->defaultPropertyPath, $propertyName), + $groups, $groups ); } @@ -118,7 +134,12 @@ abstract class AbstractValidator implements ValidatorInterface $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { - // error + 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); @@ -129,7 +150,8 @@ abstract class AbstractValidator implements ValidatorInterface $nodes[] = new PropertyNode( $value, $propertyMetadata, - $this->defaultPropertyPath, + PropertyPath::append($this->defaultPropertyPath, $propertyName), + $groups, $groups ); } @@ -139,13 +161,19 @@ abstract class AbstractValidator implements ValidatorInterface protected function traverseValue($value, $constraints, $groups = null) { + if (!is_array($constraints)) { + $constraints = array($constraints); + } + $metadata = new ValueMetadata($constraints); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; $this->nodeTraverser->traverse(array(new ValueNode( $value, $metadata, $this->defaultPropertyPath, - $groups ? $this->normalizeGroups($groups) : $this->defaultGroups + $groups, + $groups ))); } diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index ed222d57c8..0c0c38880b 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -35,7 +35,7 @@ class Validator extends AbstractValidator public function validateObject($object, $groups = null) { - $this->contextManager->startContext(); + $this->contextManager->startContext($object); $this->traverseObject($object, $groups); @@ -44,7 +44,7 @@ class Validator extends AbstractValidator public function validateProperty($object, $propertyName, $groups = null) { - $this->contextManager->startContext(); + $this->contextManager->startContext($object); $this->traverseProperty($object, $propertyName, $groups); @@ -53,7 +53,7 @@ class Validator extends AbstractValidator public function validatePropertyValue($object, $propertyName, $value, $groups = null) { - $this->contextManager->startContext(); + $this->contextManager->startContext($object); $this->traversePropertyValue($object, $propertyName, $value, $groups); @@ -62,7 +62,7 @@ class Validator extends AbstractValidator public function validateValue($value, $constraints, $groups = null) { - $this->contextManager->startContext(); + $this->contextManager->startContext($value); $this->traverseValue($value, $constraints, $groups); diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index f02ed79d9b..7985a1f9ef 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -35,6 +35,8 @@ interface ValidatorInterface */ public function validateObject($object, $groups = null); +// public function validateCollection($collection, $groups = null); + /** * Validates a property of a value against its current value. * diff --git a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php new file mode 100644 index 0000000000..5fb8488e80 --- /dev/null +++ b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Violation; + +use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\Util\PropertyPath; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class ConstraintViolationBuilder implements ConstraintViolationBuilderInterface +{ + private $violations; + + private $message; + + private $parameters; + + private $root; + + private $invalidValue; + + private $propertyPath; + + private $translator; + + private $translationDomain; + + private $pluralization; + + private $code; + + public function __construct(ConstraintViolationList $violations, $message, array $parameters, $root, $propertyPath, $invalidValue, TranslatorInterface $translator, $translationDomain = null) + { + $this->violations = $violations; + $this->message = $message; + $this->parameters = $parameters; + $this->root = $root; + $this->propertyPath = $propertyPath; + $this->invalidValue = $invalidValue; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + } + + public function atPath($subPath) + { + $this->propertyPath = PropertyPath::append($this->propertyPath, $subPath); + + return $this; + } + + public function setParameter($key, $value) + { + $this->parameters[$key] = $value; + + return $this; + } + + public function setParameters(array $parameters) + { + $this->parameters = $parameters; + + return $this; + } + + public function setTranslationDomain($translationDomain) + { + $this->translationDomain = $translationDomain; + + return $this; + } + + public function setInvalidValue($invalidValue) + { + $this->invalidValue = $invalidValue; + + return $this; + } + + public function setPluralization($pluralization) + { + $this->pluralization = $pluralization; + + return $this; + } + + public function setCode($code) + { + $this->code = $code; + + return $this; + } + + public function addViolation() + { + if (null === $this->pluralization) { + $translatedMessage = $this->translator->trans( + $this->message, + $this->parameters, + $this->translationDomain + ); + } else { + try { + $translatedMessage = $this->translator->transChoice( + $this->message, + $this->pluralization, + $this->parameters, + $this->translationDomain# + ); + } catch (\InvalidArgumentException $e) { + $translatedMessage = $this->translator->trans( + $this->message, + $this->parameters, + $this->translationDomain + ); + } + } + + $this->violations->add(new ConstraintViolation( + $translatedMessage, + $this->message, + $this->parameters, + $this->root, + $this->propertyPath, + $this->invalidValue, + $this->pluralization, + $this->code + )); + } +} diff --git a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php new file mode 100644 index 0000000000..9d62c3ccb5 --- /dev/null +++ b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Violation; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface ConstraintViolationBuilderInterface +{ + public function atPath($subPath); + + public function setParameter($key, $value); + + public function setParameters(array $parameters); + + public function setTranslationDomain($translationDomain); + + public function setInvalidValue($invalidValue); + + public function setPluralization($pluralization); + + public function setCode($code); + + public function addViolation(); +}