From 23534ca6ab4a107a5e801e710fb3c07b1a8874d9 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Sat, 22 Feb 2014 11:43:44 +0100 Subject: [PATCH] [Validator] Added a recursive clone of the new implementation for speed comparison --- .../Validator/Context/ExecutionContext.php | 89 ++- .../Context/ExecutionContextFactory.php | 12 +- .../Context/ExecutionContextInterface.php | 37 +- .../Context/LegacyExecutionContext.php | 3 +- .../Context/LegacyExecutionContextFactory.php | 12 +- .../Validator/Group/GroupManagerInterface.php | 29 - .../NodeVisitor/ContextUpdateVisitor.php | 4 +- .../NodeVisitor/NodeValidationVisitor.php | 24 +- .../Tests/Context/ExecutionContextTest.php | 69 -- .../Validator/LegacyValidator2Dot5ApiTest.php | 13 +- .../LegacyValidatorLegacyApiTest.php | 13 +- .../RecursiveValidator2Dot5ApiTest.php | 33 + ...hp => TraversingValidator2Dot5ApiTest.php} | 11 +- .../Validator/Tests/ValidatorBuilderTest.php | 2 +- src/Symfony/Component/Validator/Validator.php | 2 +- .../Validator/Validator/LegacyValidator.php | 2 +- .../RecursiveContextualValidator.php | 700 ++++++++++++++++++ .../Validator/RecursiveValidator.php | 124 ++++ ....php => TraversingContextualValidator.php} | 2 +- ...{Validator.php => TraversingValidator.php} | 6 +- .../Component/Validator/ValidatorBuilder.php | 42 +- 21 files changed, 1004 insertions(+), 225 deletions(-) delete mode 100644 src/Symfony/Component/Validator/Group/GroupManagerInterface.php delete mode 100644 src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php rename src/Symfony/Component/Validator/Tests/Validator/{Validator2Dot5ApiTest.php => TraversingValidator2Dot5ApiTest.php} (82%) create mode 100644 src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php create mode 100644 src/Symfony/Component/Validator/Validator/RecursiveValidator.php rename src/Symfony/Component/Validator/Validator/{ContextualValidator.php => TraversingContextualValidator.php} (98%) rename src/Symfony/Component/Validator/Validator/{Validator.php => TraversingValidator.php} (95%) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index e4db8a75b3..959f44ea79 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -17,6 +17,7 @@ use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Exception\BadMethodCallException; use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Util\PropertyPath; @@ -48,11 +49,6 @@ class ExecutionContext implements ExecutionContextInterface */ private $root; - /** - * @var GroupManagerInterface - */ - private $groupManager; - /** * @var TranslatorInterface */ @@ -71,11 +67,32 @@ class ExecutionContext implements ExecutionContextInterface private $violations; /** - * The current node under validation. + * The currently validated value. * - * @var Node + * @var mixed */ - private $node; + private $value; + + /** + * The property path leading to the current value. + * + * @var string + */ + private $propertyPath = ''; + + /** + * The current validation metadata. + * + * @var MetadataInterface + */ + private $metadata; + + /** + * The currently validated group. + * + * @var string|null + */ + private $group; /** * Stores which objects have been validated in which group. @@ -104,9 +121,6 @@ class ExecutionContext implements ExecutionContextInterface * @param ValidatorInterface $validator The validator * @param mixed $root The root value of the * validated object graph - * @param GroupManagerInterface $groupManager The manager for accessing - * the currently validated - * group * @param TranslatorInterface $translator The translator * @param string|null $translationDomain The translation domain to * use for translating @@ -115,24 +129,45 @@ class ExecutionContext implements ExecutionContextInterface * @internal Called by {@link ExecutionContextFactory}. Should not be used * in user code. */ - public function __construct(ValidatorInterface $validator, $root, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) + public function __construct(ValidatorInterface $validator, $root, TranslatorInterface $translator, $translationDomain = null) { $this->validator = $validator; $this->root = $root; - $this->groupManager = $groupManager; $this->translator = $translator; $this->translationDomain = $translationDomain; $this->violations = new ConstraintViolationList(); } /** - * Sets the values of the context to match the given node. - * - * @param Node $node The currently validated node + * {@inheritdoc} */ - public function setCurrentNode(Node $node) + public function setValue($value) { - $this->node = $node; + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function setMetadata(MetadataInterface $metadata = null) + { + $this->metadata = $metadata; + } + + /** + * {@inheritdoc} + */ + public function setPropertyPath($propertyPath) + { + $this->propertyPath = (string) $propertyPath; + } + + /** + * {@inheritdoc} + */ + public function setGroup($group) + { + $this->group = $group; } /** @@ -209,7 +244,7 @@ class ExecutionContext implements ExecutionContextInterface */ public function getValue() { - return $this->node ? $this->node->value : null; + return $this->value; } /** @@ -217,7 +252,7 @@ class ExecutionContext implements ExecutionContextInterface */ public function getMetadata() { - return $this->node ? $this->node->metadata : null; + return $this->metadata; } /** @@ -225,7 +260,7 @@ class ExecutionContext implements ExecutionContextInterface */ public function getGroup() { - return $this->groupManager->getCurrentGroup(); + return $this->group; } /** @@ -233,9 +268,7 @@ class ExecutionContext implements ExecutionContextInterface */ public function getClassName() { - $metadata = $this->getMetadata(); - - return $metadata instanceof ClassBasedInterface ? $metadata->getClassName() : null; + return $this->metadata instanceof ClassBasedInterface ? $this->metadata->getClassName() : null; } /** @@ -243,9 +276,7 @@ class ExecutionContext implements ExecutionContextInterface */ public function getPropertyName() { - $metadata = $this->getMetadata(); - - return $metadata instanceof PropertyMetadataInterface ? $metadata->getPropertyName() : null; + return $this->metadata instanceof PropertyMetadataInterface ? $this->metadata->getPropertyName() : null; } /** @@ -253,9 +284,7 @@ class ExecutionContext implements ExecutionContextInterface */ public function getPropertyPath($subPath = '') { - $propertyPath = $this->node ? $this->node->propertyPath : ''; - - return PropertyPath::append($propertyPath, $subPath); + return PropertyPath::append($this->propertyPath, $subPath); } /** diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php index 4553c8b126..5e660f47b0 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php @@ -26,11 +26,6 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; */ class ExecutionContextFactory implements ExecutionContextFactoryInterface { - /** - * @var GroupManagerInterface - */ - private $groupManager; - /** * @var TranslatorInterface */ @@ -44,17 +39,13 @@ class ExecutionContextFactory implements ExecutionContextFactoryInterface /** * Creates a new context factory. * - * @param GroupManagerInterface $groupManager The manager for accessing - * the currently validated - * group * @param TranslatorInterface $translator The translator * @param string|null $translationDomain The translation domain to * use for translating * violation messages */ - public function __construct(GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) + public function __construct(TranslatorInterface $translator, $translationDomain = null) { - $this->groupManager = $groupManager; $this->translator = $translator; $this->translationDomain = $translationDomain; } @@ -67,7 +58,6 @@ class ExecutionContextFactory implements ExecutionContextFactoryInterface return new ExecutionContext( $validator, $root, - $this->groupManager, $this->translator, $this->translationDomain ); diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index b955a34f27..241dc03107 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Context; use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; +use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; @@ -100,14 +101,44 @@ interface ExecutionContextInterface extends LegacyExecutionContextInterface public function getValidator(); /** - * Sets the currently traversed node. + * Sets the currently validated value. * - * @param Node $node The current node + * @param mixed $value The validated value * * @internal Used by the validator engine. Should not be called by user * code. */ - public function setCurrentNode(Node $node); + public function setValue($value); + + /** + * Sets the current validation metadata. + * + * @param MetadataInterface $metadata The validation metadata + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function setMetadata(MetadataInterface $metadata = null); + + /** + * Sets the property path leading to the current value. + * + * @param string $propertyPath The property path to the current value + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function setPropertyPath($propertyPath); + + /** + * Sets the currently validated group. + * + * @param string|null $group The validated group + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function setGroup($group); /** * Marks an object as validated in a specific validation group. diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index ea238f9bcd..abac3e9048 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -42,7 +42,7 @@ class LegacyExecutionContext extends ExecutionContext * @internal Called by {@link LegacyExecutionContextFactory}. Should not be used * in user code. */ - public function __construct(ValidatorInterface $validator, $root, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) + public function __construct(ValidatorInterface $validator, $root, TranslatorInterface $translator, $translationDomain = null) { if (!$validator instanceof LegacyValidatorInterface) { throw new InvalidArgumentException( @@ -54,7 +54,6 @@ class LegacyExecutionContext extends ExecutionContext parent::__construct( $validator, $root, - $groupManager, $translator, $translationDomain ); diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php index 7f19bb204c..88d3c0ff5d 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php @@ -26,11 +26,6 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; */ class LegacyExecutionContextFactory implements ExecutionContextFactoryInterface { - /** - * @var GroupManagerInterface - */ - private $groupManager; - /** * @var TranslatorInterface */ @@ -44,17 +39,13 @@ class LegacyExecutionContextFactory implements ExecutionContextFactoryInterface /** * Creates a new context factory. * - * @param GroupManagerInterface $groupManager The manager for accessing - * the currently validated - * group * @param TranslatorInterface $translator The translator * @param string|null $translationDomain The translation domain to * use for translating * violation messages */ - public function __construct(GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) + public function __construct(TranslatorInterface $translator, $translationDomain = null) { - $this->groupManager = $groupManager; $this->translator = $translator; $this->translationDomain = $translationDomain; } @@ -67,7 +58,6 @@ class LegacyExecutionContextFactory implements ExecutionContextFactoryInterface return new LegacyExecutionContext( $validator, $root, - $this->groupManager, $this->translator, $this->translationDomain ); diff --git a/src/Symfony/Component/Validator/Group/GroupManagerInterface.php b/src/Symfony/Component/Validator/Group/GroupManagerInterface.php deleted file mode 100644 index a231b907d5..0000000000 --- a/src/Symfony/Component/Validator/Group/GroupManagerInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Group; - -/** - * Returns the group that is currently being validated. - * - * @since 2.5 - * @author Bernhard Schussek - */ -interface GroupManagerInterface -{ - /** - * Returns the group that is currently being validated. - * - * @return string|null The current group or null, if no validation is - * active. - */ - public function getCurrentGroup(); -} diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php index ecf0b2694c..03243960b1 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php @@ -30,6 +30,8 @@ class ContextUpdateVisitor extends AbstractVisitor */ public function visit(Node $node, ExecutionContextInterface $context) { - $context->setCurrentNode($node); + $context->setValue($node->value); + $context->setMetadata($node->metadata); + $context->setPropertyPath($node->propertyPath); } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index cc6f578078..9d18c7ef48 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -14,7 +14,6 @@ namespace Symfony\Component\Validator\NodeVisitor; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\CollectionNode; use Symfony\Component\Validator\Node\Node; @@ -27,7 +26,7 @@ use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; * @since 2.5 * @author Bernhard Schussek */ -class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInterface +class NodeValidationVisitor extends AbstractVisitor { /** * @var ConstraintValidatorFactoryInterface @@ -39,13 +38,6 @@ class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInter */ private $nodeTraverser; - /** - * The currently validated group. - * - * @var string - */ - private $currentGroup; - /** * Creates a new visitor. * @@ -128,14 +120,6 @@ class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInter return true; } - /** - * {@inheritdoc} - */ - public function getCurrentGroup() - { - return $this->currentGroup; - } - /** * Validates a node's value in each group of a group sequence. * @@ -181,7 +165,7 @@ class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInter private function validateNodeForGroup(Node $node, $group, ExecutionContextInterface $context, $objectHash) { try { - $this->currentGroup = $group; + $context->setGroup($group); foreach ($node->metadata->findConstraints($group) as $constraint) { // Prevent duplicate validation of constraints, in the case @@ -211,10 +195,10 @@ class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInter $validator->validate($node->value, $constraint); } - $this->currentGroup = null; + $context->setGroup(null); } catch (\Exception $e) { // Should be put into a finally block once we switch to PHP 5.5 - $this->currentGroup = null; + $context->setGroup(null); throw $e; } diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php deleted file mode 100644 index 0f6e51f2ff..0000000000 --- a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php +++ /dev/null @@ -1,69 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Validator\Tests\Context; - -use Symfony\Component\Validator\Context\ExecutionContext; - -/** - * @since 2.5 - * @author Bernhard Schussek - */ -class ExecutionContextTest extends \PHPUnit_Framework_TestCase -{ - const ROOT = '__ROOT__'; - - const TRANSLATION_DOMAIN = '__TRANSLATION_DOMAIN__'; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $validator; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $groupManager; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $translator; - - /** - * @var ExecutionContext - */ - private $context; - - protected function setUp() - { - if (version_compare(PHP_VERSION, '5.3.9', '<')) { - $this->markTestSkipped('Not supported prior to PHP 5.3.9'); - } - - $this->validator = $this->getMock('Symfony\Component\Validator\Validator\ValidatorInterface'); - $this->groupManager = $this->getMock('Symfony\Component\Validator\Group\GroupManagerInterface'); - $this->translator = $this->getMock('Symfony\Component\Translation\TranslatorInterface'); - - $this->context = new ExecutionContext( - $this->validator, self::ROOT, $this->groupManager, $this->translator, self::TRANSLATION_DOMAIN - ); - } - - public function testGetGroup() - { - $this->groupManager->expects($this->once()) - ->method('getCurrentGroup') - ->will($this->returnValue('Current Group')); - - $this->assertSame('Current Group', $this->context->getGroup()); - } -} diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index c9a8fb509d..3ff580b110 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -34,17 +34,8 @@ class LegacyValidator2Dot5ApiTest extends Abstract2Dot5ApiTest protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); - $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); - $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); - $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new DefaultGroupReplacingVisitor(); - $contextRefresher = new ContextUpdateVisitor(); + $contextFactory = new LegacyExecutionContextFactory(new DefaultTranslator()); - $nodeTraverser->addVisitor($groupSequenceResolver); - $nodeTraverser->addVisitor($contextRefresher); - $nodeTraverser->addVisitor($nodeValidator); - - return $validator; + return new LegacyValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory()); } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index 8ba44f697d..493fa7a616 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -34,17 +34,8 @@ class LegacyValidatorLegacyApiTest extends AbstractLegacyApiTest protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); - $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); - $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); - $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new DefaultGroupReplacingVisitor(); - $contextRefresher = new ContextUpdateVisitor(); + $contextFactory = new LegacyExecutionContextFactory(new DefaultTranslator()); - $nodeTraverser->addVisitor($groupSequenceResolver); - $nodeTraverser->addVisitor($contextRefresher); - $nodeTraverser->addVisitor($nodeValidator); - - return $validator; + return new LegacyValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory()); } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php new file mode 100644 index 0000000000..57134410dc --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Validator; + +use Symfony\Component\Validator\DefaultTranslator; +use Symfony\Component\Validator\ConstraintValidatorFactory; +use Symfony\Component\Validator\Context\ExecutionContextFactory; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; +use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; +use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; +use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; +use Symfony\Component\Validator\Validator\RecursiveValidator; +use Symfony\Component\Validator\Validator\TraversingValidator; + +class RecursiveValidator2Dot5ApiTest extends Abstract2Dot5ApiTest +{ + protected function createValidator(MetadataFactoryInterface $metadataFactory) + { + $contextFactory = new ExecutionContextFactory(new DefaultTranslator()); + + return new RecursiveValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory()); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php similarity index 82% rename from src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php rename to src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php index fb6f6317c8..61e78456b3 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php @@ -19,18 +19,19 @@ use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; -use Symfony\Component\Validator\Validator\Validator; +use Symfony\Component\Validator\Validator\TraversingValidator; -class Validator2Dot5ApiTest extends Abstract2Dot5ApiTest +class TraversingValidator2Dot5ApiTest extends Abstract2Dot5ApiTest { protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); - $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); - $contextFactory = new ExecutionContextFactory($nodeValidator, new DefaultTranslator()); - $validator = new Validator($contextFactory, $nodeTraverser, $metadataFactory); + $contextFactory = new ExecutionContextFactory(new DefaultTranslator()); + $validator = new TraversingValidator($contextFactory, $nodeTraverser, $metadataFactory); + $groupSequenceResolver = new DefaultGroupReplacingVisitor(); $contextRefresher = new ContextUpdateVisitor(); + $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $nodeTraverser->addVisitor($groupSequenceResolver); $nodeTraverser->addVisitor($contextRefresher); diff --git a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php index fe45c56c07..5cd1198654 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php @@ -130,7 +130,7 @@ class ValidatorBuilderTest extends \PHPUnit_Framework_TestCase public function testSetApiVersion25() { $this->assertSame($this->builder, $this->builder->setApiVersion(Validation::API_VERSION_2_5)); - $this->assertInstanceOf('Symfony\Component\Validator\Validator\Validator', $this->builder->getValidator()); + $this->assertInstanceOf('Symfony\Component\Validator\Validator\TraversingValidator', $this->builder->getValidator()); } public function testSetApiVersion24And25() diff --git a/src/Symfony/Component/Validator/Validator.php b/src/Symfony/Component/Validator/Validator.php index e80157db81..31dd4b9c38 100644 --- a/src/Symfony/Component/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator.php @@ -22,7 +22,7 @@ use Symfony\Component\Translation\TranslatorInterface; * @author Bernhard Schussek * * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. - * Use {@link Validator\Validator} instead. + * Use {@link Validator\TraversingValidator} instead. */ class Validator implements ValidatorInterface { diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index eae4746e88..8f93b67a89 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -28,7 +28,7 @@ use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; * @deprecated Implemented for backwards compatibility with Symfony < 2.5. * To be removed in Symfony 3.0. */ -class LegacyValidator extends Validator implements LegacyValidatorInterface +class LegacyValidator extends RecursiveValidator implements LegacyValidatorInterface { public function validate($value, $groups = null, $traverse = false, $deep = false) { diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php new file mode 100644 index 0000000000..df7901f60f --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -0,0 +1,700 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Validator; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\NoSuchMetadataException; +use Symfony\Component\Validator\Exception\UnsupportedMetadataException; +use Symfony\Component\Validator\Exception\ValidatorException; +use Symfony\Component\Validator\Mapping\CascadingStrategy; +use Symfony\Component\Validator\Mapping\ClassMetadataInterface; +use Symfony\Component\Validator\Mapping\GenericMetadata; +use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; +use Symfony\Component\Validator\Mapping\TraversalStrategy; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\CollectionNode; +use Symfony\Component\Validator\Node\GenericNode; +use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\Node\PropertyNode; +use Symfony\Component\Validator\Util\PropertyPath; + +/** + * Default implementation of {@link ContextualValidatorInterface}. + * + * @since 2.5 + * @author Bernhard Schussek + */ +class RecursiveContextualValidator implements ContextualValidatorInterface +{ + /** + * @var ExecutionContextInterface + */ + private $context; + + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + + private $validatorFactory; + + private $currentGroup; + + /** + * Creates a validator for the given context. + * + * @param ExecutionContextInterface $context The execution context + * @param MetadataFactoryInterface $metadataFactory The factory for fetching + * the metadata of validated + * objects + */ + public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory) + { + $this->context = $context; + $this->defaultPropertyPath = $context->getPropertyPath(); + $this->defaultGroups = array($context->getGroup() ?: Constraint::DEFAULT_GROUP); + $this->metadataFactory = $metadataFactory; + $this->validatorFactory = $validatorFactory; + } + + /** + * {@inheritdoc} + */ + public function atPath($path) + { + $this->defaultPropertyPath = $this->context->getPropertyPath($path); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate($value, $constraints = null, $groups = null) + { + if (null === $constraints) { + $constraints = array(new Valid()); + } elseif (!is_array($constraints)) { + $constraints = array($constraints); + } + + $metadata = new GenericMetadata(); + $metadata->addConstraints($constraints); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + $this->traverseGenericNode(new GenericNode( + $value, + $metadata, + $this->defaultPropertyPath, + $groups + ), $this->context); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validateProperty($object, $propertyName, $groups = null) + { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + throw new ValidatorException(sprintf( + 'The metadata factory should return instances of '. + '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) + )); + } + + $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + foreach ($propertyMetadatas as $propertyMetadata) { + $propertyValue = $propertyMetadata->getPropertyValue($object); + + $this->traverseGenericNode(new PropertyNode( + $object, + $propertyValue, + $propertyMetadata, + PropertyPath::append($this->defaultPropertyPath, $propertyName), + $groups + ), $this->context); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validatePropertyValue($object, $propertyName, $value, $groups = null) + { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + throw new ValidatorException(sprintf( + 'The metadata factory should return instances of '. + '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) + )); + } + + $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + foreach ($propertyMetadatas as $propertyMetadata) { + $this->traverseGenericNode(new PropertyNode( + $object, + $value, + $propertyMetadata, + PropertyPath::append($this->defaultPropertyPath, $propertyName), + $groups, + $groups + ), $this->context); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getViolations() + { + return $this->context->getViolations(); + } + + /** + * Normalizes the given group or list of groups to an array. + * + * @param mixed $groups The groups to normalize + * + * @return array A group array + */ + protected function normalizeGroups($groups) + { + if (is_array($groups)) { + return $groups; + } + + return array($groups); + } + + /** + * Traverses a class node. + * + * At first, each visitor is invoked for this node. Then, unless any + * of the visitors aborts the traversal by returning false, a property + * node is put on the node stack for each constrained property of the class. + * At last, if the class is traversable and should be traversed according + * to the selected traversal strategy, a new collection node is put on the + * stack. + * + * @param ClassNode $node The class node + * @param ExecutionContextInterface $context The current execution context + * + * @throws UnsupportedMetadataException If a property metadata does not + * implement {@link PropertyMetadataInterface} + * + * @see ClassNode + * @see PropertyNode + * @see CollectionNode + * @see TraversalStrategy + */ + private function traverseClassNode(ClassNode $node, ExecutionContextInterface $context) + { + if (false === $this->validateNode($node, $context)) { + return; + } + + if (0 === count($node->groups)) { + return; + } + + foreach ($node->metadata->getConstrainedProperties() as $propertyName) { + foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { + if (!$propertyMetadata instanceof PropertyMetadataInterface) { + throw new UnsupportedMetadataException(sprintf( + 'The property metadata instances should implement '. + '"Symfony\Component\Validator\Mapping\PropertyMetadataInterface", '. + 'got: "%s".', + is_object($propertyMetadata) ? get_class($propertyMetadata) : gettype($propertyMetadata) + )); + } + + $this->traverseGenericNode(new PropertyNode( + $node->value, + $propertyMetadata->getPropertyValue($node->value), + $propertyMetadata, + $node->propertyPath + ? $node->propertyPath.'.'.$propertyName + : $propertyName, + $node->groups, + $node->cascadedGroups + ), $context); + } + } + + $traversalStrategy = $node->traversalStrategy; + + // If no specific traversal strategy was requested when this method + // was called, use the traversal strategy of the class' metadata + if ($traversalStrategy & TraversalStrategy::IMPLICIT) { + // Keep the STOP_RECURSION flag, if it was set + $traversalStrategy = $node->metadata->getTraversalStrategy() + | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); + } + + // Traverse only if IMPLICIT or TRAVERSE + if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { + return; + } + + // If IMPLICIT, stop unless we deal with a Traversable + if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) { + return; + } + + // If TRAVERSE, the constructor will fail if we have no Traversable + $this->traverseCollectionNode(new CollectionNode( + $node->value, + $node->propertyPath, + $node->groups, + $node->cascadedGroups, + $traversalStrategy + ), $context); + } + + /** + * Traverses a collection node. + * + * At first, each visitor is invoked for this node. Then, unless any + * of the visitors aborts the traversal by returning false, the successor + * nodes of the collection node are put on the stack: + * + * - for each object in the collection with associated class metadata, a + * new class node is put on the stack; + * - if an object has no associated class metadata, but is traversable, and + * unless the {@link TraversalStrategy::STOP_RECURSION} flag is set for + * collection node, a new collection node is put on the stack for that + * object; + * - for each array in the collection, a new collection node is put on the + * stack. + * + * @param CollectionNode $node The collection node + * @param ExecutionContextInterface $context The current execution context + * + * @see ClassNode + * @see CollectionNode + */ + private function traverseCollectionNode(CollectionNode $node, ExecutionContextInterface $context) + { + $traversalStrategy = $node->traversalStrategy; + + if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) { + $traversalStrategy = TraversalStrategy::NONE; + } else { + $traversalStrategy = TraversalStrategy::IMPLICIT; + } + + foreach ($node->value as $key => $value) { + if (is_array($value)) { + // Arrays are always cascaded, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $this->traverseCollectionNode(new CollectionNode( + $value, + $node->propertyPath.'['.$key.']', + $node->groups, + null, + $traversalStrategy + ), $context); + + continue; + } + + // Scalar and null values in the collection are ignored + // (BC with Symfony < 2.5) + if (is_object($value)) { + $this->cascadeObject( + $value, + $node->propertyPath.'['.$key.']', + $node->groups, + $traversalStrategy, + $context + ); + } + } + } + + /** + * Traverses a node that is neither a class nor a collection node. + * + * At first, each visitor is invoked for this node. Then, unless any + * of the visitors aborts the traversal by returning false, the successor + * nodes of the collection node are put on the stack: + * + * - if the node contains an object with associated class metadata, a new + * class node is put on the stack; + * - if the node contains a traversable object without associated class + * metadata and traversal is enabled according to the selected traversal + * strategy, a collection node is put on the stack; + * - if the node contains an array, a collection node is put on the stack. + * + * @param Node $node The node + * @param ExecutionContextInterface $context The current execution context + */ + private function traverseGenericNode(Node $node, ExecutionContextInterface $context) + { + if (false === $this->validateNode($node, $context)) { + return; + } + + if (null === $node->value) { + return; + } + + // The "cascadedGroups" property is set by the NodeValidationVisitor when + // traversing group sequences + $cascadedGroups = null !== $node->cascadedGroups + ? $node->cascadedGroups + : $node->groups; + + if (0 === count($cascadedGroups)) { + return; + } + + $cascadingStrategy = $node->metadata->getCascadingStrategy(); + $traversalStrategy = $node->traversalStrategy; + + // If no specific traversal strategy was requested when this method + // was called, use the traversal strategy of the node's metadata + if ($traversalStrategy & TraversalStrategy::IMPLICIT) { + // Keep the STOP_RECURSION flag, if it was set + $traversalStrategy = $node->metadata->getTraversalStrategy() + | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); + } + + if (is_array($node->value)) { + // Arrays are always traversed, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $this->traverseCollectionNode(new CollectionNode( + $node->value, + $node->propertyPath, + $cascadedGroups, + null, + $traversalStrategy + ), $context); + + return; + } + + if ($cascadingStrategy & CascadingStrategy::CASCADE) { + // If the value is a scalar, pass it anyway, because we want + // a NoSuchMetadataException to be thrown in that case + // (BC with Symfony < 2.5) + $this->cascadeObject( + $node->value, + $node->propertyPath, + $cascadedGroups, + $traversalStrategy, + $context + ); + + return; + } + + // Currently, the traversal strategy can only be TRAVERSE for a + // generic node if the cascading strategy is CASCADE. Thus, traversable + // objects will always be handled within cascadeObject() and there's + // nothing more to do here. + + // see GenericMetadata::addConstraint() + } + + /** + * Executes the cascading logic for an object. + * + * If class metadata is available for the object, a class node is put on + * the node stack. Otherwise, if the selected traversal strategy allows + * traversal of the object, a new collection node is put on the stack. + * Otherwise, an exception is thrown. + * + * @param object $object The object to cascade + * @param string $propertyPath The current property path + * @param string[] $groups The validated groups + * @param integer $traversalStrategy The strategy for traversing the + * cascaded object + * @param ExecutionContextInterface $context The current execution context + * + * @throws NoSuchMetadataException If the object has no associated metadata + * and does not implement {@link \Traversable} + * or if traversal is disabled via the + * $traversalStrategy argument + * @throws UnsupportedMetadataException If the metadata returned by the + * metadata factory does not implement + * {@link ClassMetadataInterface} + */ + private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) + { + try { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + throw new UnsupportedMetadataException(sprintf( + 'The metadata factory should return instances of '. + '"Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) + )); + } + + $this->traverseClassNode(new ClassNode( + $object, + $classMetadata, + $propertyPath, + $groups, + null, + $traversalStrategy + ), $context); + } catch (NoSuchMetadataException $e) { + // Rethrow if not Traversable + if (!$object instanceof \Traversable) { + throw $e; + } + + // Rethrow unless IMPLICIT or TRAVERSE + if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { + throw $e; + } + + $this->traverseCollectionNode(new CollectionNode( + $object, + $propertyPath, + $groups, + null, + $traversalStrategy + ), $context); + } + } + + /** + * Validates a node's value against the constraints defined in the node's + * metadata. + * + * Objects and constraints that were validated before in the same context + * will be skipped. + * + * @param Node $node The current node + * @param ExecutionContextInterface $context The execution context + * + * @return Boolean Whether to traverse the successor nodes + */ + public function validateNode(Node $node, ExecutionContextInterface $context) + { + if ($node instanceof CollectionNode) { + return true; + } + + $context->setValue($node->value); + $context->setMetadata($node->metadata); + $context->setPropertyPath($node->propertyPath); + + if ($node instanceof ClassNode) { + $groupSequence = null; + + if ($node->metadata->hasGroupSequence()) { + // The group sequence is statically defined for the class + $groupSequence = $node->metadata->getGroupSequence(); + } elseif ($node->metadata->isGroupSequenceProvider()) { + // The group sequence is dynamically obtained from the validated + // object + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $groupSequence = $node->value->getGroupSequence(); + + if (!$groupSequence instanceof GroupSequence) { + $groupSequence = new GroupSequence($groupSequence); + } + } + + if (null !== $groupSequence) { + $key = array_search(Constraint::DEFAULT_GROUP, $node->groups); + + if (false !== $key) { + // Replace the "Default" group by the group sequence + $node->groups[$key] = $groupSequence; + + // Cascade the "Default" group when validating the sequence + $groupSequence->cascadedGroup = Constraint::DEFAULT_GROUP; + } + } + } + + if ($node instanceof ClassNode) { + $objectHash = spl_object_hash($node->value); + } elseif ($node instanceof PropertyNode) { + $objectHash = spl_object_hash($node->object); + } else { + $objectHash = null; + } + + // if group (=[,G3,G4]) contains group sequence (=) + // then call traverse() with each entry of the group sequence and abort + // if necessary (G1, G2) + // finally call traverse() with remaining entries ([G3,G4]) or + // simply continue traversal (if possible) + + foreach ($node->groups as $key => $group) { + // Even if we remove the following clause, the constraints on an + // object won't be validated again due to the measures taken in + // validateNodeForGroup(). + // The following shortcut, however, prevents validatedNodeForGroup() + // from being called at all and enhances performance a bit. + if ($node instanceof ClassNode) { + // Use the object hash for group sequences + $groupHash = is_object($group) ? spl_object_hash($group) : $group; + + if ($context->isObjectValidatedForGroup($objectHash, $groupHash)) { + // Skip this group when validating the successor nodes + // (property and/or collection nodes) + unset($node->groups[$key]); + + continue; + } + + $context->markObjectAsValidatedForGroup($objectHash, $groupHash); + } + + // Validate normal group + if (!$group instanceof GroupSequence) { + $this->validateNodeForGroup($node, $group, $context, $objectHash); + + continue; + } + + // Traverse group sequence until a violation is generated + $this->stepThroughGroupSequence($node, $group, $context); + + // Skip the group sequence when validating successor nodes + unset($node->groups[$key]); + } + + return true; + } + + /** + * Validates a node's value in each group of a group sequence. + * + * If any of the groups' constraints generates a violation, subsequent + * groups are not validated anymore. + * + * @param Node $node The validated node + * @param GroupSequence $groupSequence The group sequence + * @param ExecutionContextInterface $context The execution context + */ + private function stepThroughGroupSequence(Node $node, GroupSequence $groupSequence, ExecutionContextInterface $context) + { + $violationCount = count($context->getViolations()); + + foreach ($groupSequence->groups as $groupInSequence) { + $node = clone $node; + $node->groups = array($groupInSequence); + + if (null !== $groupSequence->cascadedGroup) { + $node->cascadedGroups = array($groupSequence->cascadedGroup); + } + + if ($node instanceof ClassNode) { + $this->traverseClassNode($node, $context); + } elseif ($node instanceof CollectionNode) { + $this->traverseCollectionNode($node, $context); + } else { + $this->traverseGenericNode($node, $context); + } + + // Abort sequence validation if a violation was generated + if (count($context->getViolations()) > $violationCount) { + break; + } + } + } + + /** + * Validates a node's value against all constraints in the given group. + * + * @param Node $node The validated node + * @param string $group The group to validate + * @param ExecutionContextInterface $context The execution context + * @param string $objectHash The hash of the node's + * object (if any) + * + * @throws \Exception + */ + private function validateNodeForGroup(Node $node, $group, ExecutionContextInterface $context, $objectHash) + { + try { + $context->setGroup($group); + + foreach ($node->metadata->findConstraints($group) as $constraint) { + // Prevent duplicate validation of constraints, in the case + // that constraints belong to multiple validated groups + if (null !== $objectHash) { + $constraintHash = spl_object_hash($constraint); + + if ($node instanceof ClassNode) { + if ($context->isClassConstraintValidated($objectHash, $constraintHash)) { + continue; + } + + $context->markClassConstraintAsValidated($objectHash, $constraintHash); + } elseif ($node instanceof PropertyNode) { + $propertyName = $node->metadata->getPropertyName(); + + if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { + continue; + } + + $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); + } + } + + $validator = $this->validatorFactory->getInstance($constraint); + $validator->initialize($context); + $validator->validate($node->value, $constraint); + } + + $context->setGroup(null); + } catch (\Exception $e) { + // Should be put into a finally block once we switch to PHP 5.5 + $context->setGroup(null); + + throw $e; + } + } + + /** + * {@inheritdoc} + */ + public function getCurrentGroup() + { + return $this->currentGroup; + } +} diff --git a/src/Symfony/Component/Validator/Validator/RecursiveValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveValidator.php new file mode 100644 index 0000000000..a8f9307d71 --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/RecursiveValidator.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Validator; + +use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; +use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; + +/** + * Default implementation of {@link ValidatorInterface}. + * + * @since 2.5 + * @author Bernhard Schussek + */ +class RecursiveValidator implements ValidatorInterface +{ + /** + * @var ExecutionContextFactoryInterface + */ + protected $contextFactory; + + /** + * @var MetadataFactoryInterface + */ + protected $metadataFactory; + + protected $validatorFactory; + + /** + * Creates a new validator. + * + * @param ExecutionContextFactoryInterface $contextFactory The factory for + * creating new contexts + * @param MetadataFactoryInterface $metadataFactory The factory for + * fetching the metadata + * of validated objects + */ + public function __construct(ExecutionContextFactoryInterface $contextFactory, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory) + { + $this->contextFactory = $contextFactory; + $this->metadataFactory = $metadataFactory; + $this->validatorFactory = $validatorFactory; + } + + /** + * {@inheritdoc} + */ + public function startContext($root = null) + { + return new RecursiveContextualValidator( + $this->contextFactory->createContext($this, $root), + $this->metadataFactory, + $this->validatorFactory + ); + } + + /** + * {@inheritdoc} + */ + public function inContext(ExecutionContextInterface $context) + { + return new RecursiveContextualValidator( + $context, + $this->metadataFactory, + $this->validatorFactory + ); + } + + /** + * {@inheritdoc} + */ + public function getMetadataFor($object) + { + return $this->metadataFactory->getMetadataFor($object); + } + + /** + * {@inheritdoc} + */ + public function hasMetadataFor($object) + { + return $this->metadataFactory->hasMetadataFor($object); + } + + /** + * {@inheritdoc} + */ + public function validate($value, $constraints = null, $groups = null) + { + return $this->startContext($value) + ->validate($value, $constraints, $groups) + ->getViolations(); + } + + /** + * {@inheritdoc} + */ + public function validateProperty($object, $propertyName, $groups = null) + { + return $this->startContext($object) + ->validateProperty($object, $propertyName, $groups) + ->getViolations(); + } + + /** + * {@inheritdoc} + */ + public function validatePropertyValue($object, $propertyName, $value, $groups = null) + { + return $this->startContext($object) + ->validatePropertyValue($object, $propertyName, $value, $groups) + ->getViolations(); + } +} diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php similarity index 98% rename from src/Symfony/Component/Validator/Validator/ContextualValidator.php rename to src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php index b250278941..2f23ce71f3 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php @@ -29,7 +29,7 @@ use Symfony\Component\Validator\Util\PropertyPath; * @since 2.5 * @author Bernhard Schussek */ -class ContextualValidator implements ContextualValidatorInterface +class TraversingContextualValidator implements ContextualValidatorInterface { /** * @var ExecutionContextInterface diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/TraversingValidator.php similarity index 95% rename from src/Symfony/Component/Validator/Validator/Validator.php rename to src/Symfony/Component/Validator/Validator/TraversingValidator.php index a73a72ebb5..4352b3180f 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/TraversingValidator.php @@ -22,7 +22,7 @@ use Symfony\Component\Validator\MetadataFactoryInterface; * @since 2.5 * @author Bernhard Schussek */ -class Validator implements ValidatorInterface +class TraversingValidator implements ValidatorInterface { /** * @var ExecutionContextFactoryInterface @@ -61,7 +61,7 @@ class Validator implements ValidatorInterface */ public function startContext($root = null) { - return new ContextualValidator( + return new TraversingContextualValidator( $this->contextFactory->createContext($this, $root), $this->nodeTraverser, $this->metadataFactory @@ -73,7 +73,7 @@ class Validator implements ValidatorInterface */ public function inContext(ExecutionContextInterface $context) { - return new ContextualValidator( + return new TraversingContextualValidator( $context, $this->nodeTraverser, $this->metadataFactory diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index b6763ede74..351c5d654e 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator; +use Doctrine\Common\Annotations\AnnotationRegistry; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Validator\Context\ExecutionContextFactory; @@ -37,7 +38,7 @@ use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeVisitor\ObjectInitializationVisitor; use Symfony\Component\Validator\Validator as ValidatorV24; -use Symfony\Component\Validator\Validator\Validator; +use Symfony\Component\Validator\Validator\TraversingValidator; use Symfony\Component\Validator\Validator\LegacyValidator; /** @@ -373,6 +374,19 @@ class ValidatorBuilder implements ValidatorBuilderInterface if ($this->annotationReader) { $loaders[] = new AnnotationLoader($this->annotationReader); + + AnnotationRegistry::registerLoader(function ($class) { + if (0 === strpos($class, __NAMESPACE__.'\\Constraints\\')) { + $file = str_replace(__NAMESPACE__.'\\Constraints\\', __DIR__.'/Constraints/', $class).'.php'; + + if (is_file($file)) { + require_once $file; + return true; + } + } + + return false; + }); } $loader = null; @@ -401,26 +415,24 @@ class ValidatorBuilder implements ValidatorBuilderInterface return new ValidatorV24($metadataFactory, $validatorFactory, $translator, $this->translationDomain, $this->initializers); } - $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); - $nodeValidator = new NodeValidationVisitor($nodeTraverser, $validatorFactory); - if (Validation::API_VERSION_2_5 === $apiVersion) { - $contextFactory = new ExecutionContextFactory($nodeValidator, $translator, $this->translationDomain); + $contextFactory = new ExecutionContextFactory($translator, $this->translationDomain); } else { - $contextFactory = new LegacyExecutionContextFactory($nodeValidator, $translator, $this->translationDomain); + $contextFactory = new LegacyExecutionContextFactory($translator, $this->translationDomain); } - $nodeTraverser->addVisitor(new ContextUpdateVisitor()); - if (count($this->initializers) > 0) { - $nodeTraverser->addVisitor(new ObjectInitializationVisitor($this->initializers)); - } - $nodeTraverser->addVisitor(new DefaultGroupReplacingVisitor()); - $nodeTraverser->addVisitor($nodeValidator); - if (Validation::API_VERSION_2_5 === $apiVersion) { - return new Validator($contextFactory, $nodeTraverser, $metadataFactory); + $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); + if (count($this->initializers) > 0) { + $nodeTraverser->addVisitor(new ObjectInitializationVisitor($this->initializers)); + } + $nodeTraverser->addVisitor(new ContextUpdateVisitor()); + $nodeTraverser->addVisitor(new DefaultGroupReplacingVisitor()); + $nodeTraverser->addVisitor(new NodeValidationVisitor($nodeTraverser, $validatorFactory)); + + return new TraversingValidator($contextFactory, $nodeTraverser, $metadataFactory); } - return new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); + return new LegacyValidator($contextFactory, $metadataFactory, $validatorFactory); } }