From 25cdc68d363855ee0744ac9494891a84f23bba40 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 12 Feb 2014 12:38:50 +0100 Subject: [PATCH 01/86] [Validator] Refactored ValidatorTest and ValidationVisitorTest into an abstract validator test class --- UPGRADE-2.5.md | 25 +- src/Symfony/Component/Validator/CHANGELOG.md | 2 + .../Validator/Constraints/Callback.php | 2 +- .../Validator/Constraints/GroupSequence.php | 41 +- .../Validator/Mapping/ClassMetadata.php | 17 +- .../Validator/Tests/AbstractValidatorTest.php | 1236 +++++++++++++++++ .../Constraints/CallbackValidatorTest.php | 4 +- .../Tests/Constraints/GroupSequenceTest.php | 66 + .../Validator/Tests/Fixtures/Entity.php | 8 +- .../Fixtures/GroupSequenceProviderEntity.php | 8 +- .../Validator/Tests/Fixtures/Reference.php | 13 + .../Validator/Tests/ValidationVisitorTest.php | 564 -------- .../Validator/Tests/ValidatorTest.php | 259 +--- 13 files changed, 1410 insertions(+), 835 deletions(-) create mode 100644 src/Symfony/Component/Validator/Tests/AbstractValidatorTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceTest.php delete mode 100644 src/Symfony/Component/Validator/Tests/ValidationVisitorTest.php diff --git a/UPGRADE-2.5.md b/UPGRADE-2.5.md index e3b581b5b9dfd..6ce3e86af546a 100644 --- a/UPGRADE-2.5.md +++ b/UPGRADE-2.5.md @@ -56,10 +56,11 @@ Validator After: - Default email validation is now done via a simple regex which may cause invalid emails (not RFC compilant) to be + Default email validation is now done via a simple regex which may cause invalid emails (not RFC compilant) to be valid. This is the default behaviour. Strict email validation has to be explicitly activated in the configuration file by adding + ``` framework: //... @@ -68,7 +69,29 @@ Validator //... ``` + Also you have to add to your composer.json: + ``` "egulias/email-validator": "1.1.*" ``` + + * `ClassMetadata::getGroupSequence()` now returns `GroupSequence` instances + instead of an array. The sequence implements `\Traversable`, `\ArrayAccess` + and `\Countable`, so in most cases you should be fine. If you however use the + sequence with PHP's `array_*()` functions, you should cast it to an array + first using `iterator_to_array()`: + + Before: + + ``` + $sequence = $metadata->getGroupSequence(); + $result = array_map($callback, $sequence); + ``` + + After: + + ``` + $sequence = iterator_to_array($metadata->getGroupSequence()); + $result = array_map($callback, $sequence); + ``` diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 3eba52930f3f7..ecdf0cf366a6e 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -6,6 +6,8 @@ CHANGELOG * deprecated `ApcCache` in favor of `DoctrineCache` * added `DoctrineCache` to adapt any Doctrine cache + * `GroupSequence` now implements `ArrayAccess`, `Countable` and `Traversable` + * changed `ClassMetadata::getGroupSequence()` to return a `GroupSequence` instance instead of an array 2.4.0 ----- diff --git a/src/Symfony/Component/Validator/Constraints/Callback.php b/src/Symfony/Component/Validator/Constraints/Callback.php index 01aeb6ddb7982..18cd7b3e92c1a 100644 --- a/src/Symfony/Component/Validator/Constraints/Callback.php +++ b/src/Symfony/Component/Validator/Constraints/Callback.php @@ -71,6 +71,6 @@ public function getDefaultOption() */ public function getTargets() { - return self::CLASS_CONSTRAINT; + return array(self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT); } } diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index 304fab8c943b0..61f72c6231cf9 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -20,7 +20,7 @@ * * @api */ -class GroupSequence +class GroupSequence implements \ArrayAccess, \IteratorAggregate, \Countable { /** * The members of the sequence @@ -30,6 +30,43 @@ class GroupSequence public function __construct(array $groups) { - $this->groups = $groups['value']; + // Support for Doctrine annotations + $this->groups = isset($groups['value']) ? $groups['value'] : $groups; + } + + public function getIterator() + { + return new \ArrayIterator($this->groups); + } + + public function offsetExists($offset) + { + return isset($this->groups[$offset]); + } + + public function offsetGet($offset) + { + return $this->groups[$offset]; + } + + public function offsetSet($offset, $value) + { + if (null !== $offset) { + $this->groups[$offset] = $value; + + return; + } + + $this->groups[] = $value; + } + + public function offsetUnset($offset) + { + unset($this->groups[$offset]); + } + + public function count() + { + return count($this->groups); } } diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index b7e003ec3da40..8bba73a01f13e 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Mapping; +use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\PropertyMetadataContainerInterface; use Symfony\Component\Validator\ClassBasedInterface; @@ -330,27 +331,31 @@ public function getConstrainedProperties() /** * Sets the default group sequence for this class. * - * @param array $groups An array of group names + * @param array $groupSequence An array of group names * * @return ClassMetadata * * @throws GroupDefinitionException */ - public function setGroupSequence(array $groups) + public function setGroupSequence($groupSequence) { if ($this->isGroupSequenceProvider()) { throw new GroupDefinitionException('Defining a static group sequence is not allowed with a group sequence provider'); } - if (in_array(Constraint::DEFAULT_GROUP, $groups, true)) { + if (is_array($groupSequence)) { + $groupSequence = new GroupSequence($groupSequence); + } + + if (in_array(Constraint::DEFAULT_GROUP, $groupSequence->groups, true)) { throw new GroupDefinitionException(sprintf('The group "%s" is not allowed in group sequences', Constraint::DEFAULT_GROUP)); } - if (!in_array($this->getDefaultGroup(), $groups, true)) { + if (!in_array($this->getDefaultGroup(), $groupSequence->groups, true)) { throw new GroupDefinitionException(sprintf('The group "%s" is missing in the group sequence', $this->getDefaultGroup())); } - $this->groupSequence = $groups; + $this->groupSequence = $groupSequence; return $this; } @@ -368,7 +373,7 @@ public function hasGroupSequence() /** * Returns the default group sequence for this class. * - * @return array An array of group names + * @return GroupSequence The group sequence or null */ public function getGroupSequence() { diff --git a/src/Symfony/Component/Validator/Tests/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/AbstractValidatorTest.php new file mode 100644 index 0000000000000..33e39c5345e04 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/AbstractValidatorTest.php @@ -0,0 +1,1236 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\ConstraintViolationInterface; +use Symfony\Component\Validator\ExecutionContextInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; +use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\Tests\Fixtures\GroupSequenceProviderEntity; +use Symfony\Component\Validator\Tests\Fixtures\Reference; +use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\Tests\Fixtures\FailingConstraint; +use Symfony\Component\Validator\Tests\Fixtures\Entity; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\ValidatorInterface; + +/** + * @since 2.5 + * @author Bernhard Schussek + */ +abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase +{ + const ENTITY_CLASS = 'Symfony\Component\Validator\Tests\Fixtures\Entity'; + + const REFERENCE_CLASS = 'Symfony\Component\Validator\Tests\Fixtures\Reference'; + + /** + * @var ValidatorInterface + */ + private $validator; + + /** + * @var FakeMetadataFactory + */ + public $metadataFactory; + + /** + * @var ClassMetadata + */ + public $metadata; + + /** + * @var ClassMetadata + */ + public $referenceMetadata; + + protected function setUp() + { + $this->metadataFactory = new FakeMetadataFactory(); + $this->validator = $this->createValidator($this->metadataFactory); + $this->metadata = new ClassMetadata(self::ENTITY_CLASS); + $this->referenceMetadata = new ClassMetadata(self::REFERENCE_CLASS); + $this->metadataFactory->addMetadata($this->metadata); + $this->metadataFactory->addMetadata($this->referenceMetadata); + } + + protected function tearDown() + { + $this->metadataFactory = null; + $this->validator = null; + $this->metadata = null; + $this->referenceMetadata = null; + } + + abstract protected function createValidator(MetadataFactoryInterface $metadataFactory); + + public function testClassConstraint() + { + $test = $this; + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testPropertyConstraint() + { + $test = $this; + $entity = new Entity(); + $entity->firstName = 'Bernhard'; + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $propertyMetadatas = $test->metadata->getPropertyMetadata('firstName'); + + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertSame('firstName', $context->getPropertyName()); + $test->assertSame('firstName', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($propertyMetadatas[0], $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame('Bernhard', $context->getValue()); + $test->assertSame('Bernhard', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('firstName', new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('firstName', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Bernhard', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testGetterConstraint() + { + $test = $this; + $entity = new Entity(); + $entity->setLastName('Schussek'); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $propertyMetadatas = $test->metadata->getPropertyMetadata('lastName'); + + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertSame('lastName', $context->getPropertyName()); + $test->assertSame('lastName', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($propertyMetadatas[0], $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame('Schussek', $context->getValue()); + $test->assertSame('Schussek', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addGetterConstraint('lastName', new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('lastName', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Schussek', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testReferenceClassConstraint() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('reference', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('reference', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testReferencePropertyConstraint() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + $entity->reference->value = 'Foobar'; + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $propertyMetadatas = $test->referenceMetadata->getPropertyMetadata('value'); + + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertSame('value', $context->getPropertyName()); + $test->assertSame('reference.value', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($propertyMetadatas[0], $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame('Foobar', $context->getValue()); + $test->assertSame('Foobar', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addPropertyConstraint('value', new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('reference.value', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Foobar', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testReferenceGetterConstraint() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + $entity->reference->setPrivateValue('Bamboo'); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $propertyMetadatas = $test->referenceMetadata->getPropertyMetadata('privateValue'); + + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertSame('privateValue', $context->getPropertyName()); + $test->assertSame('reference.privateValue', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($propertyMetadatas[0], $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame('Bamboo', $context->getValue()); + $test->assertSame('Bamboo', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addPropertyConstraint('privateValue', new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('reference.privateValue', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Bamboo', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testsIgnoreNullReference() + { + $entity = new Entity(); + $entity->reference = null; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testFailOnScalarReferences() + { + $entity = new Entity(); + $entity->reference = 'string'; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $this->validator->validate($entity); + } + + public function testArrayReference() + { + $test = $this; + $entity = new Entity(); + $entity->reference = array('key' => new Reference()); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('reference[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference['key'], $context->getValue()); + $test->assertSame($entity->reference['key'], $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('reference[key]', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference['key'], $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + // https://github.com/symfony/symfony/issues/6246 + public function testRecursiveArrayReference() + { + $test = $this; + $entity = new Entity(); + $entity->reference = array(2 => array('key' => new Reference())); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('reference[2][key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference[2]['key'], $context->getValue()); + $test->assertSame($entity->reference[2]['key'], $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('reference[2][key]', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference[2]['key'], $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testArrayTraversalCannotBeDisabled() + { + $entity = new Entity(); + $entity->reference = array('key' => new Reference()); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'traverse' => false, + ))); + $this->referenceMetadata->addConstraint(new Callback($callback)); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testRecursiveArrayTraversalCannotBeDisabled() + { + $entity = new Entity(); + $entity->reference = array(2 => array('key' => new Reference())); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'traverse' => false, + ))); + $this->referenceMetadata->addConstraint(new Callback($callback)); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testIgnoreScalarsDuringArrayTraversal() + { + $entity = new Entity(); + $entity->reference = array('string', 1234); + + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + + public function testIgnoreNullDuringArrayTraversal() + { + $entity = new Entity(); + $entity->reference = array(null); + + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + + public function testTraversableReference() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array('key' => new Reference())); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('reference[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference['key'], $context->getValue()); + $test->assertSame($entity->reference['key'], $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('reference[key]', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference['key'], $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testDisableTraversableTraversal() + { + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array('key' => new Reference())); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'traverse' => false, + ))); + $this->referenceMetadata->addConstraint(new Callback($callback)); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testMetadataMustExistIfTraversalIsDisabled() + { + $entity = new Entity(); + $entity->reference = new \ArrayIterator(); + + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'traverse' => false, + ))); + + $this->validator->validate($entity, 'Default', ''); + } + + public function testNoRecursiveTraversableTraversal() + { + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => new Reference())), + )); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addConstraint(new Callback($callback)); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + + public function testEnableRecursiveTraversableTraversal() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => new Reference())), + )); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('reference[2][key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference[2]['key'], $context->getValue()); + $test->assertSame($entity->reference[2]['key'], $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'deep' => true, + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('reference[2][key]', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference[2]['key'], $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testValidateProperty() + { + $test = $this; + $entity = new Entity(); + $entity->firstName = 'Bernhard'; + $entity->setLastName('Schussek'); + + $callback1 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $propertyMetadatas = $test->metadata->getPropertyMetadata('firstName'); + + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertSame('firstName', $context->getPropertyName()); + $test->assertSame('firstName', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($propertyMetadatas[0], $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame('Bernhard', $context->getValue()); + $test->assertSame('Bernhard', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Other violation'); + }; + + $this->metadata->addPropertyConstraint('firstName', new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->metadata->addPropertyConstraint('lastName', new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validateProperty($entity, 'firstName', 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('firstName', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Bernhard', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ValidatorException + */ + public function testValidatePropertyFailsIfPropertiesNotSupported() + { + // $metadata does not implement PropertyMetadataContainerInterface + $metadata = $this->getMock('Symfony\Component\Validator\MetadataInterface'); + + $metadataFactory = $this->getMock('Symfony\Component\Validator\MetadataFactoryInterface'); + $metadataFactory->expects($this->any()) + ->method('getMetadataFor') + ->with('VALUE') + ->will($this->returnValue($metadata)); + $validator = $this->createValidator($metadataFactory); + + $validator->validateProperty('VALUE', 'someProperty'); + } + + public function testValidatePropertyValue() + { + $test = $this; + $entity = new Entity(); + $entity->setLastName('Schussek'); + + $callback1 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $propertyMetadatas = $test->metadata->getPropertyMetadata('firstName'); + + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertSame('firstName', $context->getPropertyName()); + $test->assertSame('firstName', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($propertyMetadatas[0], $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame('Bernhard', $context->getValue()); + $test->assertSame('Bernhard', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Other violation'); + }; + + $this->metadata->addPropertyConstraint('firstName', new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->metadata->addPropertyConstraint('lastName', new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validatePropertyValue( + $entity, + 'firstName', + 'Bernhard', + 'Group' + ); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('firstName', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Bernhard', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ValidatorException + */ + public function testValidatePropertyValueFailsIfPropertiesNotSupported() + { + // $metadata does not implement PropertyMetadataContainerInterface + $metadata = $this->getMock('Symfony\Component\Validator\MetadataInterface'); + + $metadataFactory = $this->getMock('Symfony\Component\Validator\MetadataFactoryInterface'); + $metadataFactory->expects($this->any()) + ->method('getMetadataFor') + ->with('VALUE') + ->will($this->returnValue($metadata)); + $validator = $this->createValidator($metadataFactory); + + $validator->validatePropertyValue('VALUE', 'someProperty', 'someValue'); + } + + public function testValidateValue() + { + $test = $this; + + $callback = function ($value, ExecutionContextInterface $context) use ($test) { + $test->assertNull($context->getClassName()); + $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()); + $test->assertSame('Bernhard', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $constraint = new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + )); + + $violations = $this->validator->validateValue('Bernhard', $constraint, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame('Bernhard', $violations[0]->getRoot()); + $this->assertSame('Bernhard', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ValidatorException + */ + public function testValidateValueRejectsValid() + { + $this->validator->validateValue(new Entity(), new Valid()); + } + + public function testAddCustomizedViolation() + { + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation( + 'Message %param%', + array('%param%' => 'value'), + 'Invalid value', + 2, + 'Code' + ); + }; + + $this->metadata->addConstraint(new Callback($callback)); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Invalid value', $violations[0]->getInvalidValue()); + $this->assertSame(2, $violations[0]->getMessagePluralization()); + $this->assertSame('Code', $violations[0]->getCode()); + } + + public function testValidateObjectOnlyOncePerGroup() + { + $entity = new Entity(); + $entity->reference = new Reference(); + $entity->reference2 = $entity->reference; + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->metadata->addPropertyConstraint('reference2', new Valid()); + $this->referenceMetadata->addConstraint(new Callback($callback)); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testValidateDifferentObjectsSeparately() + { + $entity = new Entity(); + $entity->reference = new Reference(); + $entity->reference2 = new Reference(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->metadata->addPropertyConstraint('reference2', new Valid()); + $this->referenceMetadata->addConstraint(new Callback($callback)); + + $violations = $this->validator->validate($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(2, $violations); + } + + public function testValidateSingleGroup() + { + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group 1', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group 2', + ))); + + $violations = $this->validator->validate($entity, 'Group 2'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testValidateMultipleGroups() + { + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group 1', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group 2', + ))); + + $violations = $this->validator->validate($entity, array('Group 1', 'Group 2')); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(2, $violations); + } + + public function testNoDuplicateValidationIfConstraintInMultipleGroups() + { + $this->markTestSkipped('Currently not supported'); + + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => array('Group 1', 'Group 2'), + ))); + + $violations = $this->validator->validate($entity, array('Group 1', 'Group 2')); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testGroupSequenceAbortsAfterFailedGroup() + { + $this->markTestSkipped('Currently not supported'); + + $entity = new Entity(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message 1'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message 2'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => function () {}, + 'groups' => 'Group 1', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group 2', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 3', + ))); + + $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3')); + $violations = $this->validator->validate($entity, $sequence); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message 1', $violations[0]->getMessage()); + } + + public function testGroupSequenceIncludesReferences() + { + $this->markTestSkipped('Currently not supported'); + + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Reference violation 1'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Reference violation 2'); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group 1', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 2', + ))); + + $sequence = new GroupSequence(array('Group 1', 'Entity')); + $violations = $this->validator->validate($entity, $sequence); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Reference violation 1', $violations[0]->getMessage()); + } + + public function testReplaceDefaultGroupByGroupSequenceObject() + { + $entity = new Entity(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Group 2'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Group 3'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => function () {}, + 'groups' => 'Group 1', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group 2', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 3', + ))); + + $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3', 'Entity')); + $this->metadata->setGroupSequence($sequence); + + $violations = $this->validator->validate($entity, 'Default'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Violation in Group 2', $violations[0]->getMessage()); + } + + public function testReplaceDefaultGroupByGroupSequenceArray() + { + $entity = new Entity(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Group 2'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Group 3'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => function () {}, + 'groups' => 'Group 1', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group 2', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 3', + ))); + + $sequence = array('Group 1', 'Group 2', 'Group 3', 'Entity'); + $this->metadata->setGroupSequence($sequence); + + $violations = $this->validator->validate($entity, 'Default'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Violation in Group 2', $violations[0]->getMessage()); + } + + public function testPropagateDefaultGroupToReferenceWhenReplacingDefaultGroup() + { + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Default group'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in group sequence'); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Default', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 1', + ))); + + $sequence = new GroupSequence(array('Group 1', 'Entity')); + $this->metadata->setGroupSequence($sequence); + + $violations = $this->validator->validate($entity, 'Default'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Violation in Default group', $violations[0]->getMessage()); + } + + public function testValidateCustomGroupWhenDefaultGroupWasReplaced() + { + $entity = new Entity(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in other group'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in group sequence'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Other Group', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 1', + ))); + + $sequence = new GroupSequence(array('Group 1', 'Entity')); + $this->metadata->setGroupSequence($sequence); + + $violations = $this->validator->validate($entity, 'Other Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Violation in other group', $violations[0]->getMessage()); + } + + public function testReplaceDefaultGroupWithObjectFromGroupSequenceProvider() + { + $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3', 'Entity')); + $entity = new GroupSequenceProviderEntity($sequence); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Group 2'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Group 3'); + }; + + $metadata = new ClassMetadata(get_class($entity)); + $metadata->addConstraint(new Callback(array( + 'callback' => function () {}, + 'groups' => 'Group 1', + ))); + $metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group 2', + ))); + $metadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 3', + ))); + $metadata->setGroupSequenceProvider(true); + + $this->metadataFactory->addMetadata($metadata); + + $violations = $this->validator->validate($entity, 'Default'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Violation in Group 2', $violations[0]->getMessage()); + } + + public function testReplaceDefaultGroupWithArrayFromGroupSequenceProvider() + { + $sequence = array('Group 1', 'Group 2', 'Group 3', 'Entity'); + $entity = new GroupSequenceProviderEntity($sequence); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Group 2'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Violation in Group 3'); + }; + + $metadata = new ClassMetadata(get_class($entity)); + $metadata->addConstraint(new Callback(array( + 'callback' => function () {}, + 'groups' => 'Group 1', + ))); + $metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group 2', + ))); + $metadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 3', + ))); + $metadata->setGroupSequenceProvider(true); + + $this->metadataFactory->addMetadata($metadata); + + $violations = $this->validator->validate($entity, 'Default'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Violation in Group 2', $violations[0]->getMessage()); + } + + public function testGetMetadataFactory() + { + $this->assertSame($this->metadataFactory, $this->validator->getMetadataFactory()); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php index cdcd49bb58ed8..e0317823d52c9 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Tests\Constraints; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ExecutionContext; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\CallbackValidator; @@ -320,8 +321,9 @@ public function testExpectEitherCallbackOrMethods() public function testConstraintGetTargets() { $constraint = new Callback(array('foo')); + $targets = array(Constraint::CLASS_CONSTRAINT, Constraint::PROPERTY_CONSTRAINT); - $this->assertEquals('class', $constraint->getTargets()); + $this->assertEquals($targets, $constraint->getTargets()); } // Should succeed. Needed when defining constraints as annotations. diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceTest.php new file mode 100644 index 0000000000000..83275d1c72733 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceTest.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Validator\Constraints\GroupSequence; + +/** + * @author Bernhard Schussek + */ +class GroupSequenceTest extends \PHPUnit_Framework_TestCase +{ + public function testCreate() + { + $sequence = new GroupSequence(array('Group 1', 'Group 2')); + + $this->assertSame(array('Group 1', 'Group 2'), $sequence->groups); + } + + public function testCreateDoctrineStyle() + { + $sequence = new GroupSequence(array('value' => array('Group 1', 'Group 2'))); + + $this->assertSame(array('Group 1', 'Group 2'), $sequence->groups); + } + + public function testIterate() + { + $sequence = new GroupSequence(array('Group 1', 'Group 2')); + + $this->assertSame(array('Group 1', 'Group 2'), iterator_to_array($sequence)); + } + + public function testCount() + { + $sequence = new GroupSequence(array('Group 1', 'Group 2')); + + $this->assertCount(2, $sequence); + } + + public function testArrayAccess() + { + $sequence = new GroupSequence(array('Group 1', 'Group 2')); + + $this->assertSame('Group 1', $sequence[0]); + $this->assertSame('Group 2', $sequence[1]); + $this->assertTrue(isset($sequence[0])); + $this->assertFalse(isset($sequence[2])); + unset($sequence[0]); + $this->assertFalse(isset($sequence[0])); + $sequence[] = 'Group 3'; + $this->assertTrue(isset($sequence[2])); + $this->assertSame('Group 3', $sequence[2]); + $sequence[0] = 'Group 1'; + $this->assertTrue(isset($sequence[0])); + $this->assertSame('Group 1', $sequence[0]); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/Entity.php b/src/Symfony/Component/Validator/Tests/Fixtures/Entity.php index fbd879a94eaf1..d841f5dc9d78e 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/Entity.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/Entity.php @@ -32,9 +32,10 @@ class Entity extends EntityParent implements EntityInterface * }) * @Assert\Choice(choices={"A", "B"}, message="Must be one of %choices%") */ - protected $firstName; + public $firstName; protected $lastName; public $reference; + public $reference2; private $internal; public $data = 'Overridden data'; @@ -48,6 +49,11 @@ public function getInternal() return $this->internal.' from getter'; } + public function setLastName($lastName) + { + $this->lastName = $lastName; + } + /** * @Assert\NotNull */ diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/GroupSequenceProviderEntity.php b/src/Symfony/Component/Validator/Tests/Fixtures/GroupSequenceProviderEntity.php index ef3711104ad43..2b0beaf9adf98 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/GroupSequenceProviderEntity.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/GroupSequenceProviderEntity.php @@ -22,15 +22,15 @@ class GroupSequenceProviderEntity implements GroupSequenceProviderInterface public $firstName; public $lastName; - protected $groups = array(); + protected $sequence = array(); - public function setGroups($groups) + public function __construct($sequence) { - $this->groups = $groups; + $this->sequence = $sequence; } public function getGroupSequence() { - return $this->groups; + return $this->sequence; } } diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/Reference.php b/src/Symfony/Component/Validator/Tests/Fixtures/Reference.php index f8ea173e019aa..af29735924379 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/Reference.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/Reference.php @@ -13,4 +13,17 @@ class Reference { + public $value; + + private $privateValue; + + public function setPrivateValue($privateValue) + { + $this->privateValue = $privateValue; + } + + public function getPrivateValue() + { + return $this->privateValue; + } } diff --git a/src/Symfony/Component/Validator/Tests/ValidationVisitorTest.php b/src/Symfony/Component/Validator/Tests/ValidationVisitorTest.php deleted file mode 100644 index 2868f57a82cd6..0000000000000 --- a/src/Symfony/Component/Validator/Tests/ValidationVisitorTest.php +++ /dev/null @@ -1,564 +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; - -use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; -use Symfony\Component\Validator\Constraints\Valid; -use Symfony\Component\Validator\Tests\Fixtures\Reference; -use Symfony\Component\Validator\DefaultTranslator; -use Symfony\Component\Validator\ConstraintViolation; -use Symfony\Component\Validator\ConstraintViolationList; -use Symfony\Component\Validator\Tests\Fixtures\FailingConstraint; -use Symfony\Component\Validator\Tests\Fixtures\ConstraintAValidator; -use Symfony\Component\Validator\Tests\Fixtures\Entity; -use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; -use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\ConstraintValidatorFactory; -use Symfony\Component\Validator\ValidationVisitor; - -/** - * @author Bernhard Schussek - */ -class ValidationVisitorTest extends \PHPUnit_Framework_TestCase -{ - const CLASS_NAME = 'Symfony\Component\Validator\Tests\Fixtures\Entity'; - - /** - * @var ValidationVisitor - */ - private $visitor; - - /** - * @var FakeMetadataFactory - */ - private $metadataFactory; - - /** - * @var ClassMetadata - */ - private $metadata; - - protected function setUp() - { - $this->metadataFactory = new FakeMetadataFactory(); - $this->visitor = new ValidationVisitor('Root', $this->metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); - $this->metadata = new ClassMetadata(self::CLASS_NAME); - $this->metadataFactory->addMetadata($this->metadata); - } - - protected function tearDown() - { - $this->metadataFactory = null; - $this->visitor = null; - $this->metadata = null; - } - - public function testValidatePassesCorrectClassAndProperty() - { - $this->metadata->addConstraint(new ConstraintA()); - - $entity = new Entity(); - $this->visitor->validate($entity, 'Default', ''); - - $context = ConstraintAValidator::$passedContext; - - $this->assertEquals('Symfony\Component\Validator\Tests\Fixtures\Entity', $context->getClassName()); - $this->assertNull($context->getPropertyName()); - } - - public function testValidateConstraints() - { - $this->metadata->addConstraint(new ConstraintA()); - - $this->visitor->validate(new Entity(), 'Default', ''); - - $this->assertCount(1, $this->visitor->getViolations()); - } - - public function testValidateTwiceValidatesConstraintsOnce() - { - $this->metadata->addConstraint(new ConstraintA()); - - $entity = new Entity(); - - $this->visitor->validate($entity, 'Default', ''); - $this->visitor->validate($entity, 'Default', ''); - - $this->assertCount(1, $this->visitor->getViolations()); - } - - public function testValidateDifferentObjectsValidatesTwice() - { - $this->metadata->addConstraint(new ConstraintA()); - - $this->visitor->validate(new Entity(), 'Default', ''); - $this->visitor->validate(new Entity(), 'Default', ''); - - $this->assertCount(2, $this->visitor->getViolations()); - } - - public function testValidateTwiceInDifferentGroupsValidatesTwice() - { - $this->metadata->addConstraint(new ConstraintA()); - $this->metadata->addConstraint(new ConstraintA(array('groups' => 'Custom'))); - - $entity = new Entity(); - - $this->visitor->validate($entity, 'Default', ''); - $this->visitor->validate($entity, 'Custom', ''); - - $this->assertCount(2, $this->visitor->getViolations()); - } - - public function testValidatePropertyConstraints() - { - $this->metadata->addPropertyConstraint('firstName', new ConstraintA()); - - $this->visitor->validate(new Entity(), 'Default', ''); - - $this->assertCount(1, $this->visitor->getViolations()); - } - - public function testValidateGetterConstraints() - { - $this->metadata->addGetterConstraint('lastName', new ConstraintA()); - - $this->visitor->validate(new Entity(), 'Default', ''); - - $this->assertCount(1, $this->visitor->getViolations()); - } - - public function testValidateInDefaultGroupTraversesGroupSequence() - { - $entity = new Entity(); - - $this->metadata->addPropertyConstraint('firstName', new FailingConstraint(array( - 'groups' => 'First', - ))); - $this->metadata->addGetterConstraint('lastName', new FailingConstraint(array( - 'groups' => 'Default', - ))); - $this->metadata->setGroupSequence(array('First', $this->metadata->getDefaultGroup())); - - $this->visitor->validate($entity, 'Default', ''); - - // After validation of group "First" failed, no more group was - // validated - $violations = new ConstraintViolationList(array( - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - 'firstName', - '' - ), - )); - - $this->assertEquals($violations, $this->visitor->getViolations()); - } - - public function testValidateInGroupSequencePropagatesDefaultGroup() - { - $entity = new Entity(); - $entity->reference = new Reference(); - - $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->metadata->setGroupSequence(array($this->metadata->getDefaultGroup())); - - $referenceMetadata = new ClassMetadata(get_class($entity->reference)); - $referenceMetadata->addConstraint(new FailingConstraint(array( - // this constraint is only evaluated if group "Default" is - // propagated to the reference - 'groups' => 'Default', - ))); - $this->metadataFactory->addMetadata($referenceMetadata); - - $this->visitor->validate($entity, 'Default', ''); - - // The validation of the reference's FailingConstraint in group - // "Default" was launched - $violations = new ConstraintViolationList(array( - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - 'reference', - $entity->reference - ), - )); - - $this->assertEquals($violations, $this->visitor->getViolations()); - } - - public function testValidateInOtherGroupTraversesNoGroupSequence() - { - $entity = new Entity(); - - $this->metadata->addPropertyConstraint('firstName', new FailingConstraint(array( - 'groups' => 'First', - ))); - $this->metadata->addGetterConstraint('lastName', new FailingConstraint(array( - 'groups' => $this->metadata->getDefaultGroup(), - ))); - $this->metadata->setGroupSequence(array('First', $this->metadata->getDefaultGroup())); - - $this->visitor->validate($entity, $this->metadata->getDefaultGroup(), ''); - - // Only group "Second" was validated - $violations = new ConstraintViolationList(array( - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - 'lastName', - '' - ), - )); - - $this->assertEquals($violations, $this->visitor->getViolations()); - } - - public function testValidateCascadedPropertyValidatesReferences() - { - $entity = new Entity(); - $entity->reference = new Entity(); - - // add a constraint for the entity that always fails - $this->metadata->addConstraint(new FailingConstraint()); - - // validate entity when validating the property "reference" - $this->metadata->addPropertyConstraint('reference', new Valid()); - - // invoke validation on an object - $this->visitor->validate($entity, 'Default', ''); - - $violations = new ConstraintViolationList(array( - // generated by the root object - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - '', - $entity - ), - // generated by the reference - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - 'reference', - $entity->reference - ), - )); - - $this->assertEquals($violations, $this->visitor->getViolations()); - } - - public function testValidateCascadedPropertyValidatesArraysByDefault() - { - $entity = new Entity(); - $entity->reference = array('key' => new Entity()); - - // add a constraint for the entity that always fails - $this->metadata->addConstraint(new FailingConstraint()); - - // validate array when validating the property "reference" - $this->metadata->addPropertyConstraint('reference', new Valid()); - - $this->visitor->validate($entity, 'Default', ''); - - $violations = new ConstraintViolationList(array( - // generated by the root object - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - '', - $entity - ), - // generated by the reference - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - 'reference[key]', - $entity->reference['key'] - ), - )); - - $this->assertEquals($violations, $this->visitor->getViolations()); - } - - public function testValidateCascadedPropertyValidatesTraversableByDefault() - { - $entity = new Entity(); - $entity->reference = new \ArrayIterator(array('key' => new Entity())); - - // add a constraint for the entity that always fails - $this->metadata->addConstraint(new FailingConstraint()); - - // validate array when validating the property "reference" - $this->metadata->addPropertyConstraint('reference', new Valid()); - - $this->visitor->validate($entity, 'Default', ''); - - $violations = new ConstraintViolationList(array( - // generated by the root object - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - '', - $entity - ), - // generated by the reference - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - 'reference[key]', - $entity->reference['key'] - ), - )); - - $this->assertEquals($violations, $this->visitor->getViolations()); - } - - public function testValidateCascadedPropertyDoesNotValidateTraversableIfDisabled() - { - $entity = new Entity(); - $entity->reference = new \ArrayIterator(array('key' => new Entity())); - - $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); - - // add a constraint for the entity that always fails - $this->metadata->addConstraint(new FailingConstraint()); - - // validate array when validating the property "reference" - $this->metadata->addPropertyConstraint('reference', new Valid(array( - 'traverse' => false, - ))); - - $this->visitor->validate($entity, 'Default', ''); - - $violations = new ConstraintViolationList(array( - // generated by the root object - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - '', - $entity - ), - // nothing generated by the reference! - )); - - $this->assertEquals($violations, $this->visitor->getViolations()); - } - - public function testMetadataMayNotExistIfTraversalIsEnabled() - { - $entity = new Entity(); - $entity->reference = new \ArrayIterator(); - - $this->metadata->addPropertyConstraint('reference', new Valid(array( - 'traverse' => true, - ))); - - $this->visitor->validate($entity, 'Default', ''); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException - */ - public function testMetadataMustExistIfTraversalIsDisabled() - { - $entity = new Entity(); - $entity->reference = new \ArrayIterator(); - - $this->metadata->addPropertyConstraint('reference', new Valid(array( - 'traverse' => false, - ))); - - $this->visitor->validate($entity, 'Default', ''); - } - - public function testValidateCascadedPropertyDoesNotRecurseByDefault() - { - $entity = new Entity(); - $entity->reference = new \ArrayIterator(array( - // The inner iterator should not be traversed by default - 'key' => new \ArrayIterator(array( - 'nested' => new Entity(), - )), - )); - - $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); - - // add a constraint for the entity that always fails - $this->metadata->addConstraint(new FailingConstraint()); - - // validate iterator when validating the property "reference" - $this->metadata->addPropertyConstraint('reference', new Valid()); - - $this->visitor->validate($entity, 'Default', ''); - - $violations = new ConstraintViolationList(array( - // generated by the root object - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - '', - $entity - ), - // nothing generated by the reference! - )); - - $this->assertEquals($violations, $this->visitor->getViolations()); - } - - // https://github.com/symfony/symfony/issues/6246 - public function testValidateCascadedPropertyRecursesArraysByDefault() - { - $entity = new Entity(); - $entity->reference = array( - 'key' => array( - 'nested' => new Entity(), - ), - ); - - // add a constraint for the entity that always fails - $this->metadata->addConstraint(new FailingConstraint()); - - // validate iterator when validating the property "reference" - $this->metadata->addPropertyConstraint('reference', new Valid()); - - $this->visitor->validate($entity, 'Default', ''); - - $violations = new ConstraintViolationList(array( - // generated by the root object - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - '', - $entity - ), - // nothing generated by the reference! - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - 'reference[key][nested]', - $entity->reference['key']['nested'] - ), - )); - - $this->assertEquals($violations, $this->visitor->getViolations()); - } - - public function testValidateCascadedPropertyRecursesIfDeepIsSet() - { - $entity = new Entity(); - $entity->reference = new \ArrayIterator(array( - // The inner iterator should now be traversed - 'key' => new \ArrayIterator(array( - 'nested' => new Entity(), - )), - )); - - // add a constraint for the entity that always fails - $this->metadata->addConstraint(new FailingConstraint()); - - // validate iterator when validating the property "reference" - $this->metadata->addPropertyConstraint('reference', new Valid(array( - 'deep' => true, - ))); - - $this->visitor->validate($entity, 'Default', ''); - - $violations = new ConstraintViolationList(array( - // generated by the root object - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - '', - $entity - ), - // nothing generated by the reference! - new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Root', - 'reference[key][nested]', - $entity->reference['key']['nested'] - ), - )); - - $this->assertEquals($violations, $this->visitor->getViolations()); - } - - public function testValidateCascadedPropertyDoesNotValidateNestedScalarValues() - { - $entity = new Entity(); - $entity->reference = array('scalar', 'values'); - - // validate array when validating the property "reference" - $this->metadata->addPropertyConstraint('reference', new Valid()); - - $this->visitor->validate($entity, 'Default', ''); - - $this->assertCount(0, $this->visitor->getViolations()); - } - - public function testValidateCascadedPropertyDoesNotValidateNullValues() - { - $entity = new Entity(); - $entity->reference = null; - - $this->metadata->addPropertyConstraint('reference', new Valid()); - - $this->visitor->validate($entity, 'Default', ''); - - $this->assertCount(0, $this->visitor->getViolations()); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException - */ - public function testValidateCascadedPropertyRequiresObjectOrArray() - { - $entity = new Entity(); - $entity->reference = 'no object'; - - $this->metadata->addPropertyConstraint('reference', new Valid()); - - $this->visitor->validate($entity, 'Default', ''); - } -} diff --git a/src/Symfony/Component/Validator/Tests/ValidatorTest.php b/src/Symfony/Component/Validator/Tests/ValidatorTest.php index 85a61e4816da8..52bdbea519af2 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorTest.php @@ -11,266 +11,15 @@ namespace Symfony\Component\Validator\Tests; -use Symfony\Component\Validator\Tests\Fixtures\Entity; -use Symfony\Component\Validator\Tests\Fixtures\GroupSequenceProviderEntity; -use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; -use Symfony\Component\Validator\Tests\Fixtures\FailingConstraint; +use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Validator; use Symfony\Component\Validator\DefaultTranslator; -use Symfony\Component\Validator\ConstraintViolation; -use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\ConstraintValidatorFactory; -use Symfony\Component\Validator\Constraints\Valid; -use Symfony\Component\Validator\Mapping\ClassMetadata; -class ValidatorTest extends \PHPUnit_Framework_TestCase +class ValidatorTest extends AbstractValidatorTest { - /** - * @var FakeMetadataFactory - */ - private $metadataFactory; - - /** - * @var Validator - */ - private $validator; - - protected function setUp() - { - $this->metadataFactory = new FakeMetadataFactory(); - $this->validator = new Validator($this->metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); - } - - protected function tearDown() - { - $this->metadataFactory = null; - $this->validator = null; - } - - public function testValidateDefaultGroup() - { - $entity = new Entity(); - $metadata = new ClassMetadata(get_class($entity)); - $metadata->addPropertyConstraint('firstName', new FailingConstraint()); - $metadata->addPropertyConstraint('lastName', new FailingConstraint(array( - 'groups' => 'Custom', - ))); - $this->metadataFactory->addMetadata($metadata); - - // Only the constraint of group "Default" failed - $violations = new ConstraintViolationList(); - $violations->add(new ConstraintViolation( - 'Failed', - 'Failed', - array(), - $entity, - 'firstName', - '' - )); - - $this->assertEquals($violations, $this->validator->validate($entity)); - } - - public function testValidateOneGroup() - { - $entity = new Entity(); - $metadata = new ClassMetadata(get_class($entity)); - $metadata->addPropertyConstraint('firstName', new FailingConstraint()); - $metadata->addPropertyConstraint('lastName', new FailingConstraint(array( - 'groups' => 'Custom', - ))); - $this->metadataFactory->addMetadata($metadata); - - // Only the constraint of group "Custom" failed - $violations = new ConstraintViolationList(); - $violations->add(new ConstraintViolation( - 'Failed', - 'Failed', - array(), - $entity, - 'lastName', - '' - )); - - $this->assertEquals($violations, $this->validator->validate($entity, 'Custom')); - } - - public function testValidateMultipleGroups() - { - $entity = new Entity(); - $metadata = new ClassMetadata(get_class($entity)); - $metadata->addPropertyConstraint('firstName', new FailingConstraint(array( - 'groups' => 'First', - ))); - $metadata->addPropertyConstraint('lastName', new FailingConstraint(array( - 'groups' => 'Second', - ))); - $this->metadataFactory->addMetadata($metadata); - - // The constraints of both groups failed - $violations = new ConstraintViolationList(); - $violations->add(new ConstraintViolation( - 'Failed', - 'Failed', - array(), - $entity, - 'firstName', - '' - )); - $violations->add(new ConstraintViolation( - 'Failed', - 'Failed', - array(), - $entity, - 'lastName', - '' - )); - - $result = $this->validator->validate($entity, array('First', 'Second')); - - $this->assertEquals($violations, $result); - } - - public function testValidateGroupSequenceProvider() - { - $entity = new GroupSequenceProviderEntity(); - $metadata = new ClassMetadata(get_class($entity)); - $metadata->addPropertyConstraint('firstName', new FailingConstraint(array( - 'groups' => 'First', - ))); - $metadata->addPropertyConstraint('lastName', new FailingConstraint(array( - 'groups' => 'Second', - ))); - $metadata->setGroupSequenceProvider(true); - $this->metadataFactory->addMetadata($metadata); - - $violations = new ConstraintViolationList(); - $violations->add(new ConstraintViolation( - 'Failed', - 'Failed', - array(), - $entity, - 'firstName', - '' - )); - - $entity->setGroups(array('First')); - $result = $this->validator->validate($entity); - $this->assertEquals($violations, $result); - - $violations = new ConstraintViolationList(); - $violations->add(new ConstraintViolation( - 'Failed', - 'Failed', - array(), - $entity, - 'lastName', - '' - )); - - $entity->setGroups(array('Second')); - $result = $this->validator->validate($entity); - $this->assertEquals($violations, $result); - - $entity->setGroups(array()); - $result = $this->validator->validate($entity); - $this->assertEquals(new ConstraintViolationList(), $result); - } - - public function testValidateProperty() - { - $entity = new Entity(); - $metadata = new ClassMetadata(get_class($entity)); - $metadata->addPropertyConstraint('firstName', new FailingConstraint()); - $this->metadataFactory->addMetadata($metadata); - - $result = $this->validator->validateProperty($entity, 'firstName'); - - $this->assertCount(1, $result); - - $result = $this->validator->validateProperty($entity, 'lastName'); - - $this->assertCount(0, $result); - } - - public function testValidatePropertyValue() - { - $entity = new Entity(); - $metadata = new ClassMetadata(get_class($entity)); - $metadata->addPropertyConstraint('firstName', new FailingConstraint()); - $this->metadataFactory->addMetadata($metadata); - - $result = $this->validator->validatePropertyValue(get_class($entity), 'firstName', 'Bernhard'); - - $this->assertCount(1, $result); - } - - public function testValidateValue() - { - $violations = new ConstraintViolationList(); - $violations->add(new ConstraintViolation( - 'Failed', - 'Failed', - array(), - 'Bernhard', - '', - 'Bernhard' - )); - - $this->assertEquals($violations, $this->validator->validateValue('Bernhard', new FailingConstraint())); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\ValidatorException - */ - public function testValidateValueRejectsValid() - { - $entity = new Entity(); - $metadata = new ClassMetadata(get_class($entity)); - $this->metadataFactory->addMetadata($metadata); - - $this->validator->validateValue($entity, new Valid()); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\ValidatorException - */ - public function testValidatePropertyFailsIfPropertiesNotSupported() - { - // $metadata does not implement PropertyMetadataContainerInterface - $metadata = $this->getMock('Symfony\Component\Validator\MetadataInterface'); - $this->metadataFactory = $this->getMock('Symfony\Component\Validator\MetadataFactoryInterface'); - $this->metadataFactory->expects($this->any()) - ->method('getMetadataFor') - ->with('VALUE') - ->will($this->returnValue($metadata)); - $this->validator = new Validator($this->metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); - - $this->validator->validateProperty('VALUE', 'someProperty'); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\ValidatorException - */ - public function testValidatePropertyValueFailsIfPropertiesNotSupported() - { - // $metadata does not implement PropertyMetadataContainerInterface - $metadata = $this->getMock('Symfony\Component\Validator\MetadataInterface'); - $this->metadataFactory = $this->getMock('Symfony\Component\Validator\MetadataFactoryInterface'); - $this->metadataFactory->expects($this->any()) - ->method('getMetadataFor') - ->with('VALUE') - ->will($this->returnValue($metadata)); - $this->validator = new Validator($this->metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); - - $this->validator->validatePropertyValue('VALUE', 'someProperty', 'propertyValue'); - } - - public function testGetMetadataFactory() + protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $this->assertInstanceOf( - 'Symfony\Component\Validator\MetadataFactoryInterface', - $this->validator->getMetadataFactory() - ); + return new Validator($metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); } } From a6ed4cae5daeebf9dec20bba359f0f1c9be9c178 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Feb 2014 14:12:00 +0100 Subject: [PATCH 02/86] [Validator] Prototype of the traverser implementation --- .../Validator/Constraints/GroupSequence.php | 18 ++ .../Validator/Context/ExecutionContext.php | 174 ++++++++++++++++++ .../Context/ExecutionContextInterface.php | 154 ++++++++++++++++ .../Context/ExecutionContextManager.php | 130 +++++++++++++ .../ExecutionContextManagerInterface.php | 34 ++++ .../Context/LegacyExecutionContext.php | 41 +++++ .../Validator/Group/GroupManagerInterface.php | 21 +++ .../Mapping/ClassMetadataInterface.php | 44 +++++ .../Validator/Mapping/MetadataInterface.php | 57 ++++++ .../Mapping/PropertyMetadataInterface.php | 46 +++++ .../Validator/Mapping/ValueMetadata.php | 46 +++++ .../Component/Validator/Node/ClassNode.php | 41 +++++ src/Symfony/Component/Validator/Node/Node.php | 37 ++++ .../Component/Validator/Node/PropertyNode.php | 37 ++++ .../Component/Validator/Node/ValueNode.php | 20 ++ .../NodeTraverser/AbstractVisitor.php | 37 ++++ .../Validator/NodeTraverser/NodeTraverser.php | 166 +++++++++++++++++ .../NodeTraverser/NodeTraverserInterface.php | 32 ++++ .../NodeTraverser/NodeVisitorInterface.php | 29 +++ .../Validator/TraversingValidatorTest.php | 41 +++++ .../Validator/Validator/AbstractValidator.php | 155 ++++++++++++++++ .../Validator/ContextualValidator.php | 123 +++++++++++++ .../ContextualValidatorInterface.php | 26 +++ .../Validator/Validator/LegacyValidator.php | 27 +++ .../Validator/Validator/NodeValidator.php | 155 ++++++++++++++++ .../Validator/Validator/Validator.php | 72 ++++++++ .../Validator/ValidatorInterface.php | 88 +++++++++ 27 files changed, 1851 insertions(+) create mode 100644 src/Symfony/Component/Validator/Context/ExecutionContext.php create mode 100644 src/Symfony/Component/Validator/Context/ExecutionContextInterface.php create mode 100644 src/Symfony/Component/Validator/Context/ExecutionContextManager.php create mode 100644 src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php create mode 100644 src/Symfony/Component/Validator/Context/LegacyExecutionContext.php create mode 100644 src/Symfony/Component/Validator/Group/GroupManagerInterface.php create mode 100644 src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php create mode 100644 src/Symfony/Component/Validator/Mapping/MetadataInterface.php create mode 100644 src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php create mode 100644 src/Symfony/Component/Validator/Mapping/ValueMetadata.php create mode 100644 src/Symfony/Component/Validator/Node/ClassNode.php create mode 100644 src/Symfony/Component/Validator/Node/Node.php create mode 100644 src/Symfony/Component/Validator/Node/PropertyNode.php create mode 100644 src/Symfony/Component/Validator/Node/ValueNode.php create mode 100644 src/Symfony/Component/Validator/NodeTraverser/AbstractVisitor.php create mode 100644 src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php create mode 100644 src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php create mode 100644 src/Symfony/Component/Validator/NodeTraverser/NodeVisitorInterface.php create mode 100644 src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php create mode 100644 src/Symfony/Component/Validator/Validator/AbstractValidator.php create mode 100644 src/Symfony/Component/Validator/Validator/ContextualValidator.php create mode 100644 src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php create mode 100644 src/Symfony/Component/Validator/Validator/LegacyValidator.php create mode 100644 src/Symfony/Component/Validator/Validator/NodeValidator.php create mode 100644 src/Symfony/Component/Validator/Validator/Validator.php create mode 100644 src/Symfony/Component/Validator/Validator/ValidatorInterface.php diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index 61f72c6231cf9..7985b6cc9726b 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Validator\Constraints; +use Traversable; + /** * Annotation for group sequences * @@ -28,6 +30,22 @@ class GroupSequence implements \ArrayAccess, \IteratorAggregate, \Countable */ public $groups; + /** + * The group under which cascaded objects are validated when validating + * this sequence. + * + * By default, cascaded objects are validated in each of the groups of + * the sequence. + * + * If a class has a group sequence attached, that sequence replaces the + * "Default" group. When validating that class in the "Default" group, the + * group sequence is used instead, but still the "Default" group should be + * cascaded to other objects. + * + * @var string|GroupSequence + */ + public $cascadedGroup; + public function __construct(array $groups) { // Support for Doctrine annotations diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php new file mode 100644 index 0000000000000..09dc2644e2a1f --- /dev/null +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -0,0 +1,174 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +use Symfony\Component\Validator\ClassBasedInterface; +use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class ExecutionContext implements ExecutionContextInterface +{ + private $root; + + private $violations; + + /** + * @var Node + */ + private $node; + + /** + * @var \SplStack + */ + private $nodeStack; + + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + + /** + * @var ValidatorInterface + */ + private $validator; + + /** + * @var GroupManagerInterface + */ + private $groupManager; + + public function __construct(MetadataFactoryInterface $metadataFactory, ValidatorInterface $validator, GroupManagerInterface $groupManager) + { + $this->metadataFactory = $metadataFactory; + $this->validator = $validator; + $this->groupManager = $groupManager; + $this->violations = new ConstraintViolationList(); + } + + public function pushNode(Node $node) + { + if (null === $this->node) { + $this->root = $node->value; + } else { + $this->nodeStack->push($this->node); + } + + $this->node = $node; + } + + public function popNode() + { + $poppedNode = $this->node; + + if (0 === count($this->nodeStack)) { + $this->node = null; + + return $poppedNode; + } + + if (1 === count($this->nodeStack)) { + $this->nodeStack->pop(); + $this->node = null; + + return $poppedNode; + } + + $this->nodeStack->pop(); + $this->node = $this->nodeStack->top(); + + return $poppedNode; + } + + public function addViolation($message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) + { + } + + public function buildViolation($message) + { + + } + + public function getMetadataFor($object) + { + + } + + public function getViolations() + { + return $this->violations; + } + + public function getRoot() + { + return $this->root; + } + + public function getValue() + { + return $this->node ? $this->node->value : null; + } + + public function getMetadata() + { + return $this->node ? $this->node->metadata : null; + } + + public function getGroup() + { + return $this->groupManager->getCurrentGroup(); + } + + public function getClassName() + { + $metadata = $this->getMetadata(); + + return $metadata instanceof ClassBasedInterface ? $metadata->getClassName() : null; + } + + public function getPropertyName() + { + $metadata = $this->getMetadata(); + + return $metadata instanceof PropertyMetadataInterface ? $metadata->getPropertyName() : null; + } + + public function getPropertyPath($subPath = '') + { + $propertyPath = $this->node ? $this->node->propertyPath : ''; + + if (strlen($subPath) > 0) { + if ('[' === $subPath{1}) { + return $propertyPath.$subPath; + } + + return $propertyPath ? $propertyPath.'.'.$subPath : $subPath; + } + + return $propertyPath; + } + + /** + * @return ValidatorInterface + */ + public function getValidator() + { + return $this->validator; + } +} diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php new file mode 100644 index 0000000000000..c7ee62dc23ca8 --- /dev/null +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -0,0 +1,154 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +use Symfony\Component\Validator\ConstraintViolationListInterface; +use Symfony\Component\Validator\Mapping\MetadataInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface ExecutionContextInterface +{ + /** + * @return ValidatorInterface + */ + public function getValidator(); + + /** + * 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. + * + * @api + */ + public function addViolation($message, array $params = array()); + + public function buildViolation($message); + + /** + * Returns the violations generated by the validator so far. + * + * @return ConstraintViolationListInterface The constraint violation list. + * + * @api + */ + public function getViolations(); + + /** + * Returns the value at which validation was started in the object graph. + * + * The validator, when given an object, traverses the properties and + * related objects and their properties. The root of the validation is the + * object from which the traversal started. + * + * The current value is returned by {@link getValue}. + * + * @return mixed The root value of the validation. + */ + public function getRoot(); + + /** + * Returns the value that the validator is currently validating. + * + * If you want to retrieve the object that was originally passed to the + * validator, use {@link getRoot}. + * + * @return mixed The currently validated value. + */ + public function getValue(); + + /** + * Returns the metadata for the currently validated value. + * + * With the core implementation, this method returns a + * {@link Mapping\ClassMetadata} instance if the current value is an object, + * a {@link Mapping\PropertyMetadata} instance if the current value is + * the value of a property and a {@link Mapping\GetterMetadata} instance if + * the validated value is the result of a getter method. + * + * If the validated value is neither of these, for example if the validator + * has been called with a plain value and constraint, this method returns + * null. + * + * @return MetadataInterface|null The metadata of the currently validated + * value. + */ + public function getMetadata(); + + public function getMetadataFor($object); + + /** + * Returns the validation group that is currently being validated. + * + * @return string The current validation group. + */ + public function getGroup(); + + /** + * Returns the class name of the current node. + * + * If the metadata of the current node does not implement + * {@link ClassBasedInterface} or if no metadata is available for the + * current node, this method returns null. + * + * @return string|null The class name or null, if no class name could be found. + */ + public function getClassName(); + + /** + * Returns the property name of the current node. + * + * If the metadata of the current node does not implement + * {@link PropertyMetadataInterface} or if no metadata is available for the + * current node, this method returns null. + * + * @return string|null The property name or null, if no property name could be found. + */ + public function getPropertyName(); + + /** + * Returns the property path to the value that the validator is currently + * validating. + * + * For example, take the following object graph: + * + *
+     * (Person)---($address: Address)---($street: string)
+     * 
+ * + * When the Person instance is passed to the validator, the + * property path is initially empty. When the $address property + * of that person is validated, the property path is "address". When + * the $street property of the related Address instance + * is validated, the property path is "address.street". + * + * Properties of objects are prefixed with a dot in the property path. + * Indices of arrays or objects implementing the {@link \ArrayAccess} + * interface are enclosed in brackets. For example, if the property in + * the previous example is $addresses and contains an array + * of Address instance, the property path generated for the + * $street property of one of these addresses is for example + * "addresses[0].street". + * + * @param string $subPath Optional. The suffix appended to the current + * property path. + * + * @return string The current property path. The result may be an empty + * string if the validator is currently validating the + * root value of the validation graph. + */ + public function getPropertyPath($subPath = ''); +} diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php new file mode 100644 index 0000000000000..08442b94e8263 --- /dev/null +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\NodeTraverser\AbstractVisitor; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class ExecutionContextManager extends AbstractVisitor implements ExecutionContextManagerInterface +{ + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + + /** + * @var GroupManagerInterface + */ + private $groupManager; + + /** + * @var ValidatorInterface + */ + private $validator; + + /** + * @var ExecutionContext + */ + private $currentContext; + + /** + * @var \SplStack|ExecutionContext[] + */ + private $contextStack; + + public function __construct(MetadataFactoryInterface $metadataFactory, GroupManagerInterface $groupManager) + { + $this->metadataFactory = $metadataFactory; + $this->groupManager = $groupManager; + + $this->reset(); + } + + public function initialize(ValidatorInterface $validator) + { + $this->validator = $validator; + } + + public function startContext() + { + if (null !== $this->currentContext) { + $this->contextStack->push($this->currentContext); + } + + $this->currentContext = new ExecutionContext($this->metadataFactory, $this->validator, $this->groupManager); + + return $this->currentContext; + } + + public function stopContext() + { + $stoppedContext = $this->currentContext; + + if (0 === count($this->contextStack)) { + $this->currentContext = null; + + return $stoppedContext; + } + + if (1 === count($this->contextStack)) { + $this->contextStack->pop(); + $this->currentContext = null; + + return $stoppedContext; + } + + $this->contextStack->pop(); + $this->currentContext = $this->contextStack->top(); + + return $stoppedContext; + } + + public function getCurrentContext() + { + return $this->currentContext; + } + + public function afterTraversal(array $nodes) + { + $this->reset(); + } + + public function enterNode(Node $node) + { + if (null === $this->currentContext) { + // error no context started + } + + $this->currentContext->pushNode($node); + } + + public function leaveNode(Node $node) + { + if (null === $this->currentContext) { + // error no context started + } + + $this->currentContext->popNode(); + } + + private function reset() + { + $this->contextStack = new \SplStack(); + } +} diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php new file mode 100644 index 0000000000000..0d79eb43bb3de --- /dev/null +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface ExecutionContextManagerInterface +{ + /** + * @return ExecutionContextInterface The started context + */ + public function startContext(); + + /** + * @return ExecutionContextInterface The stopped context + */ + public function stopContext(); + + /** + * @return ExecutionContextInterface The current context + */ + public function getCurrentContext(); +} diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php new file mode 100644 index 0000000000000..1981e0f00ed47 --- /dev/null +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class LegacyExecutionContext extends ExecutionContext implements LegacyExecutionContextInterface +{ + public function addViolationAt($subPath, $message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) + { + + } + + public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false) + { + + } + + public function validateValue($value, $constraints, $subPath = '', $groups = null) + { + + } + + public function getMetadataFactory() + { + + } +} diff --git a/src/Symfony/Component/Validator/Group/GroupManagerInterface.php b/src/Symfony/Component/Validator/Group/GroupManagerInterface.php new file mode 100644 index 0000000000000..94a0ab9544e10 --- /dev/null +++ b/src/Symfony/Component/Validator/Group/GroupManagerInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Group; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface GroupManagerInterface +{ + public function getCurrentGroup(); +} diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php new file mode 100644 index 0000000000000..fea3f7f1d7423 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping; + +use Symfony\Component\Validator\ClassBasedInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface ClassMetadataInterface extends MetadataInterface, ClassBasedInterface +{ + public function getConstrainedProperties(); + + public function hasPropertyMetadata($property); + + /** + * Returns all metadata instances for the given named property. + * + * If your implementation does not support properties, simply throw an + * exception in this method (for example a BadMethodCallException). + * + * @param string $property The property name. + * + * @return PropertyMetadataInterface[] A list of metadata instances. Empty if + * no metadata exists for the property. + */ + public function getPropertyMetadata($property); + + public function hasGroupSequence(); + + public function getGroupSequence(); + + public function isGroupSequenceProvider(); +} diff --git a/src/Symfony/Component/Validator/Mapping/MetadataInterface.php b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php new file mode 100644 index 0000000000000..3df0d9bc0d587 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping; + +/** + * A container for validation metadata. + * + * The container contains constraints that may belong to different validation + * groups. Constraints for a specific group can be fetched by calling + * {@link findConstraints}. + * + * Implement this interface to add validation metadata to your own metadata + * layer. Each metadata may have named properties. Each property can be + * represented by one or more {@link PropertyMetadataInterface} instances that + * are returned by {@link getPropertyMetadata}. Since + * PropertyMetadataInterface inherits from MetadataInterface, + * each property may be divided into further properties. + * + * The {@link accept} method of each metadata implements the Visitor pattern. + * The method should forward the call to the visitor's + * {@link ValidationVisitorInterface::visit} method and additionally call + * accept() on all structurally related metadata instances. + * + * For example, to store constraints for PHP classes and their properties, + * create a class ClassMetadata (implementing MetadataInterface) + * and a class PropertyMetadata (implementing PropertyMetadataInterface). + * ClassMetadata::getPropertyMetadata($property) returns all + * PropertyMetadata instances for a property of that class. Its + * accept()-method simply forwards to ValidationVisitorInterface::visit() + * and calls accept() on all contained PropertyMetadata + * instances, which themselves call ValidationVisitorInterface::visit() + * again. + * + * @author Bernhard Schussek + */ +interface MetadataInterface +{ + /** + * 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 supportsCascading(); +} diff --git a/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php new file mode 100644 index 0000000000000..78da11b9074a9 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping; + +use Symfony\Component\Validator\ClassBasedInterface; + +/** + * A container for validation metadata of a property. + * + * What exactly you define as "property" is up to you. The validator expects + * implementations of {@link MetadataInterface} that contain constraints and + * optionally a list of named properties that also have constraints (and may + * have further sub properties). Such properties are mapped by implementations + * of this interface. + * + * @author Bernhard Schussek + * + * @see MetadataInterface + */ +interface PropertyMetadataInterface extends MetadataInterface, ClassBasedInterface +{ + /** + * Returns the name of the property. + * + * @return string The property name. + */ + public function getPropertyName(); + + /** + * Extracts the value of the property from the given object. + * + * @param mixed $object The object to extract the property value from. + * + * @return mixed The value of the property. + */ + public function getPropertyValue($object); +} diff --git a/src/Symfony/Component/Validator/Mapping/ValueMetadata.php b/src/Symfony/Component/Validator/Mapping/ValueMetadata.php new file mode 100644 index 0000000000000..c51a6fa575dae --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/ValueMetadata.php @@ -0,0 +1,46 @@ + + * + * 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 ValueMetadata implements MetadataInterface +{ + /** + * 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 supportsCascading() + { + + } + + public function supportsIteration() + { + + } + + public function supportsRecursiveIteration() + { + + } +} diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php new file mode 100644 index 0000000000000..dfb06dbc0325e --- /dev/null +++ b/src/Symfony/Component/Validator/Node/ClassNode.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Node; + +use Symfony\Component\Validator\Mapping\ClassMetadataInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class ClassNode extends Node +{ + /** + * @var ClassMetadataInterface + */ + public $metadata; + + public function __construct($value, ClassMetadataInterface $metadata, $propertyPath, array $groups) + { + if (!is_object($value)) { + // error + } + + parent::__construct( + $value, + $metadata, + $propertyPath, + $groups + ); + } + +} diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php new file mode 100644 index 0000000000000..3dead5623d8ac --- /dev/null +++ b/src/Symfony/Component/Validator/Node/Node.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Node; + +use Symfony\Component\Validator\Mapping\MetadataInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +abstract class Node +{ + public $value; + + public $metadata; + + public $propertyPath; + + public $groups; + + public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups) + { + $this->value = $value; + $this->metadata = $metadata; + $this->propertyPath = $propertyPath; + $this->groups = $groups; + } +} diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php new file mode 100644 index 0000000000000..9424acb59f526 --- /dev/null +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Node; + +use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class PropertyNode extends Node +{ + /** + * @var PropertyMetadataInterface + */ + public $metadata; + + public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups) + { + parent::__construct( + $value, + $metadata, + $propertyPath, + $groups + ); + } + +} diff --git a/src/Symfony/Component/Validator/Node/ValueNode.php b/src/Symfony/Component/Validator/Node/ValueNode.php new file mode 100644 index 0000000000000..e0f77e41f19c0 --- /dev/null +++ b/src/Symfony/Component/Validator/Node/ValueNode.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Node; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class ValueNode extends Node +{ +} diff --git a/src/Symfony/Component/Validator/NodeTraverser/AbstractVisitor.php b/src/Symfony/Component/Validator/NodeTraverser/AbstractVisitor.php new file mode 100644 index 0000000000000..c03e87c18da5d --- /dev/null +++ b/src/Symfony/Component/Validator/NodeTraverser/AbstractVisitor.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\NodeTraverser; + +use Symfony\Component\Validator\Node\Node; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +abstract class AbstractVisitor implements NodeVisitorInterface +{ + public function beforeTraversal(array $nodes) + { + } + + public function afterTraversal(array $nodes) + { + } + + public function enterNode(Node $node) + { + } + + public function leaveNode(Node $node) + { + } +} diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php new file mode 100644 index 0000000000000..3d3c7bfe03ac8 --- /dev/null +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -0,0 +1,166 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\NodeTraverser; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class NodeTraverser implements NodeTraverserInterface +{ + /** + * @var NodeVisitorInterface[] + */ + private $visitors; + + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + + private $traversalStarted = false; + + public function __construct(MetadataFactoryInterface $metadataFactory) + { + $this->visitors = new \SplObjectStorage(); + $this->metadataFactory = $metadataFactory; + } + + public function addVisitor(NodeVisitorInterface $visitor) + { + $this->visitors->attach($visitor); + } + + public function removeVisitor(NodeVisitorInterface $visitor) + { + $this->visitors->detach($visitor); + } + + /** + * {@inheritdoc} + */ + public function traverse(array $nodes) + { + $isTopLevelCall = !$this->traversalStarted; + + if ($isTopLevelCall) { + $this->traversalStarted = true; + + foreach ($this->visitors as $visitor) { + /** @var NodeVisitorInterface $visitor */ + $visitor->beforeTraversal($nodes); + } + } + + foreach ($nodes as $node) { + if ($node instanceof ClassNode) { + $this->traverseClassNode($node); + } else { + $this->traverseNode($node); + } + } + + if ($isTopLevelCall) { + $this->traversalStarted = false; + + foreach ($this->visitors as $visitor) { + /** @var NodeVisitorInterface $visitor */ + $visitor->afterTraversal($nodes); + } + } + } + + private function traverseNode(Node $node) + { + $stopTraversal = false; + + foreach ($this->visitors as $visitor) { + if (false === $visitor->enterNode($node)) { + $stopTraversal = true; + } + } + + // 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); + + $this->traverseClassNode(new ClassNode( + $node->value, + $classMetadata, + $node->propertyPath, + $node->groups + )); + } + + foreach ($this->visitors as $visitor) { + $visitor->leaveNode($node); + } + } + + private function traverseClassNode(ClassNode $node) + { + // Replace "Default" group by the group sequence attached to the class + // (if any) + foreach ($node->groups as $key => $group) { + if (Constraint::DEFAULT_GROUP !== $group) { + continue; + } + + if ($node->metadata->hasGroupSequence()) { + $node->groups[$key] = $node->metadata->getGroupSequence(); + } elseif ($node->metadata->isGroupSequenceProvider()) { + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $node->groups[$key] = $value->getGroupSequence(); + } + + // Cascade the "Default" group when validating the sequence + $node->groups[$key]->cascadedGroup = Constraint::DEFAULT_GROUP; + + // "Default" group found, abort + break; + } + + $stopTraversal = false; + + foreach ($this->visitors as $visitor) { + if (false === $visitor->enterNode($node)) { + $stopTraversal = true; + } + } + + // Stop the traversal, but execute the leaveNode() methods anyway to + // perform possible cleanups + if (!$stopTraversal) { + foreach ($node->metadata->getConstrainedProperties() as $propertyName) { + foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { + $this->traverseNode(new PropertyNode( + $propertyMetadata->getPropertyValue($node->value), + $propertyMetadata, + $node->propertyPath + ? $node->propertyPath.'.'.$propertyName + : $propertyName, + $node->groups + )); + } + } + } + + foreach ($this->visitors as $visitor) { + $visitor->leaveNode($node); + } + } +} diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php new file mode 100644 index 0000000000000..048a1458b4760 --- /dev/null +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\NodeTraverser; + +use Symfony\Component\Validator\Node\Node; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface NodeTraverserInterface +{ + public function addVisitor(NodeVisitorInterface $visitor); + + public function removeVisitor(NodeVisitorInterface $visitor); + + /** + * @param Node[] $nodes + * + * @return mixed + */ + public function traverse(array $nodes); +} diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeVisitorInterface.php b/src/Symfony/Component/Validator/NodeTraverser/NodeVisitorInterface.php new file mode 100644 index 0000000000000..0a70cc13fe7da --- /dev/null +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeVisitorInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\NodeTraverser; + +use Symfony\Component\Validator\Node\Node; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface NodeVisitorInterface +{ + public function beforeTraversal(array $nodes); + + public function afterTraversal(array $nodes); + + public function enterNode(Node $node); + + public function leaveNode(Node $node); +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php new file mode 100644 index 0000000000000..4f3a212aa1864 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php @@ -0,0 +1,41 @@ + + * + * 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\Context\ExecutionContextManager; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Tests\AbstractValidatorTest; +use Symfony\Component\Validator\NodeTraverser\NodeTraverser; +use Symfony\Component\Validator\NodeTraverser\NodeVisitor\NodeValidator; +use Symfony\Component\Validator\DefaultTranslator; +use Symfony\Component\Validator\ConstraintValidatorFactory; +use Symfony\Component\Validator\Validator\Validator; + +class TraversingValidatorTest extends AbstractValidatorTest +{ + protected function createValidator(MetadataFactoryInterface $metadataFactory) + { + $validatorFactory = new ConstraintValidatorFactory(); + $nodeTraverser = new NodeTraverser($metadataFactory); + $nodeValidator = new NodeValidator($validatorFactory, $nodeTraverser); + $contextManager = new ExecutionContextManager($metadataFactory, $nodeValidator, new DefaultTranslator()); + $validator = new Validator($nodeTraverser, $metadataFactory, $contextManager); + + $contextManager->initialize($validator); + $nodeValidator->setContextManager($contextManager); + + $nodeTraverser->addVisitor($contextManager); + $nodeTraverser->addVisitor($nodeValidator); + + return $validator; + } +} diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php new file mode 100644 index 0000000000000..c0d9f54b52db1 --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -0,0 +1,155 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Validator; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Mapping\ClassMetadataInterface; +use Symfony\Component\Validator\Mapping\ValueMetadata; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeTraverser\ClassNode; +use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; +use Symfony\Component\Validator\NodeTraverser\PropertyNode; +use Symfony\Component\Validator\NodeTraverser\ValueNode; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +abstract class AbstractValidator implements ValidatorInterface +{ + /** + * @var NodeTraverserInterface + */ + protected $nodeTraverser; + + /** + * @var MetadataFactoryInterface + */ + protected $metadataFactory; + + /** + * @var string + */ + protected $defaultPropertyPath = ''; + + protected $defaultGroups = array(Constraint::DEFAULT_GROUP); + + public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory) + { + $this->nodeTraverser = $nodeTraverser; + $this->metadataFactory = $metadataFactory; + } + + /** + * @param ExecutionContextInterface $context + * + * @return ContextualValidatorInterface + */ + public function inContext(ExecutionContextInterface $context) + { + return new ContextualValidator($this->nodeTraverser, $this->metadataFactory, $context); + } + + public function getMetadataFactory() + { + return $this->metadataFactory; + } + + protected function traverseObject($object, $groups = null) + { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + // error + } + + $this->nodeTraverser->traverse(array(new ClassNode( + $object, + $classMetadata, + $this->defaultPropertyPath, + // TODO use cascade group here + $groups ? $this->normalizeGroups($groups) : $this->defaultGroups + ))); + } + + protected function traverseProperty($object, $propertyName, $groups = null) + { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + // error + } + + $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $nodes = array(); + + foreach ($propertyMetadatas as $propertyMetadata) { + $propertyValue = $propertyMetadata->getPropertyValue($object); + + $nodes[] = new PropertyNode( + $propertyValue, + $propertyMetadata, + $this->defaultPropertyPath, + $groups + ); + } + + $this->nodeTraverser->traverse($nodes); + } + + protected function traversePropertyValue($object, $propertyName, $value, $groups = null) + { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + // error + } + + $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $nodes = array(); + + foreach ($propertyMetadatas as $propertyMetadata) { + $nodes[] = new PropertyNode( + $value, + $propertyMetadata, + $this->defaultPropertyPath, + $groups + ); + } + + $this->nodeTraverser->traverse($nodes); + } + + protected function traverseValue($value, $constraints, $groups = null) + { + $metadata = new ValueMetadata($constraints); + + $this->nodeTraverser->traverse(array(new ValueNode( + $value, + $metadata, + $this->defaultPropertyPath, + $groups ? $this->normalizeGroups($groups) : $this->defaultGroups + ))); + } + + protected function normalizeGroups($groups) + { + if (is_array($groups)) { + return $groups; + } + + return array($groups); + } +} diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php new file mode 100644 index 0000000000000..560f088522992 --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -0,0 +1,123 @@ + + * + * 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\ConstraintViolationListInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class ContextualValidator extends AbstractValidator implements ContextualValidatorInterface +{ + /** + * @var ExecutionContextManagerInterface + */ + private $context; + + public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory, ExecutionContextInterface $context) + { + parent::__construct($nodeTraverser, $metadataFactory); + + $this->context = $context; + $this->defaultPropertyPath = $context->getPropertyPath(); + $this->defaultGroups = array($context->getGroup()); + } + + public function atPath($subPath) + { + $this->defaultPropertyPath = $this->context->getPropertyPath($subPath); + } + + /** + * Validates a value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param mixed $object The value to validate + * @param array|null $groups The validation groups to validate. + * + * @return ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. + */ + public function validateObject($object, $groups = null) + { + $this->traverseObject($object, $groups); + + return $this->context->getViolations(); + } + + /** + * Validates a property of a value against its current value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param mixed $object The value containing the property. + * @param string $propertyName The name of the property to validate. + * @param array|null $groups The validation groups to validate. + * + * @return ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. + */ + public function validateProperty($object, $propertyName, $groups = null) + { + $this->traverseProperty($object, $propertyName, $groups); + + return $this->context->getViolations(); + } + + /** + * Validate a property of a value against a potential value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param string $object The value containing the property. + * @param string $propertyName The name of the property to validate + * @param string $value The value to validate against the + * constraints of the property. + * @param array|null $groups The validation groups to validate. + * + * @return ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. + */ + public function validatePropertyValue($object, $propertyName, $value, $groups = null) + { + $this->traversePropertyValue($object, $propertyName, $value, $groups); + + return $this->context->getViolations(); + } + + /** + * Validates a value against a constraint or a list of constraints. + * + * @param mixed $value The value to validate. + * @param Constraint|Constraint[] $constraints The constraint(s) to validate against. + * @param array|null $groups The validation groups to validate. + * + * @return ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. + */ + public function validateValue($value, $constraints, $groups = null) + { + $this->traverseValue($value, $constraints, $groups); + + return $this->context->getViolations(); + } +} diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php new file mode 100644 index 0000000000000..61de8900f1431 --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Validator; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface ContextualValidatorInterface extends ValidatorInterface +{ + /** + * @param $subPath + * + * @return ContextualValidatorInterface + */ + public function atPath($subPath); +} diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php new file mode 100644 index 0000000000000..2227318289b8d --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.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\Validator; + +use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class LegacyValidator extends Validator implements LegacyValidatorInterface +{ + public function validate($value, $groups = null, $traverse = false, $deep = false) + { + // TODO what about $traverse and $deep? + return $this->validateObject($value, $groups); + } +} diff --git a/src/Symfony/Component/Validator/Validator/NodeValidator.php b/src/Symfony/Component/Validator/Validator/NodeValidator.php new file mode 100644 index 0000000000000..20177341a810c --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/NodeValidator.php @@ -0,0 +1,155 @@ + + * + * 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\Constraints\GroupSequence; +use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; +use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; +use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\NodeTraverser\AbstractVisitor; +use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class NodeValidator extends AbstractVisitor implements GroupManagerInterface +{ + private $validatedNodes = array(); + + /** + * @var ConstraintValidatorFactoryInterface + */ + private $validatorFactory; + + /** + * @var ExecutionContextManagerInterface + */ + private $contextManager; + + /** + * @var NodeTraverserInterface + */ + private $nodeTraverser; + + private $currentGroup; + + public function __construct(ConstraintValidatorFactoryInterface $validatorFactory, NodeTraverserInterface $nodeTraverser) + { + $this->validatorFactory = $validatorFactory; + $this->nodeTraverser = $nodeTraverser; + } + + public function setContextManager(ExecutionContextManagerInterface $contextManager) + { + $this->contextManager = $contextManager; + } + + public function afterTraversal(array $nodes) + { + $this->validatedNodes = array(); + } + + public function enterNode(Node $node) + { + $cacheKey = $node instanceof ClassNode + ? spl_object_hash($node->value) + : 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 $group) { + // Validate object nodes only once per group + if (null !== $cacheKey) { + // Use the object hash for group sequences + $groupKey = is_object($group) ? spl_object_hash($group) : $group; + + // Exit, if the object is already validated for the current group + if (isset($this->validatedNodes[$cacheKey][$groupKey])) { + return false; + } + + // Remember validating this object before starting and possibly + // traversing the object graph + $this->validatedNodes[$cacheKey][$groupKey] = true; + } + + // Validate group sequence until a violation is generated + if ($group instanceof GroupSequence) { + // Rename for clarity + $groupSequence = $group; + + // Only evaluate group sequences at class, not at property level + if (!$node instanceof ClassNode) { + continue; + } + + $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) + ))); + + // Abort sequence validation if a violation was generated + if (count($context->getViolations()) > $violationCount) { + break; + } + } + + // Optimization: If the groups only contain the group sequence, + // we can skip the traversal for the properties of the object + if (1 === count($node->groups)) { + return false; + } + + // We're done for the current loop execution. + continue; + } + + // Validate normal group (non group sequences) + try { + $this->currentGroup = $group; + + foreach ($node->metadata->findConstraints($group) as $constraint) { + $validator = $this->validatorFactory->getInstance($constraint); + $validator->initialize($this->contextManager->getCurrentContext()); + $validator->validate($node->value, $constraint); + } + + $this->currentGroup = null; + } catch (\Exception $e) { + $this->currentGroup = null; + + throw $e; + } + } + + return true; + } + + public function getCurrentGroup() + { + return $this->currentGroup; + } +} diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php new file mode 100644 index 0000000000000..ed222d57c81dc --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Validator; + +use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; +use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class Validator extends AbstractValidator +{ + /** + * @var ExecutionContextManagerInterface + */ + private $contextManager; + + public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory, ExecutionContextManagerInterface $contextManager) + { + parent::__construct($nodeTraverser, $metadataFactory); + + $this->contextManager = $contextManager; + } + + public function validateObject($object, $groups = null) + { + $this->contextManager->startContext(); + + $this->traverseObject($object, $groups); + + return $this->contextManager->stopContext()->getViolations(); + } + + public function validateProperty($object, $propertyName, $groups = null) + { + $this->contextManager->startContext(); + + $this->traverseProperty($object, $propertyName, $groups); + + return $this->contextManager->stopContext()->getViolations(); + } + + public function validatePropertyValue($object, $propertyName, $value, $groups = null) + { + $this->contextManager->startContext(); + + $this->traversePropertyValue($object, $propertyName, $value, $groups); + + return $this->contextManager->stopContext()->getViolations(); + } + + public function validateValue($value, $constraints, $groups = null) + { + $this->contextManager->startContext(); + + $this->traverseValue($value, $constraints, $groups); + + return $this->contextManager->stopContext()->getViolations(); + } + +} diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php new file mode 100644 index 0000000000000..4f557710c4cf1 --- /dev/null +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -0,0 +1,88 @@ + + * + * 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\ConstraintViolationListInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface ValidatorInterface +{ + /** + * Validates a value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param mixed $object The value to validate + * @param array|null $groups The validation groups to validate. + * + * @return ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. + */ + public function validateObject($object, $groups = null); + + /** + * Validates a property of a value against its current value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param mixed $object The value containing the property. + * @param string $propertyName The name of the property to validate. + * @param array|null $groups The validation groups to validate. + * + * @return ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. + */ + public function validateProperty($object, $propertyName, $groups = null); + + /** + * Validate a property of a value against a potential value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param string $object The value containing the property. + * @param string $propertyName The name of the property to validate + * @param string $value The value to validate against the + * constraints of the property. + * @param array|null $groups The validation groups to validate. + * + * @return ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. + */ + public function validatePropertyValue($object, $propertyName, $value, $groups = null); + + /** + * Validates a value against a constraint or a list of constraints. + * + * @param mixed $value The value to validate. + * @param Constraint|Constraint[] $constraints The constraint(s) to validate against. + * @param array|null $groups The validation groups to validate. + * + * @return ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. + */ + public function validateValue($value, $constraints, $groups = null); + + /** + * @param ExecutionContextInterface $context + * + * @return ContextualValidatorInterface + */ + public function inContext(ExecutionContextInterface $context); +} From a40189ccb7c90c8919effa68261488103648d4aa Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Feb 2014 14:43:28 +0100 Subject: [PATCH 03/86] [Validator] Decoupled the new classes a bit --- .../Validator/Context/ExecutionContext.php | 14 +- .../Context/ExecutionContextInterface.php | 2 - .../Context/ExecutionContextManager.php | 11 +- .../Validator/NodeTraverser/NodeTraverser.php | 4 +- .../NodeTraverser/NodeTraverserInterface.php | 2 - .../Validator/TraversingValidatorTest.php | 14 +- .../Validator/Validator/AbstractValidator.php | 15 ++- .../Validator/Validator/NodeValidator.php | 121 ++++++++++-------- .../Validator/ValidatorInterface.php | 4 + 9 files changed, 95 insertions(+), 92 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 09dc2644e2a1f..ba4b0cb8018ff 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -15,7 +15,6 @@ use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; -use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -39,11 +38,6 @@ class ExecutionContext implements ExecutionContextInterface */ private $nodeStack; - /** - * @var MetadataFactoryInterface - */ - private $metadataFactory; - /** * @var ValidatorInterface */ @@ -54,9 +48,8 @@ class ExecutionContext implements ExecutionContextInterface */ private $groupManager; - public function __construct(MetadataFactoryInterface $metadataFactory, ValidatorInterface $validator, GroupManagerInterface $groupManager) + public function __construct(ValidatorInterface $validator, GroupManagerInterface $groupManager) { - $this->metadataFactory = $metadataFactory; $this->validator = $validator; $this->groupManager = $groupManager; $this->violations = new ConstraintViolationList(); @@ -105,11 +98,6 @@ public function buildViolation($message) } - public function getMetadataFor($object) - { - - } - public function getViolations() { return $this->violations; diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index c7ee62dc23ca8..4a8765f27be5c 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -88,8 +88,6 @@ public function getValue(); */ public function getMetadata(); - public function getMetadataFor($object); - /** * Returns the validation group that is currently being validated. * diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php index 08442b94e8263..c6b3da0601133 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Validator\Context; use Symfony\Component\Validator\Group\GroupManagerInterface; -use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\NodeTraverser\AbstractVisitor; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -23,11 +22,6 @@ */ class ExecutionContextManager extends AbstractVisitor implements ExecutionContextManagerInterface { - /** - * @var MetadataFactoryInterface - */ - private $metadataFactory; - /** * @var GroupManagerInterface */ @@ -48,9 +42,8 @@ class ExecutionContextManager extends AbstractVisitor implements ExecutionContex */ private $contextStack; - public function __construct(MetadataFactoryInterface $metadataFactory, GroupManagerInterface $groupManager) + public function __construct(GroupManagerInterface $groupManager) { - $this->metadataFactory = $metadataFactory; $this->groupManager = $groupManager; $this->reset(); @@ -67,7 +60,7 @@ public function startContext() $this->contextStack->push($this->currentContext); } - $this->currentContext = new ExecutionContext($this->metadataFactory, $this->validator, $this->groupManager); + $this->currentContext = new ExecutionContext($this->validator, $this->groupManager); return $this->currentContext; } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index 3d3c7bfe03ac8..b1962c263aee8 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -12,8 +12,10 @@ namespace Symfony\Component\Validator\NodeTraverser; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\Node\PropertyNode; /** * @since %%NextVersion%% diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php index 048a1458b4760..501c0ee7d1af4 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php @@ -25,8 +25,6 @@ public function removeVisitor(NodeVisitorInterface $visitor); /** * @param Node[] $nodes - * - * @return mixed */ public function traverse(array $nodes); } diff --git a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php index 4f3a212aa1864..2dc35387c799c 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php @@ -15,23 +15,27 @@ use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Tests\AbstractValidatorTest; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; -use Symfony\Component\Validator\NodeTraverser\NodeVisitor\NodeValidator; use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; +use Symfony\Component\Validator\Validator\NodeValidator; use Symfony\Component\Validator\Validator\Validator; class TraversingValidatorTest extends AbstractValidatorTest { protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $validatorFactory = new ConstraintValidatorFactory(); $nodeTraverser = new NodeTraverser($metadataFactory); - $nodeValidator = new NodeValidator($validatorFactory, $nodeTraverser); - $contextManager = new ExecutionContextManager($metadataFactory, $nodeValidator, new DefaultTranslator()); + $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); + $contextManager = new ExecutionContextManager($nodeValidator, new DefaultTranslator()); $validator = new Validator($nodeTraverser, $metadataFactory, $contextManager); + // The context manager needs the validator for passing it to created + // contexts $contextManager->initialize($validator); - $nodeValidator->setContextManager($contextManager); + + // The node validator needs the context manager for passing the current + // context to the constraint validators + $nodeValidator->initialize($contextManager); $nodeTraverser->addVisitor($contextManager); $nodeTraverser->addVisitor($nodeValidator); diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php index c0d9f54b52db1..5401fc6dbe21d 100644 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -16,10 +16,10 @@ use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\ValueMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeTraverser\ClassNode; +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\NodeTraverser\PropertyNode; -use Symfony\Component\Validator\NodeTraverser\ValueNode; /** * @since %%NextVersion%% @@ -60,9 +60,14 @@ public function inContext(ExecutionContextInterface $context) return new ContextualValidator($this->nodeTraverser, $this->metadataFactory, $context); } - public function getMetadataFactory() + public function getMetadataFor($object) { - return $this->metadataFactory; + return $this->metadataFactory->getMetadataFor($object); + } + + public function hasMetadataFor($object) + { + return $this->metadataFactory->hasMetadataFor($object); } protected function traverseObject($object, $groups = null) diff --git a/src/Symfony/Component/Validator/Validator/NodeValidator.php b/src/Symfony/Component/Validator/Validator/NodeValidator.php index 20177341a810c..7de32e88b13e8 100644 --- a/src/Symfony/Component/Validator/Validator/NodeValidator.php +++ b/src/Symfony/Component/Validator/Validator/NodeValidator.php @@ -26,7 +26,7 @@ */ class NodeValidator extends AbstractVisitor implements GroupManagerInterface { - private $validatedNodes = array(); + private $validatedObjects = array(); /** * @var ConstraintValidatorFactoryInterface @@ -45,25 +45,25 @@ class NodeValidator extends AbstractVisitor implements GroupManagerInterface private $currentGroup; - public function __construct(ConstraintValidatorFactoryInterface $validatorFactory, NodeTraverserInterface $nodeTraverser) + public function __construct(NodeTraverserInterface $nodeTraverser, ConstraintValidatorFactoryInterface $validatorFactory) { $this->validatorFactory = $validatorFactory; $this->nodeTraverser = $nodeTraverser; } - public function setContextManager(ExecutionContextManagerInterface $contextManager) + public function initialize(ExecutionContextManagerInterface $contextManager) { $this->contextManager = $contextManager; } public function afterTraversal(array $nodes) { - $this->validatedNodes = array(); + $this->validatedObjects = array(); } public function enterNode(Node $node) { - $cacheKey = $node instanceof ClassNode + $objectHash = $node instanceof ClassNode ? spl_object_hash($node->value) : null; @@ -75,73 +75,38 @@ public function enterNode(Node $node) foreach ($node->groups as $group) { // Validate object nodes only once per group - if (null !== $cacheKey) { + if (null !== $objectHash) { // Use the object hash for group sequences - $groupKey = is_object($group) ? spl_object_hash($group) : $group; + $groupHash = is_object($group) ? spl_object_hash($group) : $group; // Exit, if the object is already validated for the current group - if (isset($this->validatedNodes[$cacheKey][$groupKey])) { + if (isset($this->validatedObjects[$objectHash][$groupHash])) { return false; } // Remember validating this object before starting and possibly // traversing the object graph - $this->validatedNodes[$cacheKey][$groupKey] = true; + $this->validatedObjects[$objectHash][$groupHash] = true; } // Validate group sequence until a violation is generated - if ($group instanceof GroupSequence) { - // Rename for clarity - $groupSequence = $group; + if (!$group instanceof GroupSequence) { + $this->validateNodeForGroup($node, $group); - // Only evaluate group sequences at class, not at property level - if (!$node instanceof ClassNode) { - continue; - } - - $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) - ))); - - // Abort sequence validation if a violation was generated - if (count($context->getViolations()) > $violationCount) { - break; - } - } - - // Optimization: If the groups only contain the group sequence, - // we can skip the traversal for the properties of the object - if (1 === count($node->groups)) { - return false; - } - - // We're done for the current loop execution. continue; } - // Validate normal group (non group sequences) - try { - $this->currentGroup = $group; - - foreach ($node->metadata->findConstraints($group) as $constraint) { - $validator = $this->validatorFactory->getInstance($constraint); - $validator->initialize($this->contextManager->getCurrentContext()); - $validator->validate($node->value, $constraint); - } + // Only traverse group sequences at class, not at property level + if (!$node instanceof ClassNode) { + continue; + } - $this->currentGroup = null; - } catch (\Exception $e) { - $this->currentGroup = null; + $this->traverseGroupSequence($node, $group); - throw $e; + // Optimization: If the groups only contain the group sequence, + // we can skip the traversal for the properties of the object + if (1 === count($node->groups)) { + return false; } } @@ -152,4 +117,50 @@ public function getCurrentGroup() { return $this->currentGroup; } + + private function traverseGroupSequence(ClassNode $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) + ))); + + // Abort sequence validation if a violation was generated + if (count($context->getViolations()) > $violationCount) { + break; + } + } + } + + /** + * @param Node $node + * @param $group + * + * @throws \Exception + */ + private function validateNodeForGroup(Node $node, $group) + { + try { + $this->currentGroup = $group; + + foreach ($node->metadata->findConstraints($group) as $constraint) { + $validator = $this->validatorFactory->getInstance($constraint); + $validator->initialize($this->contextManager->getCurrentContext()); + $validator->validate($node->value, $constraint); + } + + $this->currentGroup = null; + } catch (\Exception $e) { + $this->currentGroup = null; + + throw $e; + } + } } diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index 4f557710c4cf1..f02ed79d9b756 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -85,4 +85,8 @@ public function validateValue($value, $constraints, $groups = null); * @return ContextualValidatorInterface */ public function inContext(ExecutionContextInterface $context); + + public function getMetadataFor($object); + + public function hasMetadataFor($object); } From 7e3a41d9db8a609f010ee404ffbf4fe397ea873b Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Feb 2014 15:03:04 +0100 Subject: [PATCH 04/86] [Validator] Moved visitors to NodeVisitor namespace --- .../Validator/Context/ExecutionContextManager.php | 2 +- .../Component/Validator/NodeTraverser/NodeTraverser.php | 1 + .../Validator/NodeTraverser/NodeTraverserInterface.php | 1 + .../{NodeTraverser => NodeVisitor}/AbstractVisitor.php | 2 +- .../{Validator => NodeVisitor}/NodeValidator.php | 3 +-- .../NodeVisitorInterface.php | 2 +- .../Validator/Tests/Validator/TraversingValidatorTest.php | 8 ++++---- .../Component/Validator/Validator/LegacyValidator.php | 5 +++++ 8 files changed, 15 insertions(+), 9 deletions(-) rename src/Symfony/Component/Validator/{NodeTraverser => NodeVisitor}/AbstractVisitor.php (92%) rename src/Symfony/Component/Validator/{Validator => NodeVisitor}/NodeValidator.php (97%) rename src/Symfony/Component/Validator/{NodeTraverser => NodeVisitor}/NodeVisitorInterface.php (91%) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php index c6b3da0601133..379b563ca2acd 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php @@ -13,7 +13,7 @@ use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\NodeTraverser\AbstractVisitor; +use Symfony\Component\Validator\NodeVisitor\AbstractVisitor; use Symfony\Component\Validator\Validator\ValidatorInterface; /** diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index b1962c263aee8..036566aa44471 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -16,6 +16,7 @@ use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Node\PropertyNode; +use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; /** * @since %%NextVersion%% diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php index 501c0ee7d1af4..d9ce42f025a8b 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\NodeTraverser; use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; /** * @since %%NextVersion%% diff --git a/src/Symfony/Component/Validator/NodeTraverser/AbstractVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php similarity index 92% rename from src/Symfony/Component/Validator/NodeTraverser/AbstractVisitor.php rename to src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php index c03e87c18da5d..31b49250eb8c9 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/AbstractVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Validator\NodeTraverser; +namespace Symfony\Component\Validator\NodeVisitor; use Symfony\Component\Validator\Node\Node; diff --git a/src/Symfony/Component/Validator/Validator/NodeValidator.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php similarity index 97% rename from src/Symfony/Component/Validator/Validator/NodeValidator.php rename to src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php index 7de32e88b13e8..c3b4fbe50223b 100644 --- a/src/Symfony/Component/Validator/Validator/NodeValidator.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Validator\Validator; +namespace Symfony\Component\Validator\NodeVisitor; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; @@ -17,7 +17,6 @@ use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\NodeTraverser\AbstractVisitor; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; /** diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeVisitorInterface.php b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php similarity index 91% rename from src/Symfony/Component/Validator/NodeTraverser/NodeVisitorInterface.php rename to src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php index 0a70cc13fe7da..a7542d515a73f 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeVisitorInterface.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Validator\NodeTraverser; +namespace Symfony\Component\Validator\NodeVisitor; use Symfony\Component\Validator\Node\Node; diff --git a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php index 2dc35387c799c..968fc7a79a642 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidatorTest.php @@ -11,13 +11,13 @@ namespace Symfony\Component\Validator\Tests\Validator; +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\Tests\AbstractValidatorTest; +use Symfony\Component\Validator\NodeVisitor\NodeValidator; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; -use Symfony\Component\Validator\DefaultTranslator; -use Symfony\Component\Validator\ConstraintValidatorFactory; -use Symfony\Component\Validator\Validator\NodeValidator; +use Symfony\Component\Validator\Tests\AbstractValidatorTest; use Symfony\Component\Validator\Validator\Validator; class TraversingValidatorTest extends AbstractValidatorTest diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 2227318289b8d..f8a2cc74c1bc8 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -24,4 +24,9 @@ public function validate($value, $groups = null, $traverse = false, $deep = fals // TODO what about $traverse and $deep? return $this->validateObject($value, $groups); } + + public function getMetadataFactory() + { + return $this->metadataFactory; + } } From b1a947737ad219e5cba373ccf712bfa5287b599e Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Feb 2014 15:15:09 +0100 Subject: [PATCH 05/86] [Validator] Added ObjectInitializer visitor --- .../Context/ExecutionContextManager.php | 10 +--- .../NodeVisitor/ObjectInitializer.php | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php index 379b563ca2acd..cfb93831f87d2 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php @@ -45,8 +45,7 @@ class ExecutionContextManager extends AbstractVisitor implements ExecutionContex public function __construct(GroupManagerInterface $groupManager) { $this->groupManager = $groupManager; - - $this->reset(); + $this->contextStack = new \SplStack(); } public function initialize(ValidatorInterface $validator) @@ -95,7 +94,7 @@ public function getCurrentContext() public function afterTraversal(array $nodes) { - $this->reset(); + $this->contextStack = new \SplStack(); } public function enterNode(Node $node) @@ -115,9 +114,4 @@ public function leaveNode(Node $node) $this->currentContext->popNode(); } - - private function reset() - { - $this->contextStack = new \SplStack(); - } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php new file mode 100644 index 0000000000000..617d10c84fd0d --- /dev/null +++ b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\NodeVisitor; + +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\ObjectInitializerInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class ObjectInitializer extends AbstractVisitor +{ + /** + * @var ObjectInitializerInterface[] + */ + private $initializers; + + public function __construct(array $initializers) + { + foreach ($initializers as $initializer) { + if (!$initializer instanceof ObjectInitializerInterface) { + throw new \LogicException('Validator initializers must implement ObjectInitializerInterface.'); + } + } + + $this->initializers = $initializers; + } + + public function enterNode(Node $node) + { + if ($node instanceof ClassNode) { + foreach ($this->initializers as $initializer) { + $initializer->initialize($node->value); + } + } + } +} From 1156bde82397c7029889eaf845e0158fe219dbd1 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Feb 2014 16:54:19 +0100 Subject: [PATCH 06/86] [Validator] Extracted code for group sequence resolving into GroupSequenceResolver --- .../Validator/NodeTraverser/NodeTraverser.php | 23 +------- .../NodeVisitor/GroupSequenceResolver.php | 49 ++++++++++++++++ .../Validator/NodeVisitor/NodeValidator.php | 58 ++++++++++++++----- .../NodeVisitor/ObjectInitializer.php | 10 ++-- 4 files changed, 101 insertions(+), 39 deletions(-) create mode 100644 src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index 036566aa44471..159f7fdd5ad6c 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -116,27 +116,6 @@ private function traverseNode(Node $node) private function traverseClassNode(ClassNode $node) { - // Replace "Default" group by the group sequence attached to the class - // (if any) - foreach ($node->groups as $key => $group) { - if (Constraint::DEFAULT_GROUP !== $group) { - continue; - } - - if ($node->metadata->hasGroupSequence()) { - $node->groups[$key] = $node->metadata->getGroupSequence(); - } elseif ($node->metadata->isGroupSequenceProvider()) { - /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $node->groups[$key] = $value->getGroupSequence(); - } - - // Cascade the "Default" group when validating the sequence - $node->groups[$key]->cascadedGroup = Constraint::DEFAULT_GROUP; - - // "Default" group found, abort - break; - } - $stopTraversal = false; foreach ($this->visitors as $visitor) { @@ -147,7 +126,7 @@ private function traverseClassNode(ClassNode $node) // Stop the traversal, but execute the leaveNode() methods anyway to // perform possible cleanups - if (!$stopTraversal) { + if (!$stopTraversal && count($node->groups) > 0) { foreach ($node->metadata->getConstrainedProperties() as $propertyName) { foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { $this->traverseNode(new PropertyNode( diff --git a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php new file mode 100644 index 0000000000000..8b9741cecccb5 --- /dev/null +++ b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\NodeVisitor; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\Node; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class GroupSequenceResolver extends AbstractVisitor +{ + public function enterNode(Node $node) + { + if (!$node instanceof ClassNode) { + return; + } + + if ($node->metadata->hasGroupSequence()) { + $groupSequence = $node->metadata->getGroupSequence(); + } elseif ($node->metadata->isGroupSequenceProvider()) { + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $groupSequence = $value->getGroupSequence(); + } else { + return; + } + + $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 + $node->groups[$key]->cascadedGroup = Constraint::DEFAULT_GROUP; + } + } +} diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php index c3b4fbe50223b..12fad3cf0edfd 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php @@ -17,6 +17,7 @@ use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; /** @@ -27,6 +28,8 @@ class NodeValidator extends AbstractVisitor implements GroupManagerInterface { private $validatedObjects = array(); + private $validatedConstraints = array(); + /** * @var ConstraintValidatorFactoryInterface */ @@ -44,10 +47,15 @@ class NodeValidator extends AbstractVisitor implements GroupManagerInterface private $currentGroup; + private $currentObjectHash; + + private $objectHashStack; + public function __construct(NodeTraverserInterface $nodeTraverser, ConstraintValidatorFactoryInterface $validatorFactory) { $this->validatorFactory = $validatorFactory; $this->nodeTraverser = $nodeTraverser; + $this->objectHashStack = new \SplStack(); } public function initialize(ExecutionContextManagerInterface $contextManager) @@ -58,13 +66,20 @@ public function initialize(ExecutionContextManagerInterface $contextManager) public function afterTraversal(array $nodes) { $this->validatedObjects = array(); + $this->validatedConstraints = array(); + $this->objectHashStack = new \SplStack(); } public function enterNode(Node $node) { - $objectHash = $node instanceof ClassNode - ? spl_object_hash($node->value) - : null; + if ($node instanceof ClassNode) { + $objectHash = spl_object_hash($node->value); + $this->objectHashStack->push($objectHash); + } elseif ($node instanceof PropertyNode) { + $objectHash = $this->objectHashStack->top(); + } else { + $objectHash = null; + } // if group (=[,G3,G4]) contains group sequence (=) // then call traverse() with each entry of the group sequence and abort @@ -72,25 +87,27 @@ public function enterNode(Node $node) // finally call traverse() with remaining entries ([G3,G4]) or // simply continue traversal (if possible) - foreach ($node->groups as $group) { - // Validate object nodes only once per group - if (null !== $objectHash) { + foreach ($node->groups as $key => $group) { + // Remember which object was validated for which group + // Skip validation if the object was already validated for this + // group + if ($node instanceof ClassNode) { // Use the object hash for group sequences $groupHash = is_object($group) ? spl_object_hash($group) : $group; - // Exit, if the object is already validated for the current group if (isset($this->validatedObjects[$objectHash][$groupHash])) { - return false; + // Skip this group when validating properties + unset($node->groups[$key]); + + continue; } - // Remember validating this object before starting and possibly - // traversing the object graph $this->validatedObjects[$objectHash][$groupHash] = true; } - // Validate group sequence until a violation is generated + // Validate normal group if (!$group instanceof GroupSequence) { - $this->validateNodeForGroup($node, $group); + $this->validateNodeForGroup($objectHash, $node, $group); continue; } @@ -100,6 +117,7 @@ public function enterNode(Node $node) continue; } + // Traverse group sequence until a violation is generated $this->traverseGroupSequence($node, $group); // Optimization: If the groups only contain the group sequence, @@ -139,17 +157,31 @@ private function traverseGroupSequence(ClassNode $node, GroupSequence $groupSequ } /** + * @param $objectHash * @param Node $node * @param $group * * @throws \Exception */ - private function validateNodeForGroup(Node $node, $group) + private function validateNodeForGroup($objectHash, Node $node, $group) { try { $this->currentGroup = $group; foreach ($node->metadata->findConstraints($group) as $constraint) { + // Remember the validated constraints of each object to prevent + // duplicate validation of constraints that belong to multiple + // validated groups + if (null !== $objectHash) { + $constraintHash = spl_object_hash($constraint); + + if (isset($this->validatedConstraints[$objectHash][$constraintHash])) { + continue; + } + + $this->validatedConstraints[$objectHash][$constraintHash] = true; + } + $validator = $this->validatorFactory->getInstance($constraint); $validator->initialize($this->contextManager->getCurrentContext()); $validator->validate($node->value, $constraint); diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php index 617d10c84fd0d..725c7b787eeca 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php @@ -39,10 +39,12 @@ public function __construct(array $initializers) public function enterNode(Node $node) { - if ($node instanceof ClassNode) { - foreach ($this->initializers as $initializer) { - $initializer->initialize($node->value); - } + if (!$node instanceof ClassNode) { + return; + } + + foreach ($this->initializers as $initializer) { + $initializer->initialize($node->value); } } } From 321d5bb30a37ec1807fb04d4093efe4fb7c908b9 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Feb 2014 16:56:58 +0100 Subject: [PATCH 07/86] [Validator] Throw exception if ObjectInitializer is constructed without visitors --- .../Component/Validator/NodeVisitor/ObjectInitializer.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php index 725c7b787eeca..bd000366c518e 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php @@ -30,10 +30,15 @@ public function __construct(array $initializers) { foreach ($initializers as $initializer) { if (!$initializer instanceof ObjectInitializerInterface) { - throw new \LogicException('Validator initializers must implement ObjectInitializerInterface.'); + throw new \InvalidArgumentException('Validator initializers must implement ObjectInitializerInterface.'); } } + // If no initializer is present, this visitor should not even be created + if (0 === count($initializers)) { + throw new \InvalidArgumentException('Please pass at least one initializer.'); + } + $this->initializers = $initializers; } From 680f1ee6c71943195fb7ded4cce2e6628020e065 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Feb 2014 18:34:00 +0100 Subject: [PATCH 08/86] [Validator] Renamed $params to $parameters --- src/Symfony/Component/Validator/ExecutionContext.php | 8 ++++---- .../Component/Validator/ExecutionContextInterface.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Validator/ExecutionContext.php b/src/Symfony/Component/Validator/ExecutionContext.php index 31a959187e354..00b8cca6369ee 100644 --- a/src/Symfony/Component/Validator/ExecutionContext.php +++ b/src/Symfony/Component/Validator/ExecutionContext.php @@ -115,14 +115,14 @@ public function addViolation($message, array $params = array(), $invalidValue = /** * {@inheritdoc} */ - public function addViolationAt($subPath, $message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) { $this->globalContext->getViolations()->add(new ConstraintViolation( null === $pluralization - ? $this->translator->trans($message, $params, $this->translationDomain) - : $this->translator->transChoice($message, $pluralization, $params, $this->translationDomain), + ? $this->translator->trans($message, $parameters, $this->translationDomain) + : $this->translator->transChoice($message, $pluralization, $parameters, $this->translationDomain), $message, - $params, + $parameters, $this->globalContext->getRoot(), $this->getPropertyPath($subPath), // check using func_num_args() to allow passing null values diff --git a/src/Symfony/Component/Validator/ExecutionContextInterface.php b/src/Symfony/Component/Validator/ExecutionContextInterface.php index 0b6c86633d6e5..92f4c5690b0af 100644 --- a/src/Symfony/Component/Validator/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/ExecutionContextInterface.php @@ -104,14 +104,14 @@ public function addViolation($message, array $params = array(), $invalidValue = * * @param string $subPath The relative property path for the violation. * @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. * @param mixed $invalidValue The invalid, validated value. * @param integer|null $pluralization The number to use to pluralize of the message. * @param integer|null $code The violation code. * * @api */ - public function addViolationAt($subPath, $message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null); + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null); /** * Validates the given value within the scope of the current validation. From 8ae68c9543c6f470bf9ec68b365ee4cb655ec8ee Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 13:03:34 +0100 Subject: [PATCH 09/86] [Validator] Made tests green (yay!) --- .../Validator/Context/ExecutionContext.php | 59 ++++++-- .../Context/ExecutionContextInterface.php | 4 +- .../Context/ExecutionContextManager.php | 31 +++- .../ExecutionContextManagerInterface.php | 4 +- .../Context/LegacyExecutionContext.php | 72 ++++++++- .../Validator/Mapping/CascadingStrategy.php | 27 ++++ .../Validator/Mapping/ClassMetadata.php | 19 ++- .../Validator/Mapping/ElementMetadata.php | 2 +- .../Validator/Mapping/MemberMetadata.php | 44 ++++-- .../Validator/Mapping/MetadataInterface.php | 4 +- .../Validator/Mapping/TraversalStrategy.php | 31 ++++ .../Validator/Mapping/ValueMetadata.php | 47 +++--- .../Component/Validator/Node/ClassNode.php | 5 +- src/Symfony/Component/Validator/Node/Node.php | 5 +- .../Component/Validator/Node/PropertyNode.php | 5 +- .../Validator/NodeTraverser/NodeTraverser.php | 110 ++++++++++++-- .../NodeVisitor/GroupSequenceResolver.php | 9 +- .../Validator/NodeVisitor/NodeValidator.php | 29 ++-- .../{ => Validator}/AbstractValidatorTest.php | 9 +- .../LegacyValidatorTest.php} | 19 ++- ...ingValidatorTest.php => ValidatorTest.php} | 10 +- .../Component/Validator/Util/PropertyPath.php | 36 +++++ .../Validator/Validator/AbstractValidator.php | 44 +++++- .../Validator/Validator/Validator.php | 8 +- .../Validator/ValidatorInterface.php | 2 + .../Violation/ConstraintViolationBuilder.php | 142 ++++++++++++++++++ .../ConstraintViolationBuilderInterface.php | 35 +++++ 27 files changed, 693 insertions(+), 119 deletions(-) create mode 100644 src/Symfony/Component/Validator/Mapping/CascadingStrategy.php create mode 100644 src/Symfony/Component/Validator/Mapping/TraversalStrategy.php rename src/Symfony/Component/Validator/Tests/{ => Validator}/AbstractValidatorTest.php (99%) rename src/Symfony/Component/Validator/Tests/{ValidatorTest.php => Validator/LegacyValidatorTest.php} (55%) rename src/Symfony/Component/Validator/Tests/Validator/{TraversingValidatorTest.php => ValidatorTest.php} (78%) create mode 100644 src/Symfony/Component/Validator/Util/PropertyPath.php create mode 100644 src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php create mode 100644 src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index ba4b0cb8018ff..80af8392c512d 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 @@ public function popNode() 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 @@ public function getPropertyPath($subPath = '') { $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 4a8765f27be5c..b6eeb73d353b2 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -30,11 +30,11 @@ public function getValidator(); * 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 cfb93831f87d2..369b6a062b265 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 @@ public function initialize(ValidatorInterface $validator) $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 @@ public function afterTraversal(array $nodes) 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 0d79eb43bb3de..b805c12e43364 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php @@ -18,9 +18,11 @@ 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 1981e0f00ed47..8cf17f5ee3ec5 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 @@ */ 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 0000000000000..1218c2d484c38 --- /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 8bba73a01f13e..16c42ccd0826e 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -26,7 +26,7 @@ * @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 @@ public function isGroupSequenceProvider() { 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 9dedb79fd95e4..cfe34e3985f5b 100644 --- a/src/Symfony/Component/Validator/Mapping/ElementMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ElementMetadata.php @@ -13,7 +13,7 @@ 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 c30a87ee06269..f3b3e3cf8fed2 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 @@ public function addConstraint(Constraint $constraint) } 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 @@ public function __sleep() 'class', 'name', 'property', - 'cascaded', - 'collectionCascaded', - 'collectionCascadedDeeply', + 'cascadingStrategy', + 'traversalStrategy', )); } @@ -158,6 +160,16 @@ public function isPrivate($objectOrClassName) 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 @@ public function isPrivate($objectOrClassName) */ public function isCascaded() { - return $this->cascaded; + return $this->cascadingStrategy & CascadingStrategy::CASCADE; } /** @@ -176,7 +188,7 @@ public function isCascaded() */ public function isCollectionCascaded() { - return $this->collectionCascaded; + return $this->traversalStrategy & TraversalStrategy::TRAVERSE; } /** @@ -187,7 +199,7 @@ public function isCollectionCascaded() */ 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 3df0d9bc0d587..540c4c9d6064c 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 0000000000000..4a9d8c8aa1551 --- /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 c51a6fa575dae..230e5dd947fe6 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 supportsCascading() + 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 supportsIteration() + public function getCascadingStrategy() { } - 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 dfb06dbc0325e..d49bf81c77b80 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 @@ public function __construct($value, ClassMetadataInterface $metadata, $propertyP $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 3dead5623d8ac..08b2e4da7835a 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 9424acb59f526..76cfcb35312ee 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 159f7fdd5ad6c..ce1c6029b5aee 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 @@ private function traverseNode(Node $node) // 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); - - $this->traverseClassNode(new ClassNode( - $node->value, - $classMetadata, - $node->propertyPath, - $node->groups - )); + if (!$stopTraversal && null !== $node->value) { + $cascadingStrategy = $node->metadata->getCascadingStrategy(); + $traversalStrategy = $node->metadata->getTraversalStrategy(); + + 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 @@ private function traverseNode(Node $node) } } - private function traverseClassNode(ClassNode $node) + private function traverseClassNode(ClassNode $node, $traversalStrategy = TraversalStrategy::IMPLICIT) { $stopTraversal = false; @@ -135,14 +148,89 @@ private function traverseClassNode(ClassNode $node) $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 8b9741cecccb5..047f2ad60e496 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 @@ public function enterNode(Node $node) $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 @@ public function enterNode(Node $node) $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 12fad3cf0edfd..662262a4a5bf1 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php @@ -75,7 +75,7 @@ public function enterNode(Node $node) 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 @@ public function enterNode(Node $node) 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 @@ public function enterNode(Node $node) 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 33e39c5345e04..c3148813202d8 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 @@ public function testValidateValue() $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 @@ public function testValidateMultipleGroups() public function testNoDuplicateValidationIfConstraintInMultipleGroups() { - $this->markTestSkipped('Currently not supported'); - $entity = new Entity(); $callback = function ($value, ExecutionContextInterface $context) { @@ -963,8 +960,6 @@ public function testNoDuplicateValidationIfConstraintInMultipleGroups() public function testGroupSequenceAbortsAfterFailedGroup() { - $this->markTestSkipped('Currently not supported'); - $entity = new Entity(); $callback1 = function ($value, ExecutionContextInterface $context) { @@ -997,8 +992,6 @@ public function testGroupSequenceAbortsAfterFailedGroup() 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 52bdbea519af2..327194d751392 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 968fc7a79a642..f990f10e04c3c 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\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 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) // 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 0000000000000..bf33b50b5e4a6 --- /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 5401fc6dbe21d..69f48ca7d0e61 100644 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -13,6 +13,8 @@ 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\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 @@ protected function traverseObject($object, $groups = null) $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 @@ protected function traverseProperty($object, $propertyName, $groups = null) $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 @@ protected function traverseProperty($object, $propertyName, $groups = null) $nodes[] = new PropertyNode( $propertyValue, $propertyMetadata, - $this->defaultPropertyPath, + PropertyPath::append($this->defaultPropertyPath, $propertyName), + $groups, $groups ); } @@ -118,7 +134,12 @@ protected function traversePropertyValue($object, $propertyName, $value, $groups $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 @@ protected function traversePropertyValue($object, $propertyName, $value, $groups $nodes[] = new PropertyNode( $value, $propertyMetadata, - $this->defaultPropertyPath, + PropertyPath::append($this->defaultPropertyPath, $propertyName), + $groups, $groups ); } @@ -139,13 +161,19 @@ protected function traversePropertyValue($object, $propertyName, $value, $groups 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 ed222d57c81dc..0c0c38880b33d 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -35,7 +35,7 @@ public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFacto public function validateObject($object, $groups = null) { - $this->contextManager->startContext(); + $this->contextManager->startContext($object); $this->traverseObject($object, $groups); @@ -44,7 +44,7 @@ public function validateObject($object, $groups = null) public function validateProperty($object, $propertyName, $groups = null) { - $this->contextManager->startContext(); + $this->contextManager->startContext($object); $this->traverseProperty($object, $propertyName, $groups); @@ -53,7 +53,7 @@ public function validateProperty($object, $propertyName, $groups = null) 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 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = 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 f02ed79d9b756..7985a1f9ef4e8 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 0000000000000..5fb8488e80fcf --- /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 0000000000000..9d62c3ccb5dce --- /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(); +} From c1b1e0339990a0ba14ce9b801a8d558dec75ab4a Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 13:17:23 +0100 Subject: [PATCH 10/86] [Validator] Added TODO reminder --- .../Component/Validator/NodeVisitor/GroupSequenceResolver.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php index 047f2ad60e496..868d6fc428749 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php +++ b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php @@ -34,6 +34,7 @@ public function enterNode(Node $node) /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ $groupSequence = $node->value->getGroupSequence(); + // TODO test if (!$groupSequence instanceof GroupSequence) { $groupSequence = new GroupSequence($groupSequence); } From 5fbf848f2ff35ae413570ce1ed462b1e81bbadde Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 15:23:59 +0100 Subject: [PATCH 11/86] [Validator] Added note about Callback constraint to CHANGELOG --- src/Symfony/Component/Validator/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index ecdf0cf366a6e..005526215beae 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * added `DoctrineCache` to adapt any Doctrine cache * `GroupSequence` now implements `ArrayAccess`, `Countable` and `Traversable` * changed `ClassMetadata::getGroupSequence()` to return a `GroupSequence` instance instead of an array + * `Callback` can now be put onto properties (useful when you pass a closure to the constraint) 2.4.0 ----- From 8318286650b6ac220b64ffec2ac1ea67fff1d0f9 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 15:42:46 +0100 Subject: [PATCH 12/86] [Validator] Completed GroupSequence implementation --- .../Validator/Constraints/GroupSequence.php | 112 +++++++++++++++++- .../Exception/OutOfBoundsException.php | 21 ++++ .../Tests/Constraints/GroupSequenceTest.php | 18 +++ 3 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Component/Validator/Exception/OutOfBoundsException.php diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index 7985b6cc9726b..ef93b9bc77139 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -11,10 +11,44 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\Validator\Exception\OutOfBoundsException; use Traversable; /** - * Annotation for group sequences + * A sequence of validation groups. + * + * When validating a group sequence, each group will only be validated if all + * of the previous groups in the sequence succeeded. For example: + * + * $validator->validateObject($address, new GroupSequence('Basic', 'Strict')); + * + * In the first step, all constraints that belong to the group "Basic" will be + * validated. If none of the constraints fail, the validator will then validate + * the constraints in group "Strict". This is useful, for example, if "Strict" + * contains expensive checks that require a lot of CPU or slow, external + * services. You usually don't want to run expensive checks if any of the cheap + * checks fails. + * + * When adding metadata to a class, you can override the "Default" group of + * that class with a group sequence: + * + * /** + * * @GroupSequence({"Address", "Strict"}) + * *\/ + * class Address + * { + * // ... + * } + * + * Whenever you validate that object in the "Default" group, the group sequence + * will be validated: + * + * $validator->validateObject($address); + * + * If you want to execute the constraints of the "Default" group for a class + * with an overridden default group, pass the class name as group name instead: + * + * $validator->validateObject($address, "Address") * * @Annotation * @@ -25,13 +59,14 @@ class GroupSequence implements \ArrayAccess, \IteratorAggregate, \Countable { /** - * The members of the sequence - * @var array + * The groups in the sequence + * + * @var string[]|GroupSequence[] */ public $groups; /** - * The group under which cascaded objects are validated when validating + * The group in which cascaded objects are validated when validating * this sequence. * * By default, cascaded objects are validated in each of the groups of @@ -46,27 +81,80 @@ class GroupSequence implements \ArrayAccess, \IteratorAggregate, \Countable */ public $cascadedGroup; + /** + * Creates a new group sequence. + * + * @param string[] $groups The groups in the sequence + */ public function __construct(array $groups) { // Support for Doctrine annotations $this->groups = isset($groups['value']) ? $groups['value'] : $groups; } + /** + * Returns an iterator for this group. + * + * @return Traversable The iterator + * + * @see \IteratorAggregate::getIterator() + * + * @deprecated Implemented for backwards compatibility. To be removed in + * Symfony 3.0. + */ public function getIterator() { return new \ArrayIterator($this->groups); } + /** + * Returns whether the given offset exists in the sequence. + * + * @param integer $offset The offset + * + * @return Boolean Whether the offset exists + * + * @deprecated Implemented for backwards compatibility. To be removed in + * Symfony 3.0. + */ public function offsetExists($offset) { return isset($this->groups[$offset]); } + /** + * Returns the group at the given offset. + * + * @param integer $offset The offset + * + * @return string The group a the given offset + * + * @throws OutOfBoundsException If the object does not exist + * + * @deprecated Implemented for backwards compatibility. To be removed in + * Symfony 3.0. + */ public function offsetGet($offset) { + if (!isset($this->groups[$offset])) { + throw new OutOfBoundsException(sprintf( + 'The offset "%s" does not exist.', + $offset + )); + } + return $this->groups[$offset]; } + /** + * Sets the group at the given offset. + * + * @param integer $offset The offset + * @param string $value The group name + * + * @deprecated Implemented for backwards compatibility. To be removed in + * Symfony 3.0. + */ public function offsetSet($offset, $value) { if (null !== $offset) { @@ -78,11 +166,27 @@ public function offsetSet($offset, $value) $this->groups[] = $value; } + /** + * Removes the group at the given offset. + * + * @param integer $offset The offset + * + * @deprecated Implemented for backwards compatibility. To be removed in + * Symfony 3.0. + */ public function offsetUnset($offset) { unset($this->groups[$offset]); } + /** + * Returns the number of groups in the sequence. + * + * @return integer The number of groups + * + * @deprecated Implemented for backwards compatibility. To be removed in + * Symfony 3.0. + */ public function count() { return count($this->groups); diff --git a/src/Symfony/Component/Validator/Exception/OutOfBoundsException.php b/src/Symfony/Component/Validator/Exception/OutOfBoundsException.php new file mode 100644 index 0000000000000..30906e8a82ca0 --- /dev/null +++ b/src/Symfony/Component/Validator/Exception/OutOfBoundsException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Exception; + +/** + * Base OutOfBoundsException for the Validator component. + * + * @author Bernhard Schussek + */ +class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceTest.php index 83275d1c72733..85b60b5eee3d5 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceTest.php @@ -63,4 +63,22 @@ public function testArrayAccess() $this->assertTrue(isset($sequence[0])); $this->assertSame('Group 1', $sequence[0]); } + + /** + * @expectedException \Symfony\Component\Validator\Exception\OutOfBoundsException + */ + public function testGetExpectsExistingKey() + { + $sequence = new GroupSequence(array('Group 1', 'Group 2')); + + $sequence[2]; + } + + public function testUnsetIgnoresNonExistingKeys() + { + $sequence = new GroupSequence(array('Group 1', 'Group 2')); + + // should not fail + unset($sequence[2]); + } } From f6b72884493591f8a0d75c90d35b353d00a14c13 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 15:44:01 +0100 Subject: [PATCH 13/86] [Validator] Removed unused use statement --- src/Symfony/Component/Validator/Constraints/GroupSequence.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index ef93b9bc77139..e2fe768c35648 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Validator\Constraints; use Symfony\Component\Validator\Exception\OutOfBoundsException; -use Traversable; /** * A sequence of validation groups. From 4ea3ff6688988faab3f31b8a6af1fc5a286af896 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 16:18:15 +0100 Subject: [PATCH 14/86] [Validator] Finished inline documentation of ExecutionContext[Interface] --- .../Validator/Context/ExecutionContext.php | 98 +++++++++--- .../Context/ExecutionContextInterface.php | 145 +++++++++++++----- .../{ValueMetadata.php => AdHocMetadata.php} | 2 +- .../Validator/Validator/AbstractValidator.php | 4 +- 4 files changed, 190 insertions(+), 59 deletions(-) rename src/Symfony/Component/Validator/Mapping/{ValueMetadata.php => AdHocMetadata.php} (97%) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 80af8392c512d..bc6abc39b1d53 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -23,21 +23,39 @@ use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; /** - * @since %%NextVersion%% + * The context used and created by {@link ExecutionContextManager}. + * + * @since 2.5 * @author Bernhard Schussek + * + * @see ExecutionContextInterface */ class ExecutionContext implements ExecutionContextInterface { + /** + * The root value of the validated object graph. + * + * @var mixed + */ private $root; + /** + * The violations generated in the current context. + * + * @var ConstraintViolationList + */ private $violations; /** + * The current node under validation. + * * @var Node */ private $node; /** + * The trace of nodes from the root node to the current node. + * * @var \SplStack */ private $nodeStack; @@ -73,25 +91,39 @@ public function __construct($root, ValidatorInterface $validator, GroupManagerIn $this->nodeStack = new \SplStack(); } + /** + * Sets the values of the context to match the given node. + * + * Internally, all nodes are stored on a stack and can be removed from that + * stack using {@link popNode()}. + * + * @param Node $node The currently validated node + */ public function pushNode(Node $node) { - if (null !== $this->node) { - $this->nodeStack->push($this->node); - } - + $this->nodeStack->push($node); $this->node = $node; } + /** + * Sets the values of the context to match the previous node. + * + * The current node is removed from the internal stack and returned. + * + * @return Node|null The currently validated node or null, if no node was + * on the stack + */ public function popNode() { - $poppedNode = $this->node; - + // Nothing to do if the stack is empty if (0 === count($this->nodeStack)) { - $this->node = null; - - return $poppedNode; + return null; } + $poppedNode = $this->node; + + // After removing the last node, the stack is empty and the node + // is null if (1 === count($this->nodeStack)) { $this->nodeStack->pop(); $this->node = null; @@ -105,6 +137,9 @@ public function popNode() return $poppedNode; } + /** + * {@inheritdoc} + */ public function addViolation($message, array $parameters = array()) { $this->violations->add(new ConstraintViolation( @@ -119,6 +154,9 @@ public function addViolation($message, array $parameters = array()) )); } + /** + * {@inheritdoc} + */ public function buildViolation($message, array $parameters = array()) { return new ConstraintViolationBuilder( @@ -133,31 +171,57 @@ public function buildViolation($message, array $parameters = array()) ); } + /** + * {@inheritdoc} + */ public function getViolations() { return $this->violations; } + /** + * {@inheritdoc} + */ + public function getValidator() + { + return $this->validator; + } + + /** + * {@inheritdoc} + */ public function getRoot() { return $this->root; } + /** + * {@inheritdoc} + */ public function getValue() { return $this->node ? $this->node->value : null; } + /** + * {@inheritdoc} + */ public function getMetadata() { return $this->node ? $this->node->metadata : null; } + /** + * {@inheritdoc} + */ public function getGroup() { return $this->groupManager->getCurrentGroup(); } + /** + * {@inheritdoc} + */ public function getClassName() { $metadata = $this->getMetadata(); @@ -165,6 +229,9 @@ public function getClassName() return $metadata instanceof ClassBasedInterface ? $metadata->getClassName() : null; } + /** + * {@inheritdoc} + */ public function getPropertyName() { $metadata = $this->getMetadata(); @@ -172,18 +239,13 @@ public function getPropertyName() return $metadata instanceof PropertyMetadataInterface ? $metadata->getPropertyName() : null; } + /** + * {@inheritdoc} + */ public function getPropertyPath($subPath = '') { $propertyPath = $this->node ? $this->node->propertyPath : ''; return PropertyPath::append($propertyPath, $subPath); } - - /** - * @return ValidatorInterface - */ - public function getValidator() - { - return $this->validator; - } } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index b6eeb73d353b2..5ff0a817243f7 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -11,52 +11,121 @@ namespace Symfony\Component\Validator\Context; +use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; /** - * @since %%NextVersion%% + * The context of a validation run. + * + * The context collects all violations generated during the validation. By + * default, validators execute all validations in a new context: + * + * $violations = $validator->validateObject($object); + * + * When you make another call to the validator, while the validation is in + * progress, the violations will be isolated from each other: + * + * public function validate($value, Constraint $constraint) + * { + * $validator = $this->context->getValidator(); + * + * // The violations are not added to $this->context + * $violations = $validator->validateObject($value); + * } + * + * However, if you want to add the violations to the current context, use the + * {@link ValidatorInterface::inContext()} method: + * + * public function validate($value, Constraint $constraint) + * { + * $validator = $this->context->getValidator(); + * + * // The violations are added to $this->context + * $validator + * ->inContext($this->context) + * ->validateObject($value) + * ; + * } + * + * Additionally, the context provides information about the current state of + * the validator, such as the currently validated class, the name of the + * currently validated property and more. These values change over time, so you + * cannot store a context and expect that the methods still return the same + * results later on. + * + * @since 2.5 * @author Bernhard Schussek */ interface ExecutionContextInterface { /** - * @return ValidatorInterface + * Adds a violation at the current node of the validation graph. + * + * @param string $message The error message + * @param array $parameters The parameters substituted in the error message */ - public function getValidator(); + public function addViolation($message, array $parameters = array()); /** - * Adds a violation at the current node of the validation graph. + * Returns a builder for adding a violation with extended information. + * + * Call {@link ConstraintViolationBuilderInterface::addViolation()} to + * add the violation when you're done with the configuration: * - * @param string $message The error message. - * @param array $parameters The parameters substituted in the error message. + * $context->buildViolation('Please enter a number between %min% and %max.') + * ->setParameter('%min%', 3) + * ->setParameter('%max%', 10) + * ->setTranslationDomain('number_validation') + * ->addViolation(); * - * @api + * @param string $message The error message + * @param array $parameters The parameters substituted in the error message + * + * @return ConstraintViolationBuilderInterface The violation builder */ - public function addViolation($message, array $parameters = array()); + public function buildViolation($message, array $parameters = array()); - public function buildViolation($message); + /** + * Returns the violations generated in this context. + * + * @return ConstraintViolationListInterface The constraint violations + */ + public function getViolations(); /** - * Returns the violations generated by the validator so far. + * Returns the validator. * - * @return ConstraintViolationListInterface The constraint violation list. + * Useful if you want to validate additional constraints: * - * @api + * public function validate($value, Constraint $constraint) + * { + * $validator = $this->context->getValidator(); + * + * $violations = $validator->validateValue($value, new Length(array('min' => 3))); + * + * if (count($violations) > 0) { + * // ... + * } + * } + * + * @return ValidatorInterface */ - public function getViolations(); + public function getValidator(); /** - * Returns the value at which validation was started in the object graph. + * Returns the root value of the object graph. * * The validator, when given an object, traverses the properties and * related objects and their properties. The root of the validation is the - * object from which the traversal started. + * object at which the traversal started. * - * The current value is returned by {@link getValue}. + * The current value is returned by {@link getValue()}. * - * @return mixed The root value of the validation. + * @return mixed|null The root value of the validation or null, if no value + * is currently being validated */ public function getRoot(); @@ -64,9 +133,10 @@ public function getRoot(); * Returns the value that the validator is currently validating. * * If you want to retrieve the object that was originally passed to the - * validator, use {@link getRoot}. + * validator, use {@link getRoot()}. * - * @return mixed The currently validated value. + * @return mixed|null The currently validated value or null, if no value is + * currently being validated */ public function getValue(); @@ -77,21 +147,22 @@ public function getValue(); * {@link Mapping\ClassMetadata} instance if the current value is an object, * a {@link Mapping\PropertyMetadata} instance if the current value is * the value of a property and a {@link Mapping\GetterMetadata} instance if - * the validated value is the result of a getter method. - * - * If the validated value is neither of these, for example if the validator - * has been called with a plain value and constraint, this method returns - * null. + * the validated value is the result of a getter method. The metadata can + * also be an {@link Mapping\AdHocMetadata} if the current value does not + * belong to any structural element. * * @return MetadataInterface|null The metadata of the currently validated - * value. + * value or null, if no value is currently + * being validated */ public function getMetadata(); /** * Returns the validation group that is currently being validated. * - * @return string The current validation group. + * @return string|GroupSequence|null The current validation group or null, + * if no value is currently being + * validated */ public function getGroup(); @@ -99,10 +170,10 @@ public function getGroup(); * Returns the class name of the current node. * * If the metadata of the current node does not implement - * {@link ClassBasedInterface} or if no metadata is available for the - * current node, this method returns null. + * {@link ClassBasedInterface}, this method returns null. * - * @return string|null The class name or null, if no class name could be found. + * @return string|null The class name or null, if no class name could be + * found */ public function getClassName(); @@ -110,10 +181,10 @@ public function getClassName(); * Returns the property name of the current node. * * If the metadata of the current node does not implement - * {@link PropertyMetadataInterface} or if no metadata is available for the - * current node, this method returns null. + * {@link PropertyMetadataInterface}, this method returns null. * - * @return string|null The property name or null, if no property name could be found. + * @return string|null The property name or null, if no property name could + * be found */ public function getPropertyName(); @@ -123,9 +194,7 @@ public function getPropertyName(); * * For example, take the following object graph: * - *
-     * (Person)---($address: Address)---($street: string)
-     * 
+ * (Person)---($address: Address)---($street: string) * * When the Person instance is passed to the validator, the * property path is initially empty. When the $address property @@ -137,16 +206,16 @@ public function getPropertyName(); * Indices of arrays or objects implementing the {@link \ArrayAccess} * interface are enclosed in brackets. For example, if the property in * the previous example is $addresses and contains an array - * of Address instance, the property path generated for the + * of Address instances, the property path generated for the * $street property of one of these addresses is for example * "addresses[0].street". * * @param string $subPath Optional. The suffix appended to the current - * property path. + * property path * * @return string The current property path. The result may be an empty * string if the validator is currently validating the - * root value of the validation graph. + * root value of the validation graph */ public function getPropertyPath($subPath = ''); } diff --git a/src/Symfony/Component/Validator/Mapping/ValueMetadata.php b/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php similarity index 97% rename from src/Symfony/Component/Validator/Mapping/ValueMetadata.php rename to src/Symfony/Component/Validator/Mapping/AdHocMetadata.php index 230e5dd947fe6..a7b836d3415ba 100644 --- a/src/Symfony/Component/Validator/Mapping/ValueMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php @@ -18,7 +18,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class ValueMetadata extends ElementMetadata +class AdHocMetadata extends ElementMetadata { public function __construct(array $constraints) { diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php index 69f48ca7d0e61..c57473e751d89 100644 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -16,7 +16,7 @@ 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\Mapping\AdHocMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\PropertyNode; @@ -165,7 +165,7 @@ protected function traverseValue($value, $constraints, $groups = null) $constraints = array($constraints); } - $metadata = new ValueMetadata($constraints); + $metadata = new AdHocMetadata($constraints); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; $this->nodeTraverser->traverse(array(new ValueNode( From adc1437fab6eb6e4ec30f44059db49f9483b70fc Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 16:23:52 +0100 Subject: [PATCH 15/86] [Validator] Fixed failing tests --- src/Symfony/Component/Validator/Mapping/AdHocMetadata.php | 2 +- src/Symfony/Component/Validator/Mapping/ClassMetadata.php | 5 ++--- src/Symfony/Component/Validator/Mapping/ElementMetadata.php | 2 +- src/Symfony/Component/Validator/Mapping/MemberMetadata.php | 6 +++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php b/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php index a7b836d3415ba..f7d7d563c3028 100644 --- a/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php @@ -18,7 +18,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class AdHocMetadata extends ElementMetadata +class AdHocMetadata extends ElementMetadata implements MetadataInterface { public function __construct(array $constraints) { diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 16c42ccd0826e..5712122445966 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -14,8 +14,7 @@ use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\PropertyMetadataContainerInterface; -use Symfony\Component\Validator\ClassBasedInterface; -use Symfony\Component\Validator\MetadataInterface; +use Symfony\Component\Validator\MetadataInterface as LegacyMetadataInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\GroupDefinitionException; @@ -26,7 +25,7 @@ * @author Bernhard Schussek * @author Fabien Potencier */ -class ClassMetadata extends ElementMetadata implements MetadataInterface, ClassBasedInterface, PropertyMetadataContainerInterface, ClassMetadataInterface +class ClassMetadata extends ElementMetadata implements LegacyMetadataInterface, PropertyMetadataContainerInterface, ClassMetadataInterface { /** * @var string diff --git a/src/Symfony/Component/Validator/Mapping/ElementMetadata.php b/src/Symfony/Component/Validator/Mapping/ElementMetadata.php index cfe34e3985f5b..9dedb79fd95e4 100644 --- a/src/Symfony/Component/Validator/Mapping/ElementMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ElementMetadata.php @@ -13,7 +13,7 @@ use Symfony\Component\Validator\Constraint; -abstract class ElementMetadata implements MetadataInterface +abstract class ElementMetadata { /** * @var Constraint[] diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index f3b3e3cf8fed2..876af1ae808a0 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -177,7 +177,7 @@ public function getTraversalStrategy() */ public function isCascaded() { - return $this->cascadingStrategy & CascadingStrategy::CASCADE; + return (boolean) ($this->cascadingStrategy & CascadingStrategy::CASCADE); } /** @@ -188,7 +188,7 @@ public function isCascaded() */ public function isCollectionCascaded() { - return $this->traversalStrategy & TraversalStrategy::TRAVERSE; + return (boolean) ($this->traversalStrategy & TraversalStrategy::TRAVERSE); } /** @@ -199,7 +199,7 @@ public function isCollectionCascaded() */ public function isCollectionCascadedDeeply() { - return $this->traversalStrategy & TraversalStrategy::RECURSIVE; + return (boolean) ($this->traversalStrategy & TraversalStrategy::RECURSIVE); } /** From 499b2bb601cc46d5fda7de625702aee276ff738c Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 16:50:05 +0100 Subject: [PATCH 16/86] [Validator] Completed test coverage of ExecutionContext --- .../Tests/Context/ExecutionContextTest.php | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php new file mode 100644 index 0000000000000..413e3fbc7d81c --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php @@ -0,0 +1,108 @@ + + * + * 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; +use Symfony\Component\Validator\Mapping\AdHocMetadata; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\ValueNode; + +/** + * @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() + { + $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( + self::ROOT, + $this->validator, + $this->groupManager, + $this->translator, + self::TRANSLATION_DOMAIN + ); + } + + public function testPushAndPop() + { + $metadata = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); + $node = new ValueNode('value', $metadata, '', array(), array()); + + $this->context->pushNode($node); + + $this->assertSame('value', $this->context->getValue()); + // the other methods are covered in AbstractValidatorTest + + $this->assertSame($node, $this->context->popNode()); + + $this->assertNull($this->context->getValue()); + } + + public function testPushTwiceAndPop() + { + $metadata1 = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); + $node1 = new ValueNode('value', $metadata1, '', array(), array()); + $metadata2 = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); + $node2 = new ValueNode('other value', $metadata2, '', array(), array()); + + $this->context->pushNode($node1); + $this->context->pushNode($node2); + + $this->assertSame($node2, $this->context->popNode()); + + $this->assertSame('value', $this->context->getValue()); + } + + public function testPopWithoutPush() + { + $this->assertNull($this->context->popNode()); + } + + public function testGetGroup() + { + $this->groupManager->expects($this->once()) + ->method('getCurrentGroup') + ->will($this->returnValue('Current Group')); + + $this->assertSame('Current Group', $this->context->getGroup()); + } +} From 405a03b365ed13acea72b23cbf9b1cbc54ff9592 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 16:58:10 +0100 Subject: [PATCH 17/86] [Validator] Updated deprecation notes in GroupSequence --- .../Validator/Constraints/GroupSequence.php | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index e2fe768c35648..10dec2ef83f7b 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -94,12 +94,12 @@ public function __construct(array $groups) /** * Returns an iterator for this group. * - * @return Traversable The iterator + * @return \Traversable The iterator * * @see \IteratorAggregate::getIterator() * - * @deprecated Implemented for backwards compatibility. To be removed in - * Symfony 3.0. + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in 3.0. */ public function getIterator() { @@ -113,8 +113,8 @@ public function getIterator() * * @return Boolean Whether the offset exists * - * @deprecated Implemented for backwards compatibility. To be removed in - * Symfony 3.0. + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in 3.0. */ public function offsetExists($offset) { @@ -130,8 +130,8 @@ public function offsetExists($offset) * * @throws OutOfBoundsException If the object does not exist * - * @deprecated Implemented for backwards compatibility. To be removed in - * Symfony 3.0. + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in 3.0. */ public function offsetGet($offset) { @@ -151,8 +151,8 @@ public function offsetGet($offset) * @param integer $offset The offset * @param string $value The group name * - * @deprecated Implemented for backwards compatibility. To be removed in - * Symfony 3.0. + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in 3.0. */ public function offsetSet($offset, $value) { @@ -170,8 +170,8 @@ public function offsetSet($offset, $value) * * @param integer $offset The offset * - * @deprecated Implemented for backwards compatibility. To be removed in - * Symfony 3.0. + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in 3.0. */ public function offsetUnset($offset) { @@ -183,8 +183,8 @@ public function offsetUnset($offset) * * @return integer The number of groups * - * @deprecated Implemented for backwards compatibility. To be removed in - * Symfony 3.0. + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in 3.0. */ public function count() { From 9b07b0c67271225d750746cace5687b7b654d251 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 18:03:59 +0100 Subject: [PATCH 18/86] [Validator] Implemented BC validation of arrays through validate() --- .../Validator/Constraints/Traverse.php | 56 ++++++ .../Component/Validator/Constraints/Valid.php | 13 +- .../Validator/Context/ExecutionContext.php | 17 ++ .../Context/ExecutionContextInterface.php | 2 +- .../Context/LegacyExecutionContext.php | 28 ++- .../Validator/Mapping/AdHocMetadata.php | 59 ------- .../Validator/Mapping/ClassMetadata.php | 24 ++- .../Validator/Mapping/ElementMetadata.php | 93 +--------- .../Validator/Mapping/GenericMetadata.php | 164 ++++++++++++++++++ .../Validator/Mapping/MemberMetadata.php | 29 +--- .../Node/{ValueNode.php => GenericNode.php} | 2 +- .../Tests/Context/ExecutionContextTest.php | 10 +- .../Tests/Validator/AbstractValidatorTest.php | 55 ++++-- .../Tests/Validator/LegacyValidatorTest.php | 10 ++ .../Tests/Validator/ValidatorTest.php | 44 +++++ .../Validator/Validator/AbstractValidator.php | 28 ++- .../Validator/Validator/LegacyValidator.php | 9 +- .../Validator/Validator/Validator.php | 2 +- 18 files changed, 426 insertions(+), 219 deletions(-) create mode 100644 src/Symfony/Component/Validator/Constraints/Traverse.php delete mode 100644 src/Symfony/Component/Validator/Mapping/AdHocMetadata.php create mode 100644 src/Symfony/Component/Validator/Mapping/GenericMetadata.php rename src/Symfony/Component/Validator/Node/{ValueNode.php => GenericNode.php} (92%) diff --git a/src/Symfony/Component/Validator/Constraints/Traverse.php b/src/Symfony/Component/Validator/Constraints/Traverse.php new file mode 100644 index 0000000000000..d9afe60c0745b --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/Traverse.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; + +/** + * @Annotation + * + * @since 2.5 + * @author Bernhard Schussek + */ +class Traverse extends Constraint +{ + public $traverse = true; + + public $deep = false; + + public function __construct($options = null) + { + if (is_array($options) && array_key_exists('groups', $options)) { + throw new ConstraintDefinitionException(sprintf( + 'The option "groups" is not supported by the constraint %s', + __CLASS__ + )); + } + + parent::__construct($options); + } + + /** + * {@inheritdoc} + */ + public function getDefaultOption() + { + return 'traverse'; + } + + /** + * {@inheritdoc} + */ + public function getTargets() + { + return array(self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT); + } +} diff --git a/src/Symfony/Component/Validator/Constraints/Valid.php b/src/Symfony/Component/Validator/Constraints/Valid.php index ab4676d3dfea7..6e84e9a5f053f 100644 --- a/src/Symfony/Component/Validator/Constraints/Valid.php +++ b/src/Symfony/Component/Validator/Constraints/Valid.php @@ -21,12 +21,8 @@ * * @api */ -class Valid extends Constraint +class Valid extends Traverse { - public $traverse = true; - - public $deep = false; - public function __construct($options = null) { if (is_array($options) && array_key_exists('groups', $options)) { @@ -35,4 +31,11 @@ public function __construct($options = null) parent::__construct($options); } + + public function getDefaultOption() + { + // Traverse is extended for backwards compatibility reasons + // The parent class should be removed in 3.0 + return null; + } } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index bc6abc39b1d53..5b031aabbd0dc 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -80,6 +80,23 @@ class ExecutionContext implements ExecutionContextInterface */ private $translationDomain; + /** + * Creates a new execution context. + * + * @param mixed $root The root value of the + * validated object graph + * @param ValidatorInterface $validator The validator + * @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 + * + * @internal Called by {@link ExecutionContextManager}. Should not be used + * in user code. + */ public function __construct($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) { $this->root = $root; diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index 5ff0a817243f7..cbbc3c7447112 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -148,7 +148,7 @@ public function getValue(); * a {@link Mapping\PropertyMetadata} instance if the current value is * the value of a property and a {@link Mapping\GetterMetadata} instance if * the validated value is the result of a getter method. The metadata can - * also be an {@link Mapping\AdHocMetadata} if the current value does not + * also be an {@link Mapping\GenericMetadata} if the current value does not * belong to any structural element. * * @return MetadataInterface|null The metadata of the currently validated diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index 8cf17f5ee3ec5..ad38accc6265d 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -19,11 +19,25 @@ use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; /** - * @since %%NextVersion%% + * A backwards compatible execution context. + * + * @since 2.5 * @author Bernhard Schussek + * + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. To be + * removed in 3.0. */ class LegacyExecutionContext extends ExecutionContext implements LegacyExecutionContextInterface { + /** + * Creates a new context. + * + * This constructor ensures that the given validator implements the + * deprecated {@link \Symfony\Component\Validator\ValidatorInterface}. If + * it does not, an {@link InvalidArgumentException} is thrown. + * + * @see ExecutionContext::__construct() + */ public function __construct($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) { if (!$validator instanceof LegacyValidatorInterface) { @@ -56,6 +70,9 @@ public function addViolation($message, array $parameters = array(), $invalidValu parent::addViolation($message, $parameters); } + /** + * {@inheritdoc} + */ public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) { if (func_num_args() >= 3) { @@ -78,6 +95,9 @@ public function addViolationAt($subPath, $message, array $parameters = array(), ; } + /** + * {@inheritdoc} + */ public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false) { // TODO handle $traverse and $deep @@ -90,6 +110,9 @@ public function validate($value, $subPath = '', $groups = null, $traverse = fals ; } + /** + * {@inheritdoc} + */ public function validateValue($value, $constraints, $subPath = '', $groups = null) { return $this @@ -100,6 +123,9 @@ public function validateValue($value, $constraints, $subPath = '', $groups = nul ; } + /** + * {@inheritdoc} + */ public function getMetadataFactory() { return $this->getValidator()->getMetadataFactory(); diff --git a/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php b/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php deleted file mode 100644 index f7d7d563c3028..0000000000000 --- a/src/Symfony/Component/Validator/Mapping/AdHocMetadata.php +++ /dev/null @@ -1,59 +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\Mapping; - -use Symfony\Component\Validator\Constraints\Valid; -use Symfony\Component\Validator\Exception\ValidatorException; - -/** - * @since %%NextVersion%% - * @author Bernhard Schussek - */ -class AdHocMetadata extends ElementMetadata implements MetadataInterface -{ - 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 getTraversalStrategy() - { - - } -} diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 5712122445966..d3c775b579cf1 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\PropertyMetadataContainerInterface; use Symfony\Component\Validator\MetadataInterface as LegacyMetadataInterface; @@ -62,8 +63,6 @@ class ClassMetadata extends ElementMetadata implements LegacyMetadataInterface, */ public $groupSequenceProvider = false; - public $traversalStrategy = TraversalStrategy::IMPLICIT; - /** * @var \ReflectionClass */ @@ -126,7 +125,12 @@ public function accept(ValidationVisitorInterface $visitor, $value, $group, $pro */ public function __sleep() { - return array_merge(parent::__sleep(), array( + $parentProperties = parent::__sleep(); + + // Don't store the cascading strategy. Classes never cascade. + unset($parentProperties[array_search('cascadingStrategy', $parentProperties)]); + + return array_merge($parentProperties, array( 'getters', 'groupSequence', 'groupSequenceProvider', @@ -174,7 +178,14 @@ public function addConstraint(Constraint $constraint) { if (!in_array(Constraint::CLASS_CONSTRAINT, (array) $constraint->getTargets())) { throw new ConstraintDefinitionException(sprintf( - 'The constraint %s cannot be put on classes', + 'The constraint "%s" cannot be put on classes.', + get_class($constraint) + )); + } + + if ($constraint instanceof Valid) { + throw new ConstraintDefinitionException(sprintf( + 'The constraint "%s" cannot be put on classes.', get_class($constraint) )); } @@ -434,9 +445,4 @@ 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 9dedb79fd95e4..84a826aa10f7d 100644 --- a/src/Symfony/Component/Validator/Mapping/ElementMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ElementMetadata.php @@ -11,97 +11,6 @@ namespace Symfony\Component\Validator\Mapping; -use Symfony\Component\Validator\Constraint; - -abstract class ElementMetadata +abstract class ElementMetadata extends GenericMetadata { - /** - * @var Constraint[] - */ - public $constraints = array(); - - /** - * @var array - */ - public $constraintsByGroup = array(); - - /** - * Returns the names of the properties that should be serialized. - * - * @return array - */ - public function __sleep() - { - return array( - 'constraints', - 'constraintsByGroup', - ); - } - - /** - * Clones this object. - */ - public function __clone() - { - $constraints = $this->constraints; - - $this->constraints = array(); - $this->constraintsByGroup = array(); - - foreach ($constraints as $constraint) { - $this->addConstraint(clone $constraint); - } - } - - /** - * Adds a constraint to this element. - * - * @param Constraint $constraint - * - * @return ElementMetadata - */ - public function addConstraint(Constraint $constraint) - { - $this->constraints[] = $constraint; - - foreach ($constraint->groups as $group) { - $this->constraintsByGroup[$group][] = $constraint; - } - - return $this; - } - - /** - * Returns all constraints of this element. - * - * @return Constraint[] An array of Constraint instances - */ - public function getConstraints() - { - return $this->constraints; - } - - /** - * Returns whether this element has any constraints. - * - * @return Boolean - */ - public function hasConstraints() - { - return count($this->constraints) > 0; - } - - /** - * Returns the constraints of the given group and global ones (* group). - * - * @param string $group The group name - * - * @return array An array with all Constraint instances belonging to the group - */ - public function findConstraints($group) - { - return isset($this->constraintsByGroup[$group]) - ? $this->constraintsByGroup[$group] - : array(); - } } diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php new file mode 100644 index 0000000000000..75b07c8825ccf --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -0,0 +1,164 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping; + +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Traverse; +use Symfony\Component\Validator\Constraints\Valid; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class GenericMetadata implements MetadataInterface +{ + /** + * @var Constraint[] + */ + public $constraints = array(); + + /** + * @var array + */ + public $constraintsByGroup = array(); + + public $cascadingStrategy = CascadingStrategy::NONE; + + public $traversalStrategy = TraversalStrategy::IMPLICIT; + + /** + * Returns the names of the properties that should be serialized. + * + * @return array + */ + public function __sleep() + { + return array( + 'constraints', + 'constraintsByGroup', + 'cascadingStrategy', + 'traversalStrategy', + ); + } + + /** + * Clones this object. + */ + public function __clone() + { + $constraints = $this->constraints; + + $this->constraints = array(); + $this->constraintsByGroup = array(); + + foreach ($constraints as $constraint) { + $this->addConstraint(clone $constraint); + } + } + + /** + * Adds a constraint to this element. + * + * @param Constraint $constraint + * + * @return ElementMetadata + */ + public function addConstraint(Constraint $constraint) + { + if ($constraint instanceof Valid) { + $this->cascadingStrategy = CascadingStrategy::CASCADE; + + // Continue. Valid extends Traverse, so the return statement in the + // next block is going be executed. + } + + if ($constraint instanceof Traverse) { + if (true === $constraint->traverse) { + // If traverse is true, traversal should be explicitly enabled + $this->traversalStrategy = TraversalStrategy::TRAVERSE; + + if ($constraint->deep) { + $this->traversalStrategy |= TraversalStrategy::RECURSIVE; + } + } elseif (false === $constraint->traverse) { + // If traverse is false, traversal should be explicitly disabled + $this->traversalStrategy = TraversalStrategy::NONE; + } else { + // Else, traverse depending on the contextual information that + // is available during validation + $this->traversalStrategy = TraversalStrategy::IMPLICIT; + } + + // The constraint is not added + return $this; + } + + $this->constraints[] = $constraint; + + foreach ($constraint->groups as $group) { + $this->constraintsByGroup[$group][] = $constraint; + } + + return $this; + } + + public function addConstraints(array $constraints) + { + foreach ($constraints as $constraint) { + $this->addConstraint($constraint); + } + } + + /** + * Returns all constraints of this element. + * + * @return Constraint[] An array of Constraint instances + */ + public function getConstraints() + { + return $this->constraints; + } + + /** + * Returns whether this element has any constraints. + * + * @return Boolean + */ + public function hasConstraints() + { + return count($this->constraints) > 0; + } + + /** + * Returns the constraints of the given group and global ones (* group). + * + * @param string $group The group name + * + * @return array An array with all Constraint instances belonging to the group + */ + public function findConstraints($group) + { + return isset($this->constraintsByGroup[$group]) + ? $this->constraintsByGroup[$group] + : array(); + } + + public function getCascadingStrategy() + { + return $this->cascadingStrategy; + } + + public function getTraversalStrategy() + { + return $this->traversalStrategy; + } +} diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 876af1ae808a0..80a2687458ddb 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -14,7 +14,6 @@ use Symfony\Component\Validator\ValidationVisitorInterface; 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, LegacyPropertyMetadataInterface @@ -22,8 +21,6 @@ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadat public $class; public $name; public $property; - public $cascadingStrategy = CascadingStrategy::NONE; - public $traversalStrategy = TraversalStrategy::IMPLICIT; private $reflMember = array(); /** @@ -61,19 +58,7 @@ public function addConstraint(Constraint $constraint) )); } - if ($constraint instanceof Valid) { - $this->cascadingStrategy = CascadingStrategy::CASCADE; - - if ($constraint->traverse) { - $this->traversalStrategy = TraversalStrategy::TRAVERSE; - } - - if ($constraint->deep) { - $this->traversalStrategy |= TraversalStrategy::RECURSIVE; - } - } else { - parent::addConstraint($constraint); - } + parent::addConstraint($constraint); return $this; } @@ -89,8 +74,6 @@ public function __sleep() 'class', 'name', 'property', - 'cascadingStrategy', - 'traversalStrategy', )); } @@ -160,16 +143,6 @@ public function isPrivate($objectOrClassName) 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 * diff --git a/src/Symfony/Component/Validator/Node/ValueNode.php b/src/Symfony/Component/Validator/Node/GenericNode.php similarity index 92% rename from src/Symfony/Component/Validator/Node/ValueNode.php rename to src/Symfony/Component/Validator/Node/GenericNode.php index e0f77e41f19c0..aab73db64e235 100644 --- a/src/Symfony/Component/Validator/Node/ValueNode.php +++ b/src/Symfony/Component/Validator/Node/GenericNode.php @@ -15,6 +15,6 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class ValueNode extends Node +class GenericNode extends Node { } diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php index 413e3fbc7d81c..54e3374f66bf7 100644 --- a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php +++ b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php @@ -12,10 +12,10 @@ namespace Symfony\Component\Validator\Tests\Context; use Symfony\Component\Validator\Context\ExecutionContext; -use Symfony\Component\Validator\Mapping\AdHocMetadata; +use Symfony\Component\Validator\Mapping\GenericMetadata; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\ValueNode; +use Symfony\Component\Validator\Node\GenericNode; /** * @since 2.5 @@ -65,7 +65,7 @@ protected function setUp() public function testPushAndPop() { $metadata = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); - $node = new ValueNode('value', $metadata, '', array(), array()); + $node = new GenericNode('value', $metadata, '', array(), array()); $this->context->pushNode($node); @@ -80,9 +80,9 @@ public function testPushAndPop() public function testPushTwiceAndPop() { $metadata1 = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); - $node1 = new ValueNode('value', $metadata1, '', array(), array()); + $node1 = new GenericNode('value', $metadata1, '', array(), array()); $metadata2 = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); - $node2 = new ValueNode('other value', $metadata2, '', array(), array()); + $node2 = new GenericNode('other value', $metadata2, '', array(), array()); $this->context->pushNode($node1); $this->context->pushNode($node2); diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index c3148813202d8..d8a1c43500561 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -42,22 +42,22 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase /** * @var ValidatorInterface */ - private $validator; + protected $validator; /** * @var FakeMetadataFactory */ - public $metadataFactory; + protected $metadataFactory; /** * @var ClassMetadata */ - public $metadata; + protected $metadata; /** * @var ClassMetadata */ - public $referenceMetadata; + protected $referenceMetadata; protected function setUp() { @@ -199,6 +199,45 @@ public function testGetterConstraint() $this->assertNull($violations[0]->getCode()); } + public function testArray() + { + $test = $this; + $entity = new Entity(); + $array = array('key' => $entity); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $array) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($array, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($array, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[key]', $violations[0]->getPropertyPath()); + $this->assertSame($array, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + public function testReferenceClassConstraint() { $test = $this; @@ -815,14 +854,6 @@ public function testValidateValue() $this->assertNull($violations[0]->getCode()); } - /** - * @expectedException \Symfony\Component\Validator\Exception\ValidatorException - */ - public function testValidateValueRejectsValid() - { - $this->validator->validateValue(new Entity(), new Valid()); - } - public function testAddCustomizedViolation() { $entity = new Entity(); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php index 327194d751392..eafbc4d1282f7 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php @@ -11,7 +11,9 @@ namespace Symfony\Component\Validator\Tests\Validator; +use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Validator; use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; @@ -37,4 +39,12 @@ public function testGroupSequenceIncludesReferences() { $this->markTestSkipped('Currently not supported'); } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ValidatorException + */ + public function testValidateValueRejectsValid() + { + $this->validator->validateValue(new Entity(), new Valid()); + } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php index f990f10e04c3c..57a08fbc94960 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php @@ -11,6 +11,10 @@ namespace Symfony\Component\Validator\Tests\Validator; +use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\ConstraintViolationInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\ExecutionContextManager; @@ -18,6 +22,7 @@ use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; use Symfony\Component\Validator\NodeVisitor\NodeValidator; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; +use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Validator\LegacyValidator; class ValidatorTest extends AbstractValidatorTest @@ -44,4 +49,43 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) return $validator; } + + public function testValidateValueAcceptsValid() + { + $test = $this; + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + // This is the same as when calling validateObject() + $violations = $this->validator->validateValue($entity, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } } diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php index c57473e751d89..959e98779f81c 100644 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -12,15 +12,16 @@ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Traverse; 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\AdHocMetadata; +use Symfony\Component\Validator\Mapping\GenericMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\PropertyNode; -use Symfony\Component\Validator\Node\ValueNode; +use Symfony\Component\Validator\Node\GenericNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; use Symfony\Component\Validator\Util\PropertyPath; @@ -97,6 +98,24 @@ protected function traverseObject($object, $groups = null) ))); } + protected function traverseCollection($collection, $groups = null, $deep = false) + { + $metadata = new GenericMetadata(); + $metadata->addConstraint(new Traverse(array( + 'traverse' => true, + 'deep' => $deep, + ))); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + $this->nodeTraverser->traverse(array(new GenericNode( + $collection, + $metadata, + $this->defaultPropertyPath, + $groups, + $groups + ))); + } + protected function traverseProperty($object, $propertyName, $groups = null) { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -165,10 +184,11 @@ protected function traverseValue($value, $constraints, $groups = null) $constraints = array($constraints); } - $metadata = new AdHocMetadata($constraints); + $metadata = new GenericMetadata(); + $metadata->addConstraints($constraints); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $this->nodeTraverser->traverse(array(new ValueNode( + $this->nodeTraverser->traverse(array(new GenericNode( $value, $metadata, $this->defaultPropertyPath, diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index f8a2cc74c1bc8..b2fb4e5d35daf 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -21,7 +21,14 @@ class LegacyValidator extends Validator implements LegacyValidatorInterface { public function validate($value, $groups = null, $traverse = false, $deep = false) { - // TODO what about $traverse and $deep? + if (is_array($value)) { + $this->contextManager->startContext($value); + + $this->traverseCollection($value, $groups, $deep); + + return $this->contextManager->stopContext()->getViolations(); + } + return $this->validateObject($value, $groups); } diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index 0c0c38880b33d..fe55818e7069c 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -24,7 +24,7 @@ class Validator extends AbstractValidator /** * @var ExecutionContextManagerInterface */ - private $contextManager; + protected $contextManager; public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory, ExecutionContextManagerInterface $contextManager) { From 297ba4f585b7c08a4520f62e8554bf2824e52831 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 18:07:44 +0100 Subject: [PATCH 19/86] [Validator] Added a note why scalars are passed to cascadeObject() in NodeTraverser --- .../Component/Validator/NodeTraverser/NodeTraverser.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index ce1c6029b5aee..d0529af0050e9 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -113,6 +113,9 @@ private function traverseNode(Node $node) $traversalStrategy ); } elseif ($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, From 09f744b89cb064b2cc137d2aa8ffcfe13e4c2bdc Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Feb 2014 18:21:00 +0100 Subject: [PATCH 20/86] [Validator] Implemented BC traversal of traversables through validate() --- .../Tests/Validator/AbstractValidatorTest.php | 163 ++++++++++++++++++ .../Validator/Validator/AbstractValidator.php | 18 -- .../Validator/Validator/LegacyValidator.php | 16 +- 3 files changed, 175 insertions(+), 22 deletions(-) diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index d8a1c43500561..2b55290a1d999 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -238,6 +238,169 @@ public function testArray() $this->assertNull($violations[0]->getCode()); } + public function testRecursiveArray() + { + $test = $this; + $entity = new Entity(); + $array = array(2 => array('key' => $entity)); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $array) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[2][key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($array, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($array, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[2][key]', $violations[0]->getPropertyPath()); + $this->assertSame($array, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testTraversableTraverseDisabled() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array('key' => $entity)); + + $callback = function () use ($test) { + $test->fail('Should not be called'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $this->validator->validate($traversable, 'Group'); + } + + public function testTraversableTraverseEnabled() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array('key' => $entity)); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($traversable, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($traversable, 'Group', true); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[key]', $violations[0]->getPropertyPath()); + $this->assertSame($traversable, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testRecursiveTraversableRecursiveTraversalDisabled() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => $entity)), + )); + + $callback = function () use ($test) { + $test->fail('Should not be called'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $this->validator->validate($traversable, 'Group', true); + } + + public function testRecursiveTraversableRecursiveTraversalEnabled() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => $entity)), + )); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[2][key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($traversable, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($traversable, 'Group', true, true); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[2][key]', $violations[0]->getPropertyPath()); + $this->assertSame($traversable, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + public function testReferenceClassConstraint() { $test = $this; diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php index 959e98779f81c..f0de724ffe162 100644 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -98,24 +98,6 @@ protected function traverseObject($object, $groups = null) ))); } - protected function traverseCollection($collection, $groups = null, $deep = false) - { - $metadata = new GenericMetadata(); - $metadata->addConstraint(new Traverse(array( - 'traverse' => true, - 'deep' => $deep, - ))); - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - - $this->nodeTraverser->traverse(array(new GenericNode( - $collection, - $metadata, - $this->defaultPropertyPath, - $groups, - $groups - ))); - } - protected function traverseProperty($object, $propertyName, $groups = null) { $classMetadata = $this->metadataFactory->getMetadataFor($object); diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index b2fb4e5d35daf..9586f200dd7cb 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Validator\Validator; +use Symfony\Component\Validator\Constraints\Traverse; +use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; /** @@ -22,11 +24,17 @@ class LegacyValidator extends Validator implements LegacyValidatorInterface public function validate($value, $groups = null, $traverse = false, $deep = false) { if (is_array($value)) { - $this->contextManager->startContext($value); - - $this->traverseCollection($value, $groups, $deep); + return $this->validateValue($value, new Traverse(array( + 'traverse' => true, + 'deep' => $deep, + )), $groups); + } - return $this->contextManager->stopContext()->getViolations(); + if ($traverse && $value instanceof \Traversable) { + return $this->validateValue($value, array( + new Valid(), + new Traverse(array('traverse' => true, 'deep' => $deep)), + ), $groups); } return $this->validateObject($value, $groups); From ee1adadbfb8b620900028089135c575f20733dc7 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 11:56:27 +0100 Subject: [PATCH 21/86] [Validator] Implemented handling of arrays and Traversables in LegacyExecutionContext::validate() --- .../Context/LegacyExecutionContext.php | 30 +++++- .../Tests/Validator/AbstractValidatorTest.php | 95 +++++++++++++++++++ .../Validator/Validator/AbstractValidator.php | 2 - .../Validator/ContextualValidator.php | 15 +++ .../Validator/Validator/LegacyValidator.php | 12 ++- .../Validator/Validator/Validator.php | 15 +++ .../Validator/ValidatorInterface.php | 2 +- 7 files changed, 163 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index ad38accc6265d..db30eb92b5b6c 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Validator\Context; use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Validator\Constraints\Traverse; +use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; use Symfony\Component\Validator\Group\GroupManagerInterface; @@ -100,7 +102,33 @@ public function addViolationAt($subPath, $message, array $parameters = array(), */ public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false) { - // TODO handle $traverse and $deep + if (is_array($value)) { + $constraint = new Traverse(array( + 'traverse' => true, + 'deep' => $deep, + )); + + return $this + ->getValidator() + ->inContext($this) + ->atPath($subPath) + ->validateValue($value, $constraint, $groups) + ; + } + + if ($traverse && $value instanceof \Traversable) { + $constraints = array( + new Valid(), + new Traverse(array('traverse' => true, 'deep' => $deep)), + ); + + return $this + ->getValidator() + ->inContext($this) + ->atPath($subPath) + ->validateValue($value, $constraints, $groups) + ; + } return $this ->getValidator() diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index 2b55290a1d999..b7a9ee2e19aee 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -15,6 +15,7 @@ use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintViolationInterface; +use Symfony\Component\Validator\Context\ExecutionContext; use Symfony\Component\Validator\ExecutionContextInterface; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; @@ -1416,6 +1417,100 @@ public function testReplaceDefaultGroupWithArrayFromGroupSequenceProvider() $this->assertSame('Violation in Group 2', $violations[0]->getMessage()); } + public function testValidateInContext() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->validate($value->reference, 'subpath'); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('subpath', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('subpath', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testValidateArrayInContext() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->validate(array('key' => $value->reference), 'subpath'); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('subpath[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('subpath[key]', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + public function testGetMetadataFactory() { $this->assertSame($this->metadataFactory, $this->validator->getMetadataFactory()); diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php index f0de724ffe162..d4d4e62e9c976 100644 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -12,9 +12,7 @@ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\Traverse; 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\GenericMetadata; diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index 560f088522992..8a58f48d9fc04 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; @@ -41,6 +42,8 @@ public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFacto public function atPath($subPath) { $this->defaultPropertyPath = $this->context->getPropertyPath($subPath); + + return $this; } /** @@ -62,6 +65,18 @@ public function validateObject($object, $groups = null) return $this->context->getViolations(); } + public function validateCollection($collection, $groups = null, $deep = false) + { + $constraint = new Traverse(array( + 'traverse' => true, + 'deep' => $deep, + )); + + $this->traverseValue($collection, $constraint, $groups); + + return $this->context->getViolations(); + } + /** * Validates a property of a value against its current value. * diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 9586f200dd7cb..059d84c1daf9e 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -24,17 +24,21 @@ class LegacyValidator extends Validator implements LegacyValidatorInterface public function validate($value, $groups = null, $traverse = false, $deep = false) { if (is_array($value)) { - return $this->validateValue($value, new Traverse(array( + $constraint = new Traverse(array( 'traverse' => true, 'deep' => $deep, - )), $groups); + )); + + return $this->validateValue($value, $constraint, $groups); } if ($traverse && $value instanceof \Traversable) { - return $this->validateValue($value, array( + $constraints = array( new Valid(), new Traverse(array('traverse' => true, 'deep' => $deep)), - ), $groups); + ); + + return $this->validateValue($value, $constraints, $groups); } return $this->validateObject($value, $groups); diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index fe55818e7069c..18022ba19f5c1 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Validator; +use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; use Symfony\Component\Validator\MetadataFactoryInterface; @@ -42,6 +43,20 @@ public function validateObject($object, $groups = null) return $this->contextManager->stopContext()->getViolations(); } + public function validateCollection($collection, $groups = null, $deep = false) + { + $this->contextManager->startContext($collection); + + $constraint = new Traverse(array( + 'traverse' => true, + 'deep' => $deep, + )); + + $this->traverseValue($collection, $constraint, $groups); + + return $this->contextManager->stopContext()->getViolations(); + } + public function validateProperty($object, $propertyName, $groups = null) { $this->contextManager->startContext($object); diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index 7985a1f9ef4e8..6d1833d162129 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -35,7 +35,7 @@ interface ValidatorInterface */ public function validateObject($object, $groups = null); -// public function validateCollection($collection, $groups = null); + public function validateCollection($collection, $groups = null, $deep = false); /** * Validates a property of a value against its current value. From 718601c6c3642fa7a65a408016a34feef00fe44d Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 12:02:54 +0100 Subject: [PATCH 22/86] [Validator] Changed validateValue() to validate() in the new API The validation of values against constraints should be a first-class citizen in the API. For this reason, validate() now takes a value and a constraint or a list of constraints. This method should be used for the most basic use cases. If users want to annotate objects with constraints (this is optional, advanced functionality), they can use the more expressive validateObject() method now. For traversing arrays or Traversables, a new method validateCollection() is now available in the API. --- .../Context/LegacyExecutionContext.php | 6 ++-- .../Validator/ContextualValidator.php | 34 +++++++++---------- .../Validator/Validator/LegacyValidator.php | 11 ++++++ .../Validator/Validator/Validator.php | 19 +++++------ .../Validator/ValidatorInterface.php | 24 ++++++------- 5 files changed, 52 insertions(+), 42 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index db30eb92b5b6c..0563bdab3d87b 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -112,7 +112,7 @@ public function validate($value, $subPath = '', $groups = null, $traverse = fals ->getValidator() ->inContext($this) ->atPath($subPath) - ->validateValue($value, $constraint, $groups) + ->validate($value, $constraint, $groups) ; } @@ -126,7 +126,7 @@ public function validate($value, $subPath = '', $groups = null, $traverse = fals ->getValidator() ->inContext($this) ->atPath($subPath) - ->validateValue($value, $constraints, $groups) + ->validate($value, $constraints, $groups) ; } @@ -147,7 +147,7 @@ public function validateValue($value, $constraints, $subPath = '', $groups = nul ->getValidator() ->inContext($this) ->atPath($subPath) - ->validateValue($value, $constraints, $groups) + ->validate($value, $constraints, $groups) ; } diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index 8a58f48d9fc04..a3e21863df7b4 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -46,6 +46,23 @@ public function atPath($subPath) return $this; } + /** + * Validates a value against a constraint or a list of constraints. + * + * @param mixed $value The value to validate. + * @param Constraint|Constraint[] $constraints The constraint(s) to validate against. + * @param array|null $groups The validation groups to validate. + * + * @return ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. + */ + public function validate($value, $constraints, $groups = null) + { + $this->traverseValue($value, $constraints, $groups); + + return $this->context->getViolations(); + } + /** * Validates a value. * @@ -118,21 +135,4 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = return $this->context->getViolations(); } - - /** - * Validates a value against a constraint or a list of constraints. - * - * @param mixed $value The value to validate. - * @param Constraint|Constraint[] $constraints The constraint(s) to validate against. - * @param array|null $groups The validation groups to validate. - * - * @return ConstraintViolationListInterface A list of constraint violations. If the - * list is empty, validation succeeded. - */ - public function validateValue($value, $constraints, $groups = null) - { - $this->traverseValue($value, $constraints, $groups); - - return $this->context->getViolations(); - } } diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 059d84c1daf9e..64df69e1e26da 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Validator; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; @@ -23,6 +24,11 @@ class LegacyValidator extends Validator implements LegacyValidatorInterface { public function validate($value, $groups = null, $traverse = false, $deep = false) { + // Use new signature if constraints are given in the second argument + if (func_num_args() <= 3 && ($groups instanceof Constraint || (is_array($groups) && current($groups) instanceof Constraint))) { + return parent::validate($value, $groups, $traverse); + } + if (is_array($value)) { $constraint = new Traverse(array( 'traverse' => true, @@ -44,6 +50,11 @@ public function validate($value, $groups = null, $traverse = false, $deep = fals return $this->validateObject($value, $groups); } + public function validateValue($value, $constraints, $groups = null) + { + return parent::validate($value, $constraints, $groups); + } + public function getMetadataFactory() { return $this->metadataFactory; diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index 18022ba19f5c1..5d8b509be3eb6 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -34,6 +34,15 @@ public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFacto $this->contextManager = $contextManager; } + public function validate($value, $constraints, $groups = null) + { + $this->contextManager->startContext($value); + + $this->traverseValue($value, $constraints, $groups); + + return $this->contextManager->stopContext()->getViolations(); + } + public function validateObject($object, $groups = null) { $this->contextManager->startContext($object); @@ -74,14 +83,4 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = return $this->contextManager->stopContext()->getViolations(); } - - public function validateValue($value, $constraints, $groups = null) - { - $this->contextManager->startContext($value); - - $this->traverseValue($value, $constraints, $groups); - - return $this->contextManager->stopContext()->getViolations(); - } - } diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index 6d1833d162129..8cecaf3dc4b49 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -21,6 +21,18 @@ */ interface ValidatorInterface { + /** + * Validates a value against a constraint or a list of constraints. + * + * @param mixed $value The value to validate. + * @param Constraint|Constraint[] $constraints The constraint(s) to validate against. + * @param array|null $groups The validation groups to validate. + * + * @return ConstraintViolationListInterface A list of constraint violations. If the + * list is empty, validation succeeded. + */ + public function validate($value, $constraints, $groups = null); + /** * Validates a value. * @@ -69,18 +81,6 @@ public function validateProperty($object, $propertyName, $groups = null); */ public function validatePropertyValue($object, $propertyName, $value, $groups = null); - /** - * Validates a value against a constraint or a list of constraints. - * - * @param mixed $value The value to validate. - * @param Constraint|Constraint[] $constraints The constraint(s) to validate against. - * @param array|null $groups The validation groups to validate. - * - * @return ConstraintViolationListInterface A list of constraint violations. If the - * list is empty, validation succeeded. - */ - public function validateValue($value, $constraints, $groups = null); - /** * @param ExecutionContextInterface $context * From feb3d6f20233001c4d04e4c7ded948e554a58432 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 13:04:59 +0100 Subject: [PATCH 23/86] [Validator] Tested the validation in a separate context --- .../Validator/Context/ExecutionContext.php | 18 +- .../Context/ExecutionContextManager.php | 30 +--- .../Validator/NodeVisitor/NodeValidator.php | 2 - .../Tests/Validator/AbstractValidatorTest.php | 164 ++++++++++++++++++ .../Tests/Validator/LegacyValidatorTest.php | 25 ++- .../Tests/Validator/ValidatorTest.php | 10 +- 6 files changed, 206 insertions(+), 43 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 5b031aabbd0dc..e2b582843f28e 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -137,19 +137,13 @@ public function popNode() return null; } - $poppedNode = $this->node; + // Remove the current node from the stack + $poppedNode = $this->nodeStack->pop(); - // After removing the last node, the stack is empty and the node - // is null - if (1 === count($this->nodeStack)) { - $this->nodeStack->pop(); - $this->node = null; - - return $poppedNode; - } - - $this->nodeStack->pop(); - $this->node = $this->nodeStack->top(); + // Adjust the current node to the previous node + $this->node = count($this->nodeStack) > 0 + ? $this->nodeStack->top() + : null; return $poppedNode; } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php index 369b6a062b265..5625136b36f26 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php @@ -72,10 +72,6 @@ public function startContext($root) // TODO error, call initialize() first } - if (null !== $this->currentContext) { - $this->contextStack->push($this->currentContext); - } - $this->currentContext = new LegacyExecutionContext( $root, $this->validator, @@ -83,29 +79,24 @@ public function startContext($root) $this->translator, $this->translationDomain ); + $this->contextStack->push($this->currentContext); return $this->currentContext; } public function stopContext() { - $stoppedContext = $this->currentContext; - if (0 === count($this->contextStack)) { - $this->currentContext = null; - - return $stoppedContext; + return null; } - if (1 === count($this->contextStack)) { - $this->contextStack->pop(); - $this->currentContext = null; - - return $stoppedContext; - } + // Remove the current context from the stack + $stoppedContext = $this->contextStack->pop(); - $this->contextStack->pop(); - $this->currentContext = $this->contextStack->top(); + // Adjust the current context to the previous context + $this->currentContext = count($this->contextStack) > 0 + ? $this->contextStack->top() + : null; return $stoppedContext; } @@ -115,11 +106,6 @@ public function getCurrentContext() return $this->currentContext; } - public function afterTraversal(array $nodes) - { - $this->contextStack = new \SplStack(); - } - public function enterNode(Node $node) { if (null === $this->currentContext) { diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php index 662262a4a5bf1..100c9b7d3b538 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php @@ -47,8 +47,6 @@ class NodeValidator extends AbstractVisitor implements GroupManagerInterface private $currentGroup; - private $currentObjectHash; - private $objectHashStack; public function __construct(NodeTraverserInterface $nodeTraverser, ConstraintValidatorFactoryInterface $validatorFactory) diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index b7a9ee2e19aee..60e5ebf76e8c2 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -1423,6 +1423,58 @@ public function testValidateInContext() $entity = new Entity(); $entity->reference = new Reference(); + $callback1 = function ($value, ExecutionContextInterface $context) { + $context + ->getValidator() + ->inContext($context) + ->atPath('subpath') + ->validateObject($value->reference) + ; + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('subpath', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('subpath', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testValidateInContextLegacyApi() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + $callback1 = function ($value, ExecutionContextInterface $context) { $context->validate($value->reference, 'subpath'); }; @@ -1470,6 +1522,58 @@ public function testValidateArrayInContext() $entity = new Entity(); $entity->reference = new Reference(); + $callback1 = function ($value, ExecutionContextInterface $context) { + $context + ->getValidator() + ->inContext($context) + ->atPath('subpath') + ->validateCollection(array('key' => $value->reference)) + ; + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('subpath[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('subpath[key]', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testValidateArrayInContextLegacyApi() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + $callback1 = function ($value, ExecutionContextInterface $context) { $context->validate(array('key' => $value->reference), 'subpath'); }; @@ -1511,6 +1615,66 @@ public function testValidateArrayInContext() $this->assertNull($violations[0]->getCode()); } + public function testValidateInSeparateContext() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $violations = $context + ->getValidator() + // Since the validator is not context aware, the group must + // be passed explicitly + ->validateObject($value->reference, 'Group') + ; + + /** @var ConstraintViolationInterface[] $violations */ + $test->assertCount(1, $violations); + $test->assertSame('Message value', $violations[0]->getMessage()); + $test->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $test->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $test->assertSame('', $violations[0]->getPropertyPath()); + // The root is different as we're in a new context + $test->assertSame($entity->reference, $violations[0]->getRoot()); + $test->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $test->assertNull($violations[0]->getMessagePluralization()); + $test->assertNull($violations[0]->getCode()); + + // Verify that this method is called + $context->addViolation('Separate violation'); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity->reference, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $test->assertSame('Separate violation', $violations[0]->getMessage()); + } + public function testGetMetadataFactory() { $this->assertSame($this->metadataFactory, $this->validator->getMetadataFactory()); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php index eafbc4d1282f7..c66cde30ea886 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php @@ -14,7 +14,7 @@ use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Tests\Fixtures\Entity; -use Symfony\Component\Validator\Validator; +use Symfony\Component\Validator\Validator as LegacyValidator; use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; @@ -22,22 +22,37 @@ class LegacyValidatorTest extends AbstractValidatorTest { protected function createValidator(MetadataFactoryInterface $metadataFactory) { - return new Validator($metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); + return new LegacyValidator($metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); } public function testNoDuplicateValidationIfConstraintInMultipleGroups() { - $this->markTestSkipped('Currently not supported'); + $this->markTestSkipped('Not supported in the legacy API'); } public function testGroupSequenceAbortsAfterFailedGroup() { - $this->markTestSkipped('Currently not supported'); + $this->markTestSkipped('Not supported in the legacy API'); } public function testGroupSequenceIncludesReferences() { - $this->markTestSkipped('Currently not supported'); + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testValidateInContext() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testValidateArrayInContext() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testValidateInSeparateContext() + { + $this->markTestSkipped('Not supported in the legacy API'); } /** diff --git a/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php index 57a08fbc94960..2b0d00205e887 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php @@ -24,9 +24,15 @@ use Symfony\Component\Validator\NodeTraverser\NodeTraverser; use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Validator\LegacyValidator; +use Symfony\Component\Validator\Validator\ValidatorInterface; class ValidatorTest extends AbstractValidatorTest { + /** + * @var ValidatorInterface + */ + protected $validator; + protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); @@ -50,7 +56,7 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) return $validator; } - public function testValidateValueAcceptsValid() + public function testValidateAcceptsValid() { $test = $this; $entity = new Entity(); @@ -75,7 +81,7 @@ public function testValidateValueAcceptsValid() ))); // This is the same as when calling validateObject() - $violations = $this->validator->validateValue($entity, new Valid(), 'Group'); + $violations = $this->validator->validate($entity, new Valid(), 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); From 1e81f3bdc8dad2ba63fc8a7d88caca7f0a42f420 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 13:28:37 +0100 Subject: [PATCH 24/86] [Validator] Finished test coverage and documentation of ExecutionContextManager --- .../Context/ExecutionContextManager.php | 75 ++++++++++++++- .../ExecutionContextManagerInterface.php | 54 ++++++++++- .../Context/LegacyExecutionContext.php | 2 +- .../Context/ExecutionContextManagerTest.php | 95 +++++++++++++++++++ .../Tests/Context/ExecutionContextTest.php | 3 - 5 files changed, 218 insertions(+), 11 deletions(-) create mode 100644 src/Symfony/Component/Validator/Tests/Context/ExecutionContextManagerTest.php diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php index 5625136b36f26..decefdc8a881d 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php @@ -12,14 +12,29 @@ namespace Symfony\Component\Validator\Context; use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Validator\Exception\RuntimeException; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\NodeVisitor\AbstractVisitor; use Symfony\Component\Validator\Validator\ValidatorInterface; /** - * @since %%NextVersion%% + * The default implementation of {@link ExecutionContextManagerInterface}. + * + * This class implements {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface} + * and updates the current context with the current node of the validation + * traversal. + * + * After creating a new instance, the method {@link initialize()} must be + * called with a {@link ValidatorInterface} instance. Calling methods such as + * {@link startContext()} or {@link enterNode()} without initializing the + * manager first will lead to errors. + * + * @since 2.5 * @author Bernhard Schussek + * + * @see ExecutionContextManagerInterface + * @see \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface */ class ExecutionContextManager extends AbstractVisitor implements ExecutionContextManagerInterface { @@ -53,6 +68,17 @@ class ExecutionContextManager extends AbstractVisitor implements ExecutionContex */ private $translationDomain; + /** + * Creates a new context manager. + * + * @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) { $this->groupManager = $groupManager; @@ -61,15 +87,27 @@ public function __construct(GroupManagerInterface $groupManager, TranslatorInter $this->contextStack = new \SplStack(); } + /** + * Initializes the manager with a validator. + * + * @param ValidatorInterface $validator The validator + */ public function initialize(ValidatorInterface $validator) { $this->validator = $validator; } + /** + * {@inheritdoc} + * + * @throws RuntimeException If {@link initialize()} wasn't called + */ public function startContext($root) { if (null === $this->validator) { - // TODO error, call initialize() first + throw new RuntimeException( + 'initialize() must be called before startContext().' + ); } $this->currentContext = new LegacyExecutionContext( @@ -84,10 +122,18 @@ public function startContext($root) return $this->currentContext; } + /** + * {@inheritdoc} + * + * @throws RuntimeException If {@link startContext()} wasn't called + */ public function stopContext() { if (0 === count($this->contextStack)) { - return null; + throw new RuntimeException( + 'No context was started yet. Call startContext() before '. + 'stopContext().' + ); } // Remove the current context from the stack @@ -101,24 +147,43 @@ public function stopContext() return $stoppedContext; } + /** + * {@inheritdoc} + */ public function getCurrentContext() { return $this->currentContext; } + /** + * {@inheritdoc} + * + * @throws RuntimeException If {@link initialize()} wasn't called + */ public function enterNode(Node $node) { if (null === $this->currentContext) { - // TODO error call startContext() first + throw new RuntimeException( + 'No context was started yet. Call startContext() before '. + 'enterNode().' + ); } $this->currentContext->pushNode($node); } + /** + * {@inheritdoc} + * + * @throws RuntimeException If {@link initialize()} wasn't called + */ public function leaveNode(Node $node) { if (null === $this->currentContext) { - // error no context started + throw new RuntimeException( + 'No context was started yet. Call startContext() before '. + 'leaveNode().' + ); } $this->currentContext->popNode(); diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php index b805c12e43364..a2dd04d7dccd5 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php @@ -12,24 +12,74 @@ namespace Symfony\Component\Validator\Context; /** - * @since %%NextVersion%% + * Manages the creation and deletion of {@link ExecutionContextInterface} + * instances. + * + * Start a new context with {@link startContext()}. You can retrieve the context + * with {@link getCurrentContext()} and stop it again with {@link stopContext()}. + * + * $contextManager->startContext(); + * $context = $contextManager->getCurrentContext(); + * $contextManager->stopContext(); + * + * You can also start several nested contexts. The {@link getCurrentContext()} + * method will always return the most recently started context. + * + * // Start context 1 + * $contextManager->startContext(); + * + * // Start context 2 + * $contextManager->startContext(); + * + * // Returns context 2 + * $context = $contextManager->getCurrentContext(); + * + * // Stop context 2 + * $contextManager->stopContext(); + * + * // Returns context 1 + * $context = $contextManager->getCurrentContext(); + * + * See also {@link ExecutionContextInterface} for more information. + * + * @since 2.5 * @author Bernhard Schussek + * + * @see ExecutionContextInterface */ interface ExecutionContextManagerInterface { /** - * @param mixed $root + * Starts a new context. + * + * The newly started context is returned. You can subsequently access the + * context with {@link getCurrentContext()}. + * + * @param mixed $root The root value of the object graph in the new context * * @return ExecutionContextInterface The started context */ public function startContext($root); /** + * Stops the current context. + * + * If multiple contexts have been started, the most recently started context + * is stopped. The stopped context is returned from this method. + * + * After calling this method, {@link getCurrentContext()} will return the + * context that was started before the stopped context. + * * @return ExecutionContextInterface The stopped context */ public function stopContext(); /** + * Returns the current context. + * + * If multiple contexts have been started, the current context refers to the + * most recently started context. + * * @return ExecutionContextInterface The current context */ public function getCurrentContext(); diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index 0563bdab3d87b..685f06931bb61 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -21,7 +21,7 @@ use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; /** - * A backwards compatible execution context. + * An execution context that is compatible with the legacy API (< 2.5). * * @since 2.5 * @author Bernhard Schussek diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextManagerTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextManagerTest.php new file mode 100644 index 0000000000000..548a11170d57d --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextManagerTest.php @@ -0,0 +1,95 @@ + + * + * 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\ExecutionContextManager; + +/** + * @since 2.5 + * @author Bernhard Schussek + */ +class ExecutionContextManagerTest extends \PHPUnit_Framework_TestCase +{ + 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 ExecutionContextManager + */ + private $contextManager; + + protected function setUp() + { + $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->contextManager = new ExecutionContextManager( + $this->groupManager, + $this->translator, + self::TRANSLATION_DOMAIN + ); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\RuntimeException + */ + public function testInitializeMustBeCalledBeforeStartContext() + { + $this->contextManager->startContext('root'); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\RuntimeException + */ + public function testCannotStopContextIfNoneWasStarted() + { + $this->contextManager->stopContext(); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\RuntimeException + */ + public function testCannotEnterNodeWithoutActiveContext() + { + $node = $this->getMockBuilder('Symfony\Component\Validator\Node\Node') + ->disableOriginalConstructor() + ->getMock(); + + $this->contextManager->enterNode($node); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\RuntimeException + */ + public function testCannotLeaveNodeWithoutActiveContext() + { + $node = $this->getMockBuilder('Symfony\Component\Validator\Node\Node') + ->disableOriginalConstructor() + ->getMock(); + + $this->contextManager->leaveNode($node); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php index 54e3374f66bf7..1133f413a7cd6 100644 --- a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php +++ b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php @@ -12,9 +12,6 @@ namespace Symfony\Component\Validator\Tests\Context; use Symfony\Component\Validator\Context\ExecutionContext; -use Symfony\Component\Validator\Mapping\GenericMetadata; -use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\GenericNode; /** From 9c9e715ca82c74fc9b657e3dd702d9134935d13b Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 13:31:44 +0100 Subject: [PATCH 25/86] [Validator] Completed documentation of GroupManagerInterface --- .../Validator/Group/GroupManagerInterface.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Group/GroupManagerInterface.php b/src/Symfony/Component/Validator/Group/GroupManagerInterface.php index 94a0ab9544e10..a231b907d55f3 100644 --- a/src/Symfony/Component/Validator/Group/GroupManagerInterface.php +++ b/src/Symfony/Component/Validator/Group/GroupManagerInterface.php @@ -12,10 +12,18 @@ namespace Symfony\Component\Validator\Group; /** - * @since %%NextVersion%% + * 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(); } From 2c65a28608b54acca7a1d311c804aa8ceb8efc29 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 13:50:19 +0100 Subject: [PATCH 26/86] [Validator] Completed test coverage and documentation of the Node classes --- .../Component/Validator/Node/ClassNode.php | 30 +++++++++++--- .../Component/Validator/Node/GenericNode.php | 9 +++- src/Symfony/Component/Validator/Node/Node.php | 41 ++++++++++++++++++- .../Component/Validator/Node/PropertyNode.php | 28 ++++++++++++- .../Validator/Tests/Node/ClassNodeTest.php | 31 ++++++++++++++ 5 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php index d49bf81c77b80..07554963d7feb 100644 --- a/src/Symfony/Component/Validator/Node/ClassNode.php +++ b/src/Symfony/Component/Validator/Node/ClassNode.php @@ -11,10 +11,13 @@ namespace Symfony\Component\Validator\Node; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; /** - * @since %%NextVersion%% + * Represents an object and its class metadata in the validation graph. + * + * @since 2.5 * @author Bernhard Schussek */ class ClassNode extends Node @@ -24,19 +27,34 @@ class ClassNode extends Node */ public $metadata; - public function __construct($value, ClassMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) + /** + * Creates a new class node. + * + * @param object $object The validated object + * @param ClassMetadataInterface $metadata The class metadata of that + * object + * @param string $propertyPath The property path leading + * to this node + * @param string[] $groups The groups in which this + * node should be validated + * @param string[] $cascadedGroups The groups in which + * cascaded objects should be + * validated + * + * @throws UnexpectedTypeException If the given value is not an object + */ + public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) { - if (!is_object($value)) { - // error + if (!is_object($object)) { + throw new UnexpectedTypeException($object, 'object'); } parent::__construct( - $value, + $object, $metadata, $propertyPath, $groups, $cascadedGroups ); } - } diff --git a/src/Symfony/Component/Validator/Node/GenericNode.php b/src/Symfony/Component/Validator/Node/GenericNode.php index aab73db64e235..82ee9ac7fbaad 100644 --- a/src/Symfony/Component/Validator/Node/GenericNode.php +++ b/src/Symfony/Component/Validator/Node/GenericNode.php @@ -12,7 +12,14 @@ namespace Symfony\Component\Validator\Node; /** - * @since %%NextVersion%% + * Represents a value that has neither class metadata nor property metadata + * attached to it. + * + * Together with {@link \Symfony\Component\Validator\Mapping\GenericMetadata}, + * this node type can be used to validate a value against some given + * constraints. + * + * @since 2.5 * @author Bernhard Schussek */ class GenericNode extends Node diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php index 08b2e4da7835a..014665abf4ffb 100644 --- a/src/Symfony/Component/Validator/Node/Node.php +++ b/src/Symfony/Component/Validator/Node/Node.php @@ -14,21 +14,60 @@ use Symfony\Component\Validator\Mapping\MetadataInterface; /** - * @since %%NextVersion%% + * A node in the validated graph. + * + * @since 2.5 * @author Bernhard Schussek */ abstract class Node { + /** + * The validated value. + * + * @var mixed + */ public $value; + /** + * The metadata specifying how the value should be validated. + * + * @var MetadataInterface + */ public $metadata; + /** + * The property path leading to this node. + * + * @var string + */ public $propertyPath; + /** + * The groups in which the value should be validated. + * + * @var string[] + */ public $groups; + /** + * The groups in which cascaded values should be validated. + * + * @var string[] + */ public $cascadedGroups; + /** + * Creates a new property node. + * + * @param mixed $value The property value + * @param MetadataInterface $metadata The property's metadata + * @param string $propertyPath The property path leading to + * this node + * @param string[] $groups The groups in which this node + * should be validated + * @param string[] $cascadedGroups The groups in which cascaded + * objects should be validated + */ public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) { $this->value = $value; diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php index 76cfcb35312ee..bcb462b1761f5 100644 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -14,7 +14,20 @@ use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; /** - * @since %%NextVersion%% + * Represents the value of a property and its associated metadata. + * + * If the property contains an object and should be cascaded, a new + * {@link ClassNode} instance will be created for that object. + * + * Example: + * + * (Article:ClassNode) + * \ + * (author:PropertyNode) + * \ + * (Author:ClassNode) + * + * @since 2.5 * @author Bernhard Schussek */ class PropertyNode extends Node @@ -24,6 +37,19 @@ class PropertyNode extends Node */ public $metadata; + /** + * Creates a new property node. + * + * @param mixed $value The property value + * @param PropertyMetadataInterface $metadata The property's metadata + * @param string $propertyPath The property path leading + * to this node + * @param string[] $groups The groups in which this + * node should be validated + * @param string[] $cascadedGroups The groups in which + * cascaded objects should + * be validated + */ public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) { parent::__construct( diff --git a/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php b/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php new file mode 100644 index 0000000000000..1241d1bb5b830 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.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\Tests\Node; + +use Symfony\Component\Validator\Node\ClassNode; + +/** + * @since 2.5 + * @author Bernhard Schussek + */ +class ClassNodeTest extends \PHPUnit_Framework_TestCase +{ + /** + * @expectedException \Symfony\Component\Validator\Exception\UnexpectedTypeException + */ + public function testConstructorExpectsObject() + { + $metadata = $this->getMock('Symfony\Component\Validator\Mapping\ClassMetadataInterface'); + + new ClassNode('foobar', $metadata, '', array(), array()); + } +} From a3555fbd992accd92e81c43174ffd490ec69c239 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 17:20:08 +0100 Subject: [PATCH 27/86] [Validator] Fixed: Objects are not traversed unless they are instances of Traversable --- .../Component/Validator/Constraints/Valid.php | 19 +- .../Validator/Mapping/GenericMetadata.php | 3 +- .../Validator/Mapping/MemberMetadata.php | 19 ++ .../Validator/Mapping/TraversalStrategy.php | 2 + .../Component/Validator/Node/ClassNode.php | 4 +- src/Symfony/Component/Validator/Node/Node.php | 11 +- .../Component/Validator/Node/PropertyNode.php | 4 +- .../Validator/NodeTraverser/NodeTraverser.php | 283 +++++++++++++----- .../Validator/NodeVisitor/NodeValidator.php | 5 +- .../Tests/Validator/AbstractValidatorTest.php | 220 +++++++++++++- .../Tests/Validator/LegacyValidatorTest.php | 35 +++ .../Validator/Validator/AbstractValidator.php | 38 +-- .../Validator/ContextualValidator.php | 4 +- .../Validator/Validator/LegacyValidator.php | 4 +- .../Validator/Validator/Validator.php | 4 +- 15 files changed, 539 insertions(+), 116 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/Valid.php b/src/Symfony/Component/Validator/Constraints/Valid.php index 6e84e9a5f053f..9f15fdb04e1db 100644 --- a/src/Symfony/Component/Validator/Constraints/Valid.php +++ b/src/Symfony/Component/Validator/Constraints/Valid.php @@ -21,12 +21,27 @@ * * @api */ -class Valid extends Traverse +class Valid extends Constraint { + /** + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use the {@link Traverse} constraint instead. + */ + public $traverse = true; + + /** + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use the {@link Traverse} constraint instead. + */ + public $deep = false; + public function __construct($options = null) { if (is_array($options) && array_key_exists('groups', $options)) { - throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint %s', __CLASS__)); + throw new ConstraintDefinitionException(sprintf( + 'The option "groups" is not supported by the constraint %s', + __CLASS__ + )); } parent::__construct($options); diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index 75b07c8825ccf..369276f220e89 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -77,8 +77,7 @@ public function addConstraint(Constraint $constraint) if ($constraint instanceof Valid) { $this->cascadingStrategy = CascadingStrategy::CASCADE; - // Continue. Valid extends Traverse, so the return statement in the - // next block is going be executed. + return $this; } if ($constraint instanceof Traverse) { diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 80a2687458ddb..230af7d89e416 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Mapping; +use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\PropertyMetadataInterface as LegacyPropertyMetadataInterface; use Symfony\Component\Validator\Constraint; @@ -58,6 +59,24 @@ public function addConstraint(Constraint $constraint) )); } + // BC with Symfony < 2.5 + // Only process if the traversal strategy was not already set by the + // Traverse constraint + if ($constraint instanceof Valid && !$this->traversalStrategy) { + if (true === $constraint->traverse) { + // Try to traverse cascaded objects, but ignore if they do not + // implement Traversable + $this->traversalStrategy = TraversalStrategy::TRAVERSE + | TraversalStrategy::IGNORE_NON_TRAVERSABLE; + + if ($constraint->deep) { + $this->traversalStrategy |= TraversalStrategy::RECURSIVE; + } + } elseif (false === $constraint->traverse) { + $this->traversalStrategy = TraversalStrategy::NONE; + } + } + parent::addConstraint($constraint); return $this; diff --git a/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php index 4a9d8c8aa1551..951ec6058d87a 100644 --- a/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php +++ b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php @@ -25,6 +25,8 @@ class TraversalStrategy const RECURSIVE = 4; + const IGNORE_NON_TRAVERSABLE = 8; + private function __construct() { } diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php index 07554963d7feb..904e8651fdbc6 100644 --- a/src/Symfony/Component/Validator/Node/ClassNode.php +++ b/src/Symfony/Component/Validator/Node/ClassNode.php @@ -37,13 +37,13 @@ class ClassNode extends Node * to this node * @param string[] $groups The groups in which this * node should be validated - * @param string[] $cascadedGroups The groups in which + * @param string[]|null $cascadedGroups The groups in which * cascaded objects should be * validated * * @throws UnexpectedTypeException If the given value is not an object */ - public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) + public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) { if (!is_object($object)) { throw new UnexpectedTypeException($object, 'object'); diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php index 014665abf4ffb..b301db8dda696 100644 --- a/src/Symfony/Component/Validator/Node/Node.php +++ b/src/Symfony/Component/Validator/Node/Node.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Node; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Mapping\MetadataInterface; /** @@ -65,11 +66,17 @@ abstract class Node * this node * @param string[] $groups The groups in which this node * should be validated - * @param string[] $cascadedGroups The groups in which cascaded + * @param string[]|null $cascadedGroups The groups in which cascaded * objects should be validated + * + * @throws UnexpectedTypeException If $cascadedGroups is invalid */ - public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) + public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) { + if (null !== $cascadedGroups && !is_array($cascadedGroups)) { + throw new UnexpectedTypeException($cascadedGroups, 'null or array'); + } + $this->value = $value; $this->metadata = $metadata; $this->propertyPath = $propertyPath; diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php index bcb462b1761f5..313da63ab7487 100644 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -46,11 +46,11 @@ class PropertyNode extends Node * to this node * @param string[] $groups The groups in which this * node should be validated - * @param string[] $cascadedGroups The groups in which + * @param string[]|null $cascadedGroups The groups in which * cascaded objects should * be validated */ - public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, array $cascadedGroups) + public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) { parent::__construct( $value, diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index d0529af0050e9..d9cbf62c71626 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -12,8 +12,10 @@ namespace Symfony\Component\Validator\NodeTraverser; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Mapping\CascadingStrategy; +use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; @@ -80,100 +82,219 @@ public function traverse(array $nodes) } if ($isTopLevelCall) { - $this->traversalStarted = false; - foreach ($this->visitors as $visitor) { /** @var NodeVisitorInterface $visitor */ $visitor->afterTraversal($nodes); } + + $this->traversalStarted = false; } } - private function traverseNode(Node $node) + /** + * @param Node $node + * + * @return Boolean + */ + private function enterNode(Node $node) { - $stopTraversal = false; + $continueTraversal = true; foreach ($this->visitors as $visitor) { if (false === $visitor->enterNode($node)) { - $stopTraversal = true; - } - } - - // Stop the traversal, but execute the leaveNode() methods anyway to - // perform possible cleanups - if (!$stopTraversal && null !== $node->value) { - $cascadingStrategy = $node->metadata->getCascadingStrategy(); - $traversalStrategy = $node->metadata->getTraversalStrategy(); + $continueTraversal = false; - if (is_array($node->value)) { - $this->cascadeCollection( - $node->value, - $node->propertyPath, - $node->cascadedGroups, - $traversalStrategy - ); - } elseif ($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, - $node->cascadedGroups, - $traversalStrategy - ); + // Continue, so that the enterNode() method of all visitors + // is called } } + return $continueTraversal; + } + + /** + * @param Node $node + */ + private function leaveNode(Node $node) + { foreach ($this->visitors as $visitor) { $visitor->leaveNode($node); } } - private function traverseClassNode(ClassNode $node, $traversalStrategy = TraversalStrategy::IMPLICIT) + private function traverseNode(Node $node) { - $stopTraversal = false; + $continue = $this->enterNode($node); - foreach ($this->visitors as $visitor) { - if (false === $visitor->enterNode($node)) { - $stopTraversal = true; - } + // Visitors have two possibilities to influence the traversal: + // + // 1. If a visitor's enterNode() method returns false, the traversal is + // skipped entirely. + // 2. If a visitor's enterNode() method removes a group from the node, + // that group will be skipped in the subtree of that node. + + if (false === $continue) { + $this->leaveNode($node); + + return; } - // Stop the traversal, but execute the leaveNode() methods anyway to - // perform possible cleanups - if (!$stopTraversal && count($node->groups) > 0) { - foreach ($node->metadata->getConstrainedProperties() as $propertyName) { - foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { - $this->traverseNode(new PropertyNode( - $propertyMetadata->getPropertyValue($node->value), - $propertyMetadata, - $node->propertyPath - ? $node->propertyPath.'.'.$propertyName - : $propertyName, - $node->groups, - $node->cascadedGroups - )); - } - } + if (null === $node->value) { + $this->leaveNode($node); + + return; + } + + // The "cascadedGroups" property is set by the NodeValidator when + // traversing group sequences + $cascadedGroups = null !== $node->cascadedGroups + ? $node->cascadedGroups + : $node->groups; + + if (0 === count($cascadedGroups)) { + $this->leaveNode($node); + + return; + } + + $cascadingStrategy = $node->metadata->getCascadingStrategy(); + $traversalStrategy = $node->metadata->getTraversalStrategy(); + + if (is_array($node->value)) { + // Arrays are always traversed, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $this->cascadeEachObjectIn( + $node->value, + $node->propertyPath, + $node->cascadedGroups, + $traversalStrategy + ); + + $this->leaveNode($node); + + 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, + $node->cascadedGroups, + $traversalStrategy + ); - if ($traversalStrategy & TraversalStrategy::IMPLICIT) { - $traversalStrategy = $node->metadata->getTraversalStrategy(); + $this->leaveNode($node); + + return; + } + + // Traverse only if the TRAVERSE bit is set + if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { + $this->leaveNode($node); + + return; + } + + if (!$node->value instanceof \Traversable) { + if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { + $this->leaveNode($node); + + return; } - if ($traversalStrategy & TraversalStrategy::TRAVERSE) { - $this->cascadeCollection( - $node->value, - $node->propertyPath, - $node->groups, - $traversalStrategy - ); + throw new ConstraintDefinitionException(sprintf( + 'Traversal was enabled for "%s", but this class '. + 'does not implement "\Traversable".', + get_class($node->value) + )); + } + + $this->cascadeEachObjectIn( + $node->value, + $node->propertyPath, + $node->groups, + $traversalStrategy + ); + + $this->leaveNode($node); + } + + private function traverseClassNode(ClassNode $node, $traversalStrategy = TraversalStrategy::IMPLICIT) + { + $continue = $this->enterNode($node); + + // Visitors have two possibilities to influence the traversal: + // + // 1. If a visitor's enterNode() method returns false, the traversal is + // skipped entirely. + // 2. If a visitor's enterNode() method removes a group from the node, + // that group will be skipped in the subtree of that node. + + if (false === $continue) { + $this->leaveNode($node); + + return; + } + + if (0 === count($node->groups)) { + $this->leaveNode($node); + + return; + } + + foreach ($node->metadata->getConstrainedProperties() as $propertyName) { + foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { + $this->traverseNode(new PropertyNode( + $propertyMetadata->getPropertyValue($node->value), + $propertyMetadata, + $node->propertyPath + ? $node->propertyPath.'.'.$propertyName + : $propertyName, + $node->groups, + $node->cascadedGroups + )); } } - foreach ($this->visitors as $visitor) { - $visitor->leaveNode($node); + // If no specific traversal strategy was requested when this method + // was called, use the traversal strategy of the class' metadata + if (TraversalStrategy::IMPLICIT === $traversalStrategy) { + $traversalStrategy = $node->metadata->getTraversalStrategy(); } + + // Traverse only if the TRAVERSE bit is set + if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { + $this->leaveNode($node); + + return; + } + + if (!$node->value instanceof \Traversable) { + if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { + $this->leaveNode($node); + + return; + } + + throw new ConstraintDefinitionException(sprintf( + 'Traversal was enabled for "%s", but this class '. + 'does not implement "\Traversable".', + get_class($node->value) + )); + } + + $this->cascadeEachObjectIn( + $node->value, + $node->propertyPath, + $node->groups, + $traversalStrategy + ); + + $this->leaveNode($node); } private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy) @@ -181,24 +302,31 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal try { $classMetadata = $this->metadataFactory->getMetadataFor($object); + if (!$classMetadata instanceof ClassMetadataInterface) { + // error + } + $classNode = new ClassNode( $object, $classMetadata, $propertyPath, - $groups, $groups ); $this->traverseClassNode($classNode, $traversalStrategy); } catch (NoSuchMetadataException $e) { - if (!$object instanceof \Traversable || !($traversalStrategy & TraversalStrategy::TRAVERSE)) { + // Rethrow if the TRAVERSE bit is not set + if (!($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( + // Rethrow if the object does not implement Traversable + if (!$object instanceof \Traversable) { + throw $e; + } + + // In that case, iterate the object and cascade each entry + $this->cascadeEachObjectIn( $object, $propertyPath, $groups, @@ -207,15 +335,25 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal } } - private function cascadeCollection($collection, $propertyPath, array $groups, $traversalStrategy) + private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy) { - if (!($traversalStrategy & TraversalStrategy::RECURSIVE)) { + if ($traversalStrategy & TraversalStrategy::RECURSIVE) { + // Try to traverse nested objects, but ignore if they do not + // implement Traversable + $traversalStrategy |= TraversalStrategy::IGNORE_NON_TRAVERSABLE; + } else { + // If the RECURSIVE bit is not set, change the strategy to IMPLICIT + // in order to respect the metadata's traversal strategy of each entry + // in the collection $traversalStrategy = TraversalStrategy::IMPLICIT; } foreach ($collection as $key => $value) { if (is_array($value)) { - $this->cascadeCollection( + // Arrays are always cascaded, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $this->cascadeEachObjectIn( $value, $propertyPath.'['.$key.']', $groups, @@ -226,6 +364,7 @@ private function cascadeCollection($collection, $propertyPath, array $groups, $t } // Scalar and null values in the collection are ignored + // (BC with Symfony < 2.5) if (is_object($value)) { $this->cascadeObject( $value, diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php index 100c9b7d3b538..6ee8b7f8260db 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php @@ -146,7 +146,10 @@ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence) foreach ($groupSequence->groups as $groupInSequence) { $node = clone $node; $node->groups = array($groupInSequence); - $node->cascadedGroups = array($groupSequence->cascadedGroup ?: $groupInSequence); + + if (null !== $groupSequence->cascadedGroup) { + $node->cascadedGroups = array($groupSequence->cascadedGroup); + } $this->nodeTraverser->traverse(array($node)); diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index 60e5ebf76e8c2..36b13cfd96444 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -14,6 +14,7 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\Context\ExecutionContext; use Symfony\Component\Validator\ExecutionContextInterface; @@ -225,6 +226,45 @@ public function testArray() 'groups' => 'Group', ))); + $violations = $this->validator->validateCollection($array, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[key]', $violations[0]->getPropertyPath()); + $this->assertSame($array, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testArrayLegacyApi() + { + $test = $this; + $entity = new Entity(); + $array = array('key' => $entity); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $array) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($array, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $violations = $this->validator->validate($array, 'Group'); /** @var ConstraintViolationInterface[] $violations */ @@ -264,6 +304,45 @@ public function testRecursiveArray() 'groups' => 'Group', ))); + $violations = $this->validator->validateCollection($array, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[2][key]', $violations[0]->getPropertyPath()); + $this->assertSame($array, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testRecursiveArrayLegacyApi() + { + $test = $this; + $entity = new Entity(); + $array = array(2 => array('key' => $entity)); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $array) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[2][key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($array, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $violations = $this->validator->validate($array, 'Group'); /** @var ConstraintViolationInterface[] $violations */ @@ -278,17 +357,24 @@ public function testRecursiveArray() $this->assertNull($violations[0]->getCode()); } - /** - * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException - */ - public function testTraversableTraverseDisabled() + public function testTraversableTraverseEnabled() { $test = $this; $entity = new Entity(); $traversable = new \ArrayIterator(array('key' => $entity)); - $callback = function () use ($test) { - $test->fail('Should not be called'); + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($traversable, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); }; $this->metadata->addConstraint(new Callback(array( @@ -296,10 +382,21 @@ public function testTraversableTraverseDisabled() 'groups' => 'Group', ))); - $this->validator->validate($traversable, 'Group'); + $violations = $this->validator->validateCollection($traversable, 'Group', true); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[key]', $violations[0]->getPropertyPath()); + $this->assertSame($traversable, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); } - public function testTraversableTraverseEnabled() + public function testTraversableTraverseEnabledLegacyApi() { $test = $this; $entity = new Entity(); @@ -338,6 +435,27 @@ public function testTraversableTraverseEnabled() $this->assertNull($violations[0]->getCode()); } + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testTraversableTraverseDisabledLegacyApi() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array('key' => $entity)); + + $callback = function () use ($test) { + $test->fail('Should not be called'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $this->validator->validate($traversable, 'Group'); + } + /** * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException */ @@ -358,6 +476,29 @@ public function testRecursiveTraversableRecursiveTraversalDisabled() 'groups' => 'Group', ))); + $this->validator->validateCollection($traversable, 'Group'); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testRecursiveTraversableRecursiveTraversalDisabledLegacyApi() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => $entity)), + )); + + $callback = function () use ($test) { + $test->fail('Should not be called'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $this->validator->validate($traversable, 'Group', true); } @@ -388,6 +529,47 @@ public function testRecursiveTraversableRecursiveTraversalEnabled() 'groups' => 'Group', ))); + $violations = $this->validator->validateCollection($traversable, 'Group', true); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[2][key]', $violations[0]->getPropertyPath()); + $this->assertSame($traversable, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testRecursiveTraversableRecursiveTraversalEnabledLegacyApi() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => $entity)), + )); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[2][key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($traversable, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $violations = $this->validator->validate($traversable, 'Group', true, true); /** @var ConstraintViolationInterface[] $violations */ @@ -1675,6 +1857,28 @@ public function testValidateInSeparateContext() $test->assertSame('Separate violation', $violations[0]->getMessage()); } + /** + * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException + */ + public function testExpectTraversableIfTraverse() + { + $entity = new Entity(); + + $this->validator->validateValue($entity, new Traverse()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException + */ + public function testExpectTraversableIfTraverseOnClass() + { + $entity = new Entity(); + + $this->metadata->addConstraint(new Traverse()); + + $this->validator->validate($entity); + } + public function testGetMetadataFactory() { $this->assertSame($this->metadataFactory, $this->validator->getMetadataFactory()); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php index c66cde30ea886..0d8ade455cc82 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php @@ -55,6 +55,41 @@ public function testValidateInSeparateContext() $this->markTestSkipped('Not supported in the legacy API'); } + public function testArray() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testRecursiveArray() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testTraversableTraverseEnabled() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testRecursiveTraversableRecursiveTraversalDisabled() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testRecursiveTraversableRecursiveTraversalEnabled() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testExpectTraversableIfTraverse() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + + public function testExpectTraversableIfTraverseOnClass() + { + $this->markTestSkipped('Not supported in the legacy API'); + } + /** * @expectedException \Symfony\Component\Validator\Exception\ValidatorException */ diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php index d4d4e62e9c976..8cd2767bd03eb 100644 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ b/src/Symfony/Component/Validator/Validator/AbstractValidator.php @@ -72,6 +72,25 @@ public function hasMetadataFor($object) return $this->metadataFactory->hasMetadataFor($object); } + protected function traverse($value, $constraints, $groups = null) + { + if (!is_array($constraints)) { + $constraints = array($constraints); + } + + $metadata = new GenericMetadata(); + $metadata->addConstraints($constraints); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + $this->nodeTraverser->traverse(array(new GenericNode( + $value, + $metadata, + $this->defaultPropertyPath, + $groups, + $groups + ))); + } + protected function traverseObject($object, $groups = null) { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -158,25 +177,6 @@ protected function traversePropertyValue($object, $propertyName, $value, $groups $this->nodeTraverser->traverse($nodes); } - protected function traverseValue($value, $constraints, $groups = null) - { - if (!is_array($constraints)) { - $constraints = array($constraints); - } - - $metadata = new GenericMetadata(); - $metadata->addConstraints($constraints); - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - - $this->nodeTraverser->traverse(array(new GenericNode( - $value, - $metadata, - $this->defaultPropertyPath, - $groups, - $groups - ))); - } - protected function normalizeGroups($groups) { if (is_array($groups)) { diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index a3e21863df7b4..b975ef4bde1d3 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -58,7 +58,7 @@ public function atPath($subPath) */ public function validate($value, $constraints, $groups = null) { - $this->traverseValue($value, $constraints, $groups); + $this->traverse($value, $constraints, $groups); return $this->context->getViolations(); } @@ -89,7 +89,7 @@ public function validateCollection($collection, $groups = null, $deep = false) 'deep' => $deep, )); - $this->traverseValue($collection, $constraint, $groups); + $this->traverse($collection, $constraint, $groups); return $this->context->getViolations(); } diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 64df69e1e26da..73d6948e3ece4 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -35,7 +35,7 @@ public function validate($value, $groups = null, $traverse = false, $deep = fals 'deep' => $deep, )); - return $this->validateValue($value, $constraint, $groups); + return parent::validate($value, $constraint, $groups); } if ($traverse && $value instanceof \Traversable) { @@ -44,7 +44,7 @@ public function validate($value, $groups = null, $traverse = false, $deep = fals new Traverse(array('traverse' => true, 'deep' => $deep)), ); - return $this->validateValue($value, $constraints, $groups); + return parent::validate($value, $constraints, $groups); } return $this->validateObject($value, $groups); diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index 5d8b509be3eb6..94857a3a1b555 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -38,7 +38,7 @@ public function validate($value, $constraints, $groups = null) { $this->contextManager->startContext($value); - $this->traverseValue($value, $constraints, $groups); + $this->traverse($value, $constraints, $groups); return $this->contextManager->stopContext()->getViolations(); } @@ -61,7 +61,7 @@ public function validateCollection($collection, $groups = null, $deep = false) 'deep' => $deep, )); - $this->traverseValue($collection, $constraint, $groups); + $this->traverse($collection, $constraint, $groups); return $this->contextManager->stopContext()->getViolations(); } From bc295919358eaf625dc5cf751c1b73fb0b4fa43d Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 18:06:10 +0100 Subject: [PATCH 28/86] [Validator] Clearly separated classes supporting the API <2.5/2.5+ --- .../Validator/Context/ExecutionContext.php | 57 +- .../Context/ExecutionContextManager.php | 26 +- .../Context/LegacyExecutionContextManager.php | 36 + .../Tests/Fixtures/FakeMetadataFactory.php | 8 +- .../Tests/Validator/Abstract2Dot5ApiTest.php | 407 +++++++++ .../Tests/Validator/AbstractLegacyApiTest.php | 224 +++++ .../Tests/Validator/AbstractValidatorTest.php | 821 ++---------------- .../Validator/LegacyValidator2Dot5ApiTest.php | 47 + .../LegacyValidatorLegacyApiTest.php | 47 + .../Tests/Validator/LegacyValidatorTest.php | 100 --- .../Tests/Validator/Validator2Dot5ApiTest.php | 47 + .../Tests/Validator/ValidatorTest.php | 97 --- .../Validator/Tests/ValidatorTest.php | 36 + 13 files changed, 1025 insertions(+), 928 deletions(-) create mode 100644 src/Symfony/Component/Validator/Context/LegacyExecutionContextManager.php create mode 100644 src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php delete mode 100644 src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php delete mode 100644 src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php create mode 100644 src/Symfony/Component/Validator/Tests/ValidatorTest.php diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index e2b582843f28e..43794988056b6 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -13,10 +13,14 @@ use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\ClassBasedInterface; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\Exception\BadMethodCallException; +use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -30,7 +34,7 @@ * * @see ExecutionContextInterface */ -class ExecutionContext implements ExecutionContextInterface +class ExecutionContext implements ExecutionContextInterface, LegacyExecutionContextInterface { /** * The root value of the validated object graph. @@ -151,8 +155,12 @@ public function popNode() /** * {@inheritdoc} */ - public function addViolation($message, array $parameters = array()) + public function addViolation($message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) { + // The parameters $invalidValue and following are ignored by the new + // API, as they are not present in the new interface anymore. + // You should use buildViolation() instead. + $this->violations->add(new ConstraintViolation( $this->translator->trans($message, $parameters, $this->translationDomain), $message, @@ -259,4 +267,49 @@ public function getPropertyPath($subPath = '') return PropertyPath::append($propertyPath, $subPath); } + + /** + * {@inheritdoc} + */ + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) + { + throw new BadMethodCallException( + 'addViolationAt() is not supported anymore in the new API. '. + 'Please use buildViolation() or enable the legacy mode.' + ); + } + + /** + * {@inheritdoc} + */ + public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false) + { + throw new BadMethodCallException( + 'validate() is not supported anymore in the new API. '. + 'Please use getValidator() or enable the legacy mode.' + ); + } + + /** + * {@inheritdoc} + */ + public function validateValue($value, $constraints, $subPath = '', $groups = null) + { + throw new BadMethodCallException( + 'validateValue() is not supported anymore in the new API. '. + 'Please use getValidator() or enable the legacy mode.' + ); + } + + /** + * {@inheritdoc} + */ + public function getMetadataFactory() + { + throw new BadMethodCallException( + 'getMetadataFactory() is not supported anymore in the new API. '. + 'Please use getMetadataFor() or hasMetadataFor() or enable the '. + 'legacy mode.' + ); + } } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php index decefdc8a881d..6214eabf46a66 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php @@ -110,13 +110,14 @@ public function startContext($root) ); } - $this->currentContext = new LegacyExecutionContext( + $this->currentContext = $this->createContext( $root, $this->validator, $this->groupManager, $this->translator, $this->translationDomain ); + $this->contextStack->push($this->currentContext); return $this->currentContext; @@ -188,4 +189,27 @@ public function leaveNode(Node $node) $this->currentContext->popNode(); } + + /** + * Creates a new context. + * + * Can be overridden by subclasses. + * + * @param mixed $root The root value of the + * validated object graph + * @param ValidatorInterface $validator The validator + * @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 + * + * @return ExecutionContextInterface The created context + */ + protected function createContext($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain) + { + return new ExecutionContext($root, $validator, $groupManager, $translator, $translationDomain); + } } diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContextManager.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContextManager.php new file mode 100644 index 0000000000000..361445bbfc080 --- /dev/null +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContextManager.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\Context; + +use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * A context manager that creates contexts compatible to the API < Symfony 2.5. + * + * @since 2.5 + * @author Bernhard Schussek + * + * @see ExecutionContextManagerInterface + * @see \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface + */ +class LegacyExecutionContextManager extends ExecutionContextManager +{ + /** + * {@inheritdoc} + */ + protected function createContext($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain) + { + return new LegacyExecutionContext($root, $validator, $groupManager, $translator, $translationDomain); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php index ba39823be6e16..b03eacf71e90a 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php @@ -11,9 +11,10 @@ namespace Symfony\Component\Validator\Tests\Fixtures; -use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\MetadataInterface; class FakeMetadataFactory implements MetadataFactoryInterface { @@ -53,4 +54,9 @@ public function addMetadata(ClassMetadata $metadata) { $this->metadatas[$metadata->getClassName()] = $metadata; } + + public function addMetadataForValue($value, MetadataInterface $metadata) + { + $this->metadatas[$value] = $metadata; + } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php new file mode 100644 index 0000000000000..48d8f07141d07 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -0,0 +1,407 @@ + + * + * 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\Constraints\Callback; +use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\Traverse; +use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\ConstraintViolationInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Tests\Fixtures\Entity; +use Symfony\Component\Validator\Tests\Fixtures\Reference; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * Verifies that a validator satisfies the API of Symfony 2.5+. + * + * @since 2.5 + * @author Bernhard Schussek + */ +abstract class Abstract2Dot5ApiTest extends AbstractValidatorTest +{ + /** + * @var ValidatorInterface + */ + protected $validator; + + /** + * @param MetadataFactoryInterface $metadataFactory + * + * @return ValidatorInterface + */ + abstract protected function createValidator(MetadataFactoryInterface $metadataFactory); + + protected function setUp() + { + parent::setUp(); + + $this->validator = $this->createValidator($this->metadataFactory); + } + + protected function validate($value, $constraints, $groups = null) + { + return $this->validator->validate($value, $constraints, $groups); + } + + protected function validateObject($object, $groups = null) + { + return $this->validator->validateObject($object, $groups); + } + + protected function validateCollection($collection, $groups = null, $deep = false) + { + return $this->validator->validateCollection($collection, $groups, $deep); + } + + protected function validateProperty($object, $propertyName, $groups = null) + { + return $this->validator->validateProperty($object, $propertyName, $groups); + } + + protected function validatePropertyValue($object, $propertyName, $value, $groups = null) + { + return $this->validator->validatePropertyValue($object, $propertyName, $value, $groups); + } + + public function testNoDuplicateValidationIfConstraintInMultipleGroups() + { + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => array('Group 1', 'Group 2'), + ))); + + $violations = $this->validateObject($entity, array('Group 1', 'Group 2')); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testGroupSequenceAbortsAfterFailedGroup() + { + $entity = new Entity(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message 1'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message 2'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => function () {}, + 'groups' => 'Group 1', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group 2', + ))); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 3', + ))); + + $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3')); + $violations = $this->validateObject($entity, $sequence); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message 1', $violations[0]->getMessage()); + } + + public function testGroupSequenceIncludesReferences() + { + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Reference violation 1'); + }; + $callback2 = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Reference violation 2'); + }; + + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group 1', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group 2', + ))); + + $sequence = new GroupSequence(array('Group 1', 'Entity')); + $violations = $this->validateObject($entity, $sequence); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Reference violation 1', $violations[0]->getMessage()); + } + + public function testValidateInSeparateContext() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $violations = $context + ->getValidator() + // Since the validator is not context aware, the group must + // be passed explicitly + ->validateObject($value->reference, 'Group') + ; + + /** @var ConstraintViolationInterface[] $violations */ + $test->assertCount(1, $violations); + $test->assertSame('Message value', $violations[0]->getMessage()); + $test->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $test->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $test->assertSame('', $violations[0]->getPropertyPath()); + // The root is different as we're in a new context + $test->assertSame($entity->reference, $violations[0]->getRoot()); + $test->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $test->assertNull($violations[0]->getMessagePluralization()); + $test->assertNull($violations[0]->getCode()); + + // Verify that this method is called + $context->addViolation('Separate violation'); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($entity->reference, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validateObject($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $test->assertSame('Separate violation', $violations[0]->getMessage()); + } + + public function testValidateInContext() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context + ->getValidator() + ->inContext($context) + ->atPath('subpath') + ->validateObject($value->reference) + ; + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('subpath', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validateObject($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('subpath', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testValidateArrayInContext() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context + ->getValidator() + ->inContext($context) + ->atPath('subpath') + ->validateCollection(array('key' => $value->reference)) + ; + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('subpath[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validateObject($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('subpath[key]', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testValidateAcceptsValid() + { + $test = $this; + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + // This is the same as when calling validateObject() + $violations = $this->validator->validate($entity, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException + */ + public function testExpectTraversableIfTraverse() + { + $entity = new Entity(); + + $this->validate($entity, new Traverse()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException + */ + public function testExpectTraversableIfTraverseOnClass() + { + $entity = new Entity(); + + $this->metadata->addConstraint(new Traverse()); + + $this->validateObject($entity); + } + + public function testAddCustomizedViolation() + { + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->buildViolation('Message %param%') + ->setParameter('%param%', 'value') + ->setInvalidValue('Invalid value') + ->setPluralization(2) + ->setCode('Code') + ->addViolation(); + }; + + $this->metadata->addConstraint(new Callback($callback)); + + $violations = $this->validateObject($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Invalid value', $violations[0]->getInvalidValue()); + $this->assertSame(2, $violations[0]->getMessagePluralization()); + $this->assertSame('Code', $violations[0]->getCode()); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php new file mode 100644 index 0000000000000..4d76fa38c18aa --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php @@ -0,0 +1,224 @@ + + * + * 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\Constraints\Callback; +use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\ConstraintViolationInterface; +use Symfony\Component\Validator\ExecutionContextInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Tests\Fixtures\Entity; +use Symfony\Component\Validator\Tests\Fixtures\Reference; +use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; + +/** + * Verifies that a validator satisfies the API of Symfony < 2.5. + * + * @since 2.5 + * @author Bernhard Schussek + */ +abstract class AbstractLegacyApiTest extends AbstractValidatorTest +{ + /** + * @var LegacyValidatorInterface + */ + protected $validator; + + /** + * @param MetadataFactoryInterface $metadataFactory + * + * @return LegacyValidatorInterface + */ + abstract protected function createValidator(MetadataFactoryInterface $metadataFactory); + + protected function setUp() + { + parent::setUp(); + + $this->validator = $this->createValidator($this->metadataFactory); + } + + protected function validate($value, $constraints, $groups = null) + { + return $this->validator->validateValue($value, $constraints, $groups); + } + + protected function validateObject($object, $groups = null) + { + return $this->validator->validate($object, $groups); + } + + protected function validateCollection($collection, $groups = null, $deep = false) + { + return $this->validator->validate($collection, $groups, true, $deep); + } + + protected function validateProperty($object, $propertyName, $groups = null) + { + return $this->validator->validateProperty($object, $propertyName, $groups); + } + + protected function validatePropertyValue($object, $propertyName, $value, $groups = null) + { + return $this->validator->validatePropertyValue($object, $propertyName, $value, $groups); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testTraversableTraverseDisabled() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array('key' => $entity)); + + $callback = function () use ($test) { + $test->fail('Should not be called'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $this->validator->validate($traversable, 'Group'); + } + + public function testValidateInContext() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->validate($value->reference, 'subpath'); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('subpath', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('subpath', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testValidateArrayInContext() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new Reference(); + + $callback1 = function ($value, ExecutionContextInterface $context) { + $context->validate(array('key' => $value->reference), 'subpath'); + }; + + $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { + $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('subpath[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->referenceMetadata, $context->getMetadata()); + $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); + $test->assertSame($entity, $context->getRoot()); + $test->assertSame($entity->reference, $context->getValue()); + $test->assertSame($entity->reference, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback1, + 'groups' => 'Group', + ))); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback2, + 'groups' => 'Group', + ))); + + $violations = $this->validator->validate($entity, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('subpath[key]', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } + + public function testAddCustomizedViolation() + { + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation( + 'Message %param%', + array('%param%' => 'value'), + 'Invalid value', + 2, + 'Code' + ); + }; + + $this->metadata->addConstraint(new Callback($callback)); + + $violations = $this->validateObject($entity); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame($entity, $violations[0]->getRoot()); + $this->assertSame('Invalid value', $violations[0]->getInvalidValue()); + $this->assertSame(2, $violations[0]->getMessagePluralization()); + $this->assertSame('Code', $violations[0]->getCode()); + } + + public function testGetMetadataFactory() + { + $this->assertSame($this->metadataFactory, $this->validator->getMetadataFactory()); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index 36b13cfd96444..99d250fb5a587 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -11,25 +11,16 @@ namespace Symfony\Component\Validator\Tests\Validator; -use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\GroupSequence; -use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\ConstraintViolationInterface; -use Symfony\Component\Validator\Context\ExecutionContext; use Symfony\Component\Validator\ExecutionContextInterface; -use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Tests\Fixtures\GroupSequenceProviderEntity; use Symfony\Component\Validator\Tests\Fixtures\Reference; -use Symfony\Component\Validator\ConstraintViolation; -use Symfony\Component\Validator\ConstraintViolationList; -use Symfony\Component\Validator\Tests\Fixtures\FailingConstraint; use Symfony\Component\Validator\Tests\Fixtures\Entity; -use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\ValidatorInterface; /** * @since 2.5 @@ -41,11 +32,6 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase const REFERENCE_CLASS = 'Symfony\Component\Validator\Tests\Fixtures\Reference'; - /** - * @var ValidatorInterface - */ - protected $validator; - /** * @var FakeMetadataFactory */ @@ -64,7 +50,6 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->metadataFactory = new FakeMetadataFactory(); - $this->validator = $this->createValidator($this->metadataFactory); $this->metadata = new ClassMetadata(self::ENTITY_CLASS); $this->referenceMetadata = new ClassMetadata(self::REFERENCE_CLASS); $this->metadataFactory->addMetadata($this->metadata); @@ -74,12 +59,54 @@ protected function setUp() protected function tearDown() { $this->metadataFactory = null; - $this->validator = null; $this->metadata = null; $this->referenceMetadata = null; } - abstract protected function createValidator(MetadataFactoryInterface $metadataFactory); + abstract protected function validate($value, $constraints, $groups = null); + + abstract protected function validateObject($object, $groups = null); + + abstract protected function validateCollection($collection, $groups = null, $deep = false); + + abstract protected function validateProperty($object, $propertyName, $groups = null); + + abstract protected function validatePropertyValue($object, $propertyName, $value, $groups = null); + + public function testValidate() + { + $test = $this; + + $callback = function ($value, ExecutionContextInterface $context) use ($test) { + $test->assertNull($context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame('Bernhard', $context->getRoot()); + $test->assertSame('Bernhard', $context->getValue()); + $test->assertSame('Bernhard', $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; + + $constraint = new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + )); + + $violations = $this->validate('Bernhard', $constraint, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('', $violations[0]->getPropertyPath()); + $this->assertSame('Bernhard', $violations[0]->getRoot()); + $this->assertSame('Bernhard', $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); + } public function testClassConstraint() { @@ -92,7 +119,6 @@ public function testClassConstraint() $test->assertSame('', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame($entity, $context->getValue()); $test->assertSame($entity, $value); @@ -105,7 +131,7 @@ public function testClassConstraint() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -133,7 +159,6 @@ public function testPropertyConstraint() $test->assertSame('firstName', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($propertyMetadatas[0], $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame('Bernhard', $context->getValue()); $test->assertSame('Bernhard', $value); @@ -146,7 +171,7 @@ public function testPropertyConstraint() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -174,7 +199,6 @@ public function testGetterConstraint() $test->assertSame('lastName', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($propertyMetadatas[0], $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame('Schussek', $context->getValue()); $test->assertSame('Schussek', $value); @@ -187,7 +211,7 @@ public function testGetterConstraint() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -213,46 +237,6 @@ public function testArray() $test->assertSame('[key]', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($array, $context->getRoot()); - $test->assertSame($entity, $context->getValue()); - $test->assertSame($entity, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validateCollection($array, 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('[key]', $violations[0]->getPropertyPath()); - $this->assertSame($array, $violations[0]->getRoot()); - $this->assertSame($entity, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testArrayLegacyApi() - { - $test = $this; - $entity = new Entity(); - $array = array('key' => $entity); - - $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $array) { - $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('[key]', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($array, $context->getRoot()); $test->assertSame($entity, $context->getValue()); $test->assertSame($entity, $value); @@ -265,7 +249,7 @@ public function testArrayLegacyApi() 'groups' => 'Group', ))); - $violations = $this->validator->validate($array, 'Group'); + $violations = $this->validateCollection($array, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -291,46 +275,6 @@ public function testRecursiveArray() $test->assertSame('[2][key]', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($array, $context->getRoot()); - $test->assertSame($entity, $context->getValue()); - $test->assertSame($entity, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validateCollection($array, 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('[2][key]', $violations[0]->getPropertyPath()); - $this->assertSame($array, $violations[0]->getRoot()); - $this->assertSame($entity, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testRecursiveArrayLegacyApi() - { - $test = $this; - $entity = new Entity(); - $array = array(2 => array('key' => $entity)); - - $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $array) { - $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('[2][key]', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($array, $context->getRoot()); $test->assertSame($entity, $context->getValue()); $test->assertSame($entity, $value); @@ -343,7 +287,7 @@ public function testRecursiveArrayLegacyApi() 'groups' => 'Group', ))); - $violations = $this->validator->validate($array, 'Group'); + $violations = $this->validateCollection($array, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -357,46 +301,7 @@ public function testRecursiveArrayLegacyApi() $this->assertNull($violations[0]->getCode()); } - public function testTraversableTraverseEnabled() - { - $test = $this; - $entity = new Entity(); - $traversable = new \ArrayIterator(array('key' => $entity)); - - $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) { - $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('[key]', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($traversable, $context->getRoot()); - $test->assertSame($entity, $context->getValue()); - $test->assertSame($entity, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validateCollection($traversable, 'Group', true); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('[key]', $violations[0]->getPropertyPath()); - $this->assertSame($traversable, $violations[0]->getRoot()); - $this->assertSame($entity, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testTraversableTraverseEnabledLegacyApi() + public function testTraversable() { $test = $this; $entity = new Entity(); @@ -408,7 +313,6 @@ public function testTraversableTraverseEnabledLegacyApi() $test->assertSame('[key]', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($traversable, $context->getRoot()); $test->assertSame($entity, $context->getValue()); $test->assertSame($entity, $value); @@ -421,7 +325,7 @@ public function testTraversableTraverseEnabledLegacyApi() 'groups' => 'Group', ))); - $violations = $this->validator->validate($traversable, 'Group', true); + $violations = $this->validateCollection($traversable, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -435,27 +339,6 @@ public function testTraversableTraverseEnabledLegacyApi() $this->assertNull($violations[0]->getCode()); } - /** - * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException - */ - public function testTraversableTraverseDisabledLegacyApi() - { - $test = $this; - $entity = new Entity(); - $traversable = new \ArrayIterator(array('key' => $entity)); - - $callback = function () use ($test) { - $test->fail('Should not be called'); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - $this->validator->validate($traversable, 'Group'); - } - /** * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException */ @@ -476,30 +359,7 @@ public function testRecursiveTraversableRecursiveTraversalDisabled() 'groups' => 'Group', ))); - $this->validator->validateCollection($traversable, 'Group'); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException - */ - public function testRecursiveTraversableRecursiveTraversalDisabledLegacyApi() - { - $test = $this; - $entity = new Entity(); - $traversable = new \ArrayIterator(array( - 2 => new \ArrayIterator(array('key' => $entity)), - )); - - $callback = function () use ($test) { - $test->fail('Should not be called'); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - $this->validator->validate($traversable, 'Group', true); + $this->validateCollection($traversable, 'Group'); } public function testRecursiveTraversableRecursiveTraversalEnabled() @@ -516,48 +376,6 @@ public function testRecursiveTraversableRecursiveTraversalEnabled() $test->assertSame('[2][key]', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($traversable, $context->getRoot()); - $test->assertSame($entity, $context->getValue()); - $test->assertSame($entity, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validateCollection($traversable, 'Group', true); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('[2][key]', $violations[0]->getPropertyPath()); - $this->assertSame($traversable, $violations[0]->getRoot()); - $this->assertSame($entity, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testRecursiveTraversableRecursiveTraversalEnabledLegacyApi() - { - $test = $this; - $entity = new Entity(); - $traversable = new \ArrayIterator(array( - 2 => new \ArrayIterator(array('key' => $entity)), - )); - - $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) { - $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('[2][key]', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($traversable, $context->getRoot()); $test->assertSame($entity, $context->getValue()); $test->assertSame($entity, $value); @@ -570,7 +388,7 @@ public function testRecursiveTraversableRecursiveTraversalEnabledLegacyApi() 'groups' => 'Group', ))); - $violations = $this->validator->validate($traversable, 'Group', true, true); + $violations = $this->validateCollection($traversable, 'Group', true); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -596,7 +414,6 @@ public function testReferenceClassConstraint() $test->assertSame('reference', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame($entity->reference, $context->getValue()); $test->assertSame($entity->reference, $value); @@ -610,7 +427,7 @@ public function testReferenceClassConstraint() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -639,7 +456,6 @@ public function testReferencePropertyConstraint() $test->assertSame('reference.value', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($propertyMetadatas[0], $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame('Foobar', $context->getValue()); $test->assertSame('Foobar', $value); @@ -653,7 +469,7 @@ public function testReferencePropertyConstraint() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -682,7 +498,6 @@ public function testReferenceGetterConstraint() $test->assertSame('reference.privateValue', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($propertyMetadatas[0], $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame('Bamboo', $context->getValue()); $test->assertSame('Bamboo', $value); @@ -696,7 +511,7 @@ public function testReferenceGetterConstraint() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -717,7 +532,7 @@ public function testsIgnoreNullReference() $this->metadata->addPropertyConstraint('reference', new Valid()); - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -733,7 +548,7 @@ public function testFailOnScalarReferences() $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->validator->validate($entity); + $this->validateObject($entity); } public function testArrayReference() @@ -748,7 +563,6 @@ public function testArrayReference() $test->assertSame('reference[key]', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame($entity->reference['key'], $context->getValue()); $test->assertSame($entity->reference['key'], $value); @@ -762,7 +576,7 @@ public function testArrayReference() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -789,7 +603,6 @@ public function testRecursiveArrayReference() $test->assertSame('reference[2][key]', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame($entity->reference[2]['key'], $context->getValue()); $test->assertSame($entity->reference[2]['key'], $value); @@ -803,7 +616,7 @@ public function testRecursiveArrayReference() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -831,7 +644,7 @@ public function testArrayTraversalCannotBeDisabled() ))); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -851,7 +664,7 @@ public function testRecursiveArrayTraversalCannotBeDisabled() ))); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -864,7 +677,7 @@ public function testIgnoreScalarsDuringArrayTraversal() $this->metadata->addPropertyConstraint('reference', new Valid()); - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -877,7 +690,7 @@ public function testIgnoreNullDuringArrayTraversal() $this->metadata->addPropertyConstraint('reference', new Valid()); - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -895,7 +708,6 @@ public function testTraversableReference() $test->assertSame('reference[key]', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame($entity->reference['key'], $context->getValue()); $test->assertSame($entity->reference['key'], $value); @@ -909,7 +721,7 @@ public function testTraversableReference() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -938,7 +750,7 @@ public function testDisableTraversableTraversal() ))); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -956,7 +768,7 @@ public function testMetadataMustExistIfTraversalIsDisabled() 'traverse' => false, ))); - $this->validator->validate($entity, 'Default', ''); + $this->validateObject($entity, 'Default', ''); } public function testNoRecursiveTraversableTraversal() @@ -974,7 +786,7 @@ public function testNoRecursiveTraversableTraversal() $this->metadata->addPropertyConstraint('reference', new Valid()); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -994,7 +806,6 @@ public function testEnableRecursiveTraversableTraversal() $test->assertSame('reference[2][key]', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame($entity->reference[2]['key'], $context->getValue()); $test->assertSame($entity->reference[2]['key'], $value); @@ -1010,7 +821,7 @@ public function testEnableRecursiveTraversableTraversal() 'groups' => 'Group', ))); - $violations = $this->validator->validate($entity, 'Group'); + $violations = $this->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1039,7 +850,6 @@ public function testValidateProperty() $test->assertSame('firstName', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($propertyMetadatas[0], $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame('Bernhard', $context->getValue()); $test->assertSame('Bernhard', $value); @@ -1060,7 +870,7 @@ public function testValidateProperty() 'groups' => 'Group', ))); - $violations = $this->validator->validateProperty($entity, 'firstName', 'Group'); + $violations = $this->validateProperty($entity, 'firstName', 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1082,14 +892,9 @@ public function testValidatePropertyFailsIfPropertiesNotSupported() // $metadata does not implement PropertyMetadataContainerInterface $metadata = $this->getMock('Symfony\Component\Validator\MetadataInterface'); - $metadataFactory = $this->getMock('Symfony\Component\Validator\MetadataFactoryInterface'); - $metadataFactory->expects($this->any()) - ->method('getMetadataFor') - ->with('VALUE') - ->will($this->returnValue($metadata)); - $validator = $this->createValidator($metadataFactory); + $this->metadataFactory->addMetadataForValue('VALUE', $metadata); - $validator->validateProperty('VALUE', 'someProperty'); + $this->validateProperty('VALUE', 'someProperty'); } public function testValidatePropertyValue() @@ -1106,7 +911,6 @@ public function testValidatePropertyValue() $test->assertSame('firstName', $context->getPropertyPath()); $test->assertSame('Group', $context->getGroup()); $test->assertSame($propertyMetadatas[0], $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); $test->assertSame($entity, $context->getRoot()); $test->assertSame('Bernhard', $context->getValue()); $test->assertSame('Bernhard', $value); @@ -1127,7 +931,7 @@ public function testValidatePropertyValue() 'groups' => 'Group', ))); - $violations = $this->validator->validatePropertyValue( + $violations = $this->validatePropertyValue( $entity, 'firstName', 'Bernhard', @@ -1154,97 +958,26 @@ public function testValidatePropertyValueFailsIfPropertiesNotSupported() // $metadata does not implement PropertyMetadataContainerInterface $metadata = $this->getMock('Symfony\Component\Validator\MetadataInterface'); - $metadataFactory = $this->getMock('Symfony\Component\Validator\MetadataFactoryInterface'); - $metadataFactory->expects($this->any()) - ->method('getMetadataFor') - ->with('VALUE') - ->will($this->returnValue($metadata)); - $validator = $this->createValidator($metadataFactory); + $this->metadataFactory->addMetadataForValue('VALUE', $metadata); - $validator->validatePropertyValue('VALUE', 'someProperty', 'someValue'); + $this->validatePropertyValue('VALUE', 'someProperty', 'someValue'); } - public function testValidateValue() + public function testValidateObjectOnlyOncePerGroup() { - $test = $this; - - $callback = function ($value, ExecutionContextInterface $context) use ($test) { - $test->assertNull($context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame('Bernhard', $context->getRoot()); - $test->assertSame('Bernhard', $context->getValue()); - $test->assertSame('Bernhard', $value); + $entity = new Entity(); + $entity->reference = new Reference(); + $entity->reference2 = $entity->reference; - $context->addViolation('Message %param%', array('%param%' => 'value')); + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); }; - $constraint = new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - )); + $this->metadata->addPropertyConstraint('reference', new Valid()); + $this->metadata->addPropertyConstraint('reference2', new Valid()); + $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validator->validateValue('Bernhard', $constraint, 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('', $violations[0]->getPropertyPath()); - $this->assertSame('Bernhard', $violations[0]->getRoot()); - $this->assertSame('Bernhard', $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testAddCustomizedViolation() - { - $entity = new Entity(); - - $callback = function ($value, ExecutionContextInterface $context) { - $context->addViolation( - 'Message %param%', - array('%param%' => 'value'), - 'Invalid value', - 2, - 'Code' - ); - }; - - $this->metadata->addConstraint(new Callback($callback)); - - $violations = $this->validator->validate($entity); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('', $violations[0]->getPropertyPath()); - $this->assertSame($entity, $violations[0]->getRoot()); - $this->assertSame('Invalid value', $violations[0]->getInvalidValue()); - $this->assertSame(2, $violations[0]->getMessagePluralization()); - $this->assertSame('Code', $violations[0]->getCode()); - } - - public function testValidateObjectOnlyOncePerGroup() - { - $entity = new Entity(); - $entity->reference = new Reference(); - $entity->reference2 = $entity->reference; - - $callback = function ($value, ExecutionContextInterface $context) { - $context->addViolation('Message'); - }; - - $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->metadata->addPropertyConstraint('reference2', new Valid()); - $this->referenceMetadata->addConstraint(new Callback($callback)); - - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1264,7 +997,7 @@ public function testValidateDifferentObjectsSeparately() $this->metadata->addPropertyConstraint('reference2', new Valid()); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validator->validate($entity); + $violations = $this->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(2, $violations); @@ -1287,7 +1020,7 @@ public function testValidateSingleGroup() 'groups' => 'Group 2', ))); - $violations = $this->validator->validate($entity, 'Group 2'); + $violations = $this->validateObject($entity, 'Group 2'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1310,93 +1043,12 @@ public function testValidateMultipleGroups() 'groups' => 'Group 2', ))); - $violations = $this->validator->validate($entity, array('Group 1', 'Group 2')); + $violations = $this->validateObject($entity, array('Group 1', 'Group 2')); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(2, $violations); } - public function testNoDuplicateValidationIfConstraintInMultipleGroups() - { - $entity = new Entity(); - - $callback = function ($value, ExecutionContextInterface $context) { - $context->addViolation('Message'); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => array('Group 1', 'Group 2'), - ))); - - $violations = $this->validator->validate($entity, array('Group 1', 'Group 2')); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - } - - public function testGroupSequenceAbortsAfterFailedGroup() - { - $entity = new Entity(); - - $callback1 = function ($value, ExecutionContextInterface $context) { - $context->addViolation('Message 1'); - }; - $callback2 = function ($value, ExecutionContextInterface $context) { - $context->addViolation('Message 2'); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => function () {}, - 'groups' => 'Group 1', - ))); - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback1, - 'groups' => 'Group 2', - ))); - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback2, - 'groups' => 'Group 3', - ))); - - $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3')); - $violations = $this->validator->validate($entity, $sequence); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message 1', $violations[0]->getMessage()); - } - - public function testGroupSequenceIncludesReferences() - { - $entity = new Entity(); - $entity->reference = new Reference(); - - $callback1 = function ($value, ExecutionContextInterface $context) { - $context->addViolation('Reference violation 1'); - }; - $callback2 = function ($value, ExecutionContextInterface $context) { - $context->addViolation('Reference violation 2'); - }; - - $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->referenceMetadata->addConstraint(new Callback(array( - 'callback' => $callback1, - 'groups' => 'Group 1', - ))); - $this->referenceMetadata->addConstraint(new Callback(array( - 'callback' => $callback2, - 'groups' => 'Group 2', - ))); - - $sequence = new GroupSequence(array('Group 1', 'Entity')); - $violations = $this->validator->validate($entity, $sequence); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Reference violation 1', $violations[0]->getMessage()); - } - public function testReplaceDefaultGroupByGroupSequenceObject() { $entity = new Entity(); @@ -1424,7 +1076,7 @@ public function testReplaceDefaultGroupByGroupSequenceObject() $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3', 'Entity')); $this->metadata->setGroupSequence($sequence); - $violations = $this->validator->validate($entity, 'Default'); + $violations = $this->validateObject($entity, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1458,7 +1110,7 @@ public function testReplaceDefaultGroupByGroupSequenceArray() $sequence = array('Group 1', 'Group 2', 'Group 3', 'Entity'); $this->metadata->setGroupSequence($sequence); - $violations = $this->validator->validate($entity, 'Default'); + $violations = $this->validateObject($entity, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1490,7 +1142,7 @@ public function testPropagateDefaultGroupToReferenceWhenReplacingDefaultGroup() $sequence = new GroupSequence(array('Group 1', 'Entity')); $this->metadata->setGroupSequence($sequence); - $violations = $this->validator->validate($entity, 'Default'); + $violations = $this->validateObject($entity, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1520,7 +1172,7 @@ public function testValidateCustomGroupWhenDefaultGroupWasReplaced() $sequence = new GroupSequence(array('Group 1', 'Entity')); $this->metadata->setGroupSequence($sequence); - $violations = $this->validator->validate($entity, 'Other Group'); + $violations = $this->validateObject($entity, 'Other Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1556,7 +1208,7 @@ public function testReplaceDefaultGroupWithObjectFromGroupSequenceProvider() $this->metadataFactory->addMetadata($metadata); - $violations = $this->validator->validate($entity, 'Default'); + $violations = $this->validateObject($entity, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1592,295 +1244,10 @@ public function testReplaceDefaultGroupWithArrayFromGroupSequenceProvider() $this->metadataFactory->addMetadata($metadata); - $violations = $this->validator->validate($entity, 'Default'); + $violations = $this->validateObject($entity, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); $this->assertSame('Violation in Group 2', $violations[0]->getMessage()); } - - public function testValidateInContext() - { - $test = $this; - $entity = new Entity(); - $entity->reference = new Reference(); - - $callback1 = function ($value, ExecutionContextInterface $context) { - $context - ->getValidator() - ->inContext($context) - ->atPath('subpath') - ->validateObject($value->reference) - ; - }; - - $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { - $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('subpath', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($entity, $context->getRoot()); - $test->assertSame($entity->reference, $context->getValue()); - $test->assertSame($entity->reference, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback1, - 'groups' => 'Group', - ))); - $this->referenceMetadata->addConstraint(new Callback(array( - 'callback' => $callback2, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validate($entity, 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('subpath', $violations[0]->getPropertyPath()); - $this->assertSame($entity, $violations[0]->getRoot()); - $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testValidateInContextLegacyApi() - { - $test = $this; - $entity = new Entity(); - $entity->reference = new Reference(); - - $callback1 = function ($value, ExecutionContextInterface $context) { - $context->validate($value->reference, 'subpath'); - }; - - $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { - $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('subpath', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($entity, $context->getRoot()); - $test->assertSame($entity->reference, $context->getValue()); - $test->assertSame($entity->reference, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback1, - 'groups' => 'Group', - ))); - $this->referenceMetadata->addConstraint(new Callback(array( - 'callback' => $callback2, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validate($entity, 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('subpath', $violations[0]->getPropertyPath()); - $this->assertSame($entity, $violations[0]->getRoot()); - $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testValidateArrayInContext() - { - $test = $this; - $entity = new Entity(); - $entity->reference = new Reference(); - - $callback1 = function ($value, ExecutionContextInterface $context) { - $context - ->getValidator() - ->inContext($context) - ->atPath('subpath') - ->validateCollection(array('key' => $value->reference)) - ; - }; - - $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { - $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('subpath[key]', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($entity, $context->getRoot()); - $test->assertSame($entity->reference, $context->getValue()); - $test->assertSame($entity->reference, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback1, - 'groups' => 'Group', - ))); - $this->referenceMetadata->addConstraint(new Callback(array( - 'callback' => $callback2, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validate($entity, 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('subpath[key]', $violations[0]->getPropertyPath()); - $this->assertSame($entity, $violations[0]->getRoot()); - $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testValidateArrayInContextLegacyApi() - { - $test = $this; - $entity = new Entity(); - $entity->reference = new Reference(); - - $callback1 = function ($value, ExecutionContextInterface $context) { - $context->validate(array('key' => $value->reference), 'subpath'); - }; - - $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { - $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('subpath[key]', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($entity, $context->getRoot()); - $test->assertSame($entity->reference, $context->getValue()); - $test->assertSame($entity->reference, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback1, - 'groups' => 'Group', - ))); - $this->referenceMetadata->addConstraint(new Callback(array( - 'callback' => $callback2, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validate($entity, 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('subpath[key]', $violations[0]->getPropertyPath()); - $this->assertSame($entity, $violations[0]->getRoot()); - $this->assertSame($entity->reference, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - - public function testValidateInSeparateContext() - { - $test = $this; - $entity = new Entity(); - $entity->reference = new Reference(); - - $callback1 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { - $violations = $context - ->getValidator() - // Since the validator is not context aware, the group must - // be passed explicitly - ->validateObject($value->reference, 'Group') - ; - - /** @var ConstraintViolationInterface[] $violations */ - $test->assertCount(1, $violations); - $test->assertSame('Message value', $violations[0]->getMessage()); - $test->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $test->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $test->assertSame('', $violations[0]->getPropertyPath()); - // The root is different as we're in a new context - $test->assertSame($entity->reference, $violations[0]->getRoot()); - $test->assertSame($entity->reference, $violations[0]->getInvalidValue()); - $test->assertNull($violations[0]->getMessagePluralization()); - $test->assertNull($violations[0]->getCode()); - - // Verify that this method is called - $context->addViolation('Separate violation'); - }; - - $callback2 = function ($value, ExecutionContextInterface $context) use ($test, $entity) { - $test->assertSame($test::REFERENCE_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->referenceMetadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($entity->reference, $context->getRoot()); - $test->assertSame($entity->reference, $context->getValue()); - $test->assertSame($entity->reference, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback1, - 'groups' => 'Group', - ))); - $this->referenceMetadata->addConstraint(new Callback(array( - 'callback' => $callback2, - 'groups' => 'Group', - ))); - - $violations = $this->validator->validate($entity, 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $test->assertSame('Separate violation', $violations[0]->getMessage()); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException - */ - public function testExpectTraversableIfTraverse() - { - $entity = new Entity(); - - $this->validator->validateValue($entity, new Traverse()); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException - */ - public function testExpectTraversableIfTraverseOnClass() - { - $entity = new Entity(); - - $this->metadata->addConstraint(new Traverse()); - - $this->validator->validate($entity); - } - - public function testGetMetadataFactory() - { - $this->assertSame($this->metadataFactory, $this->validator->getMetadataFactory()); - } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php new file mode 100644 index 0000000000000..e3715731ce081 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -0,0 +1,47 @@ + + * + * 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\LegacyExecutionContextManager; +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\Validator\LegacyValidator; + +class LegacyValidator2Dot5ApiTest extends Abstract2Dot5ApiTest +{ + protected function createValidator(MetadataFactoryInterface $metadataFactory) + { + $nodeTraverser = new NodeTraverser($metadataFactory); + $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); + $contextManager = new LegacyExecutionContextManager($nodeValidator, new DefaultTranslator()); + $validator = new LegacyValidator($nodeTraverser, $metadataFactory, $contextManager); + $groupSequenceResolver = new GroupSequenceResolver(); + + // The context manager needs the validator for passing it to created + // contexts + $contextManager->initialize($validator); + + // The node validator needs the context manager for passing the current + // context to the constraint validators + $nodeValidator->initialize($contextManager); + + $nodeTraverser->addVisitor($groupSequenceResolver); + $nodeTraverser->addVisitor($contextManager); + $nodeTraverser->addVisitor($nodeValidator); + + return $validator; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php new file mode 100644 index 0000000000000..c106d765c12c3 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -0,0 +1,47 @@ + + * + * 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\LegacyExecutionContextManager; +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\Validator\LegacyValidator; + +class LegacyValidatorLegacyApiTest extends AbstractLegacyApiTest +{ + protected function createValidator(MetadataFactoryInterface $metadataFactory) + { + $nodeTraverser = new NodeTraverser($metadataFactory); + $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); + $contextManager = new LegacyExecutionContextManager($nodeValidator, new DefaultTranslator()); + $validator = new LegacyValidator($nodeTraverser, $metadataFactory, $contextManager); + $groupSequenceResolver = new GroupSequenceResolver(); + + // The context manager needs the validator for passing it to created + // contexts + $contextManager->initialize($validator); + + // The node validator needs the context manager for passing the current + // context to the constraint validators + $nodeValidator->initialize($contextManager); + + $nodeTraverser->addVisitor($groupSequenceResolver); + $nodeTraverser->addVisitor($contextManager); + $nodeTraverser->addVisitor($nodeValidator); + + return $validator; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php deleted file mode 100644 index 0d8ade455cc82..0000000000000 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorTest.php +++ /dev/null @@ -1,100 +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\Validator; - -use Symfony\Component\Validator\Constraints\Valid; -use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\Tests\Fixtures\Entity; -use Symfony\Component\Validator\Validator as LegacyValidator; -use Symfony\Component\Validator\DefaultTranslator; -use Symfony\Component\Validator\ConstraintValidatorFactory; - -class LegacyValidatorTest extends AbstractValidatorTest -{ - protected function createValidator(MetadataFactoryInterface $metadataFactory) - { - return new LegacyValidator($metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); - } - - public function testNoDuplicateValidationIfConstraintInMultipleGroups() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testGroupSequenceAbortsAfterFailedGroup() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testGroupSequenceIncludesReferences() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testValidateInContext() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testValidateArrayInContext() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testValidateInSeparateContext() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testArray() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testRecursiveArray() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testTraversableTraverseEnabled() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testRecursiveTraversableRecursiveTraversalDisabled() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testRecursiveTraversableRecursiveTraversalEnabled() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testExpectTraversableIfTraverse() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - public function testExpectTraversableIfTraverseOnClass() - { - $this->markTestSkipped('Not supported in the legacy API'); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\ValidatorException - */ - public function testValidateValueRejectsValid() - { - $this->validator->validateValue(new Entity(), new Valid()); - } -} diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php new file mode 100644 index 0000000000000..2bf4693dde361 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php @@ -0,0 +1,47 @@ + + * + * 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\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\Validator\Validator; + +class Validator2Dot5ApiTest extends Abstract2Dot5ApiTest +{ + 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); + $groupSequenceResolver = new GroupSequenceResolver(); + + // The context manager needs the validator for passing it to created + // contexts + $contextManager->initialize($validator); + + // The node validator needs the context manager for passing the current + // context to the constraint validators + $nodeValidator->initialize($contextManager); + + $nodeTraverser->addVisitor($groupSequenceResolver); + $nodeTraverser->addVisitor($contextManager); + $nodeTraverser->addVisitor($nodeValidator); + + return $validator; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php deleted file mode 100644 index 2b0d00205e887..0000000000000 --- a/src/Symfony/Component/Validator/Tests/Validator/ValidatorTest.php +++ /dev/null @@ -1,97 +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\Validator; - -use Symfony\Component\Validator\Constraints\Callback; -use Symfony\Component\Validator\Constraints\Valid; -use Symfony\Component\Validator\ConstraintViolationInterface; -use Symfony\Component\Validator\Context\ExecutionContextInterface; -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\Fixtures\Entity; -use Symfony\Component\Validator\Validator\LegacyValidator; -use Symfony\Component\Validator\Validator\ValidatorInterface; - -class ValidatorTest extends AbstractValidatorTest -{ - /** - * @var ValidatorInterface - */ - protected $validator; - - protected function createValidator(MetadataFactoryInterface $metadataFactory) - { - $nodeTraverser = new NodeTraverser($metadataFactory); - $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); - $contextManager = new ExecutionContextManager($nodeValidator, new DefaultTranslator()); - $validator = new LegacyValidator($nodeTraverser, $metadataFactory, $contextManager); - $groupSequenceResolver = new GroupSequenceResolver(); - - // The context manager needs the validator for passing it to created - // contexts - $contextManager->initialize($validator); - - // The node validator needs the context manager for passing the current - // context to the constraint validators - $nodeValidator->initialize($contextManager); - - $nodeTraverser->addVisitor($groupSequenceResolver); - $nodeTraverser->addVisitor($contextManager); - $nodeTraverser->addVisitor($nodeValidator); - - return $validator; - } - - public function testValidateAcceptsValid() - { - $test = $this; - $entity = new Entity(); - - $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { - $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($test->metadataFactory, $context->getMetadataFactory()); - $test->assertSame($entity, $context->getRoot()); - $test->assertSame($entity, $context->getValue()); - $test->assertSame($entity, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - // This is the same as when calling validateObject() - $violations = $this->validator->validate($entity, new Valid(), 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('', $violations[0]->getPropertyPath()); - $this->assertSame($entity, $violations[0]->getRoot()); - $this->assertSame($entity, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } -} diff --git a/src/Symfony/Component/Validator/Tests/ValidatorTest.php b/src/Symfony/Component/Validator/Tests/ValidatorTest.php new file mode 100644 index 0000000000000..a983a78a70819 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/ValidatorTest.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\Tests; + +use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Tests\Fixtures\Entity; +use Symfony\Component\Validator\Tests\Validator\AbstractLegacyApiTest; +use Symfony\Component\Validator\Validator as LegacyValidator; +use Symfony\Component\Validator\DefaultTranslator; +use Symfony\Component\Validator\ConstraintValidatorFactory; + +class ValidatorTest extends AbstractLegacyApiTest +{ + protected function createValidator(MetadataFactoryInterface $metadataFactory) + { + return new LegacyValidator($metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ValidatorException + */ + public function testValidateValueRejectsValid() + { + $this->validator->validateValue(new Entity(), new Valid()); + } +} From 26eafa43f7e601ecf2a7454db2e0bce65f5fa82c Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 18:16:34 +0100 Subject: [PATCH 29/86] [Validator] Removed unused use statements --- src/Symfony/Component/Validator/Context/ExecutionContext.php | 2 -- src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php | 1 - 2 files changed, 3 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 43794988056b6..f96370288ca4e 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -13,14 +13,12 @@ use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\ClassBasedInterface; -use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Exception\BadMethodCallException; use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; -use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\ValidatorInterface; diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index d9cbf62c71626..eacfd85c2c994 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Validator\NodeTraverser; -use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Mapping\CascadingStrategy; From df41974f31e8a4c14a4c4f1bd82ace0cf5cf755c Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 20:10:51 +0100 Subject: [PATCH 30/86] [Validator] Changed context manager to context factory The current context is not stored anymore. Instead, it is passed around the traverser and the visitors. For this reason, validation can occur in multiple contexts at the same time. --- .../Validator/Context/ExecutionContext.php | 50 ++-- .../Context/ExecutionContextFactory.php | 72 ++++++ .../ExecutionContextFactoryInterface.php | 37 +++ .../Context/ExecutionContextManager.php | 215 ------------------ .../ExecutionContextManagerInterface.php | 86 ------- .../Context/LegacyExecutionContext.php | 13 +- .../Context/LegacyExecutionContextFactory.php | 75 ++++++ .../Context/LegacyExecutionContextManager.php | 36 --- .../Validator/NodeTraverser/NodeTraverser.php | 114 +++++----- .../NodeTraverser/NodeTraverserInterface.php | 7 +- .../Validator/NodeVisitor/AbstractVisitor.php | 9 +- .../NodeVisitor/ContextRefresher.php | 60 +++++ .../NodeVisitor/GroupSequenceResolver.php | 3 +- .../Validator/NodeVisitor/NodeValidator.php | 38 +--- .../NodeVisitor/NodeVisitorInterface.php | 9 +- .../NodeVisitor/ObjectInitializer.php | 3 +- .../Context/ExecutionContextManagerTest.php | 95 -------- .../Tests/Context/ExecutionContextTest.php | 6 +- .../Tests/Validator/Abstract2Dot5ApiTest.php | 18 +- .../Validator/LegacyValidator2Dot5ApiTest.php | 18 +- .../LegacyValidatorLegacyApiTest.php | 18 +- .../Tests/Validator/Validator2Dot5ApiTest.php | 18 +- .../Validator/Validator/AbstractValidator.php | 188 --------------- .../Validator/ContextualValidator.php | 199 ++++++++++------ .../ContextualValidatorInterface.php | 70 +++++- .../Validator/Validator/Validator.php | 109 ++++++--- .../Validator/ValidatorInterface.php | 5 + 27 files changed, 667 insertions(+), 904 deletions(-) create mode 100644 src/Symfony/Component/Validator/Context/ExecutionContextFactory.php create mode 100644 src/Symfony/Component/Validator/Context/ExecutionContextFactoryInterface.php delete mode 100644 src/Symfony/Component/Validator/Context/ExecutionContextManager.php delete mode 100644 src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php create mode 100644 src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php delete mode 100644 src/Symfony/Component/Validator/Context/LegacyExecutionContextManager.php create mode 100644 src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php delete mode 100644 src/Symfony/Component/Validator/Tests/Context/ExecutionContextManagerTest.php delete mode 100644 src/Symfony/Component/Validator/Validator/AbstractValidator.php diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index f96370288ca4e..187de382fb84e 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -25,7 +25,7 @@ use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; /** - * The context used and created by {@link ExecutionContextManager}. + * The context used and created by {@link ExecutionContextFactory}. * * @since 2.5 * @author Bernhard Schussek @@ -34,6 +34,11 @@ */ class ExecutionContext implements ExecutionContextInterface, LegacyExecutionContextInterface { + /** + * @var ValidatorInterface + */ + private $validator; + /** * The root value of the validated object graph. * @@ -41,6 +46,21 @@ class ExecutionContext implements ExecutionContextInterface, LegacyExecutionCont */ private $root; + /** + * @var GroupManagerInterface + */ + private $groupManager; + + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var string + */ + private $translationDomain; + /** * The violations generated in the current context. * @@ -62,32 +82,12 @@ class ExecutionContext implements ExecutionContextInterface, LegacyExecutionCont */ private $nodeStack; - /** - * @var ValidatorInterface - */ - private $validator; - - /** - * @var GroupManagerInterface - */ - private $groupManager; - - /** - * @var TranslatorInterface - */ - private $translator; - - /** - * @var string - */ - private $translationDomain; - /** * Creates a new execution context. * + * @param ValidatorInterface $validator The validator * @param mixed $root The root value of the * validated object graph - * @param ValidatorInterface $validator The validator * @param GroupManagerInterface $groupManager The manager for accessing * the currently validated * group @@ -96,13 +96,13 @@ class ExecutionContext implements ExecutionContextInterface, LegacyExecutionCont * use for translating * violation messages * - * @internal Called by {@link ExecutionContextManager}. Should not be used + * @internal Called by {@link ExecutionContextFactory}. Should not be used * in user code. */ - public function __construct($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) + public function __construct(ValidatorInterface $validator, $root, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) { - $this->root = $root; $this->validator = $validator; + $this->root = $root; $this->groupManager = $groupManager; $this->translator = $translator; $this->translationDomain = $translationDomain; diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php new file mode 100644 index 0000000000000..3305e1a943452 --- /dev/null +++ b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * Creates new {@link ExecutionContext} instances. + * + * @since 2.5 + * @author Bernhard Schussek + */ +class ExecutionContextFactory implements ExecutionContextFactoryInterface +{ + /** + * @var GroupManagerInterface + */ + private $groupManager; + + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var string|null + */ + private $translationDomain; + + /** + * 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) + { + $this->groupManager = $groupManager; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + } + + /** + * {@inheritdoc} + */ + public function createContext(ValidatorInterface $validator, $root) + { + return new ExecutionContext( + $validator, + $root, + $this->groupManager, + $this->translator, + $this->translationDomain + ); + } +} diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextFactoryInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextFactoryInterface.php new file mode 100644 index 0000000000000..f0ee00174f7c3 --- /dev/null +++ b/src/Symfony/Component/Validator/Context/ExecutionContextFactoryInterface.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * Creates instances of {@link ExecutionContextInterface}. + * + * You can use a custom factory if you want to customize the execution context + * that is passed through the validation run. + * + * @since 2.5 + * @author Bernhard Schussek + */ +interface ExecutionContextFactoryInterface +{ + /** + * Creates a new execution context. + * + * @param ValidatorInterface $validator The validator + * @param mixed $root The root value of the validated + * object graph + * + * @return ExecutionContextInterface The new execution context + */ + public function createContext(ValidatorInterface $validator, $root); +} diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php b/src/Symfony/Component/Validator/Context/ExecutionContextManager.php deleted file mode 100644 index 6214eabf46a66..0000000000000 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManager.php +++ /dev/null @@ -1,215 +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\Context; - -use Symfony\Component\Translation\TranslatorInterface; -use Symfony\Component\Validator\Exception\RuntimeException; -use Symfony\Component\Validator\Group\GroupManagerInterface; -use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\NodeVisitor\AbstractVisitor; -use Symfony\Component\Validator\Validator\ValidatorInterface; - -/** - * The default implementation of {@link ExecutionContextManagerInterface}. - * - * This class implements {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface} - * and updates the current context with the current node of the validation - * traversal. - * - * After creating a new instance, the method {@link initialize()} must be - * called with a {@link ValidatorInterface} instance. Calling methods such as - * {@link startContext()} or {@link enterNode()} without initializing the - * manager first will lead to errors. - * - * @since 2.5 - * @author Bernhard Schussek - * - * @see ExecutionContextManagerInterface - * @see \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface - */ -class ExecutionContextManager extends AbstractVisitor implements ExecutionContextManagerInterface -{ - /** - * @var GroupManagerInterface - */ - private $groupManager; - - /** - * @var ValidatorInterface - */ - private $validator; - - /** - * @var ExecutionContext - */ - private $currentContext; - - /** - * @var \SplStack|ExecutionContext[] - */ - private $contextStack; - - /** - * @var TranslatorInterface - */ - private $translator; - - /** - * @var string|null - */ - private $translationDomain; - - /** - * Creates a new context manager. - * - * @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) - { - $this->groupManager = $groupManager; - $this->translator = $translator; - $this->translationDomain = $translationDomain; - $this->contextStack = new \SplStack(); - } - - /** - * Initializes the manager with a validator. - * - * @param ValidatorInterface $validator The validator - */ - public function initialize(ValidatorInterface $validator) - { - $this->validator = $validator; - } - - /** - * {@inheritdoc} - * - * @throws RuntimeException If {@link initialize()} wasn't called - */ - public function startContext($root) - { - if (null === $this->validator) { - throw new RuntimeException( - 'initialize() must be called before startContext().' - ); - } - - $this->currentContext = $this->createContext( - $root, - $this->validator, - $this->groupManager, - $this->translator, - $this->translationDomain - ); - - $this->contextStack->push($this->currentContext); - - return $this->currentContext; - } - - /** - * {@inheritdoc} - * - * @throws RuntimeException If {@link startContext()} wasn't called - */ - public function stopContext() - { - if (0 === count($this->contextStack)) { - throw new RuntimeException( - 'No context was started yet. Call startContext() before '. - 'stopContext().' - ); - } - - // Remove the current context from the stack - $stoppedContext = $this->contextStack->pop(); - - // Adjust the current context to the previous context - $this->currentContext = count($this->contextStack) > 0 - ? $this->contextStack->top() - : null; - - return $stoppedContext; - } - - /** - * {@inheritdoc} - */ - public function getCurrentContext() - { - return $this->currentContext; - } - - /** - * {@inheritdoc} - * - * @throws RuntimeException If {@link initialize()} wasn't called - */ - public function enterNode(Node $node) - { - if (null === $this->currentContext) { - throw new RuntimeException( - 'No context was started yet. Call startContext() before '. - 'enterNode().' - ); - } - - $this->currentContext->pushNode($node); - } - - /** - * {@inheritdoc} - * - * @throws RuntimeException If {@link initialize()} wasn't called - */ - public function leaveNode(Node $node) - { - if (null === $this->currentContext) { - throw new RuntimeException( - 'No context was started yet. Call startContext() before '. - 'leaveNode().' - ); - } - - $this->currentContext->popNode(); - } - - /** - * Creates a new context. - * - * Can be overridden by subclasses. - * - * @param mixed $root The root value of the - * validated object graph - * @param ValidatorInterface $validator The validator - * @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 - * - * @return ExecutionContextInterface The created context - */ - protected function createContext($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain) - { - return new ExecutionContext($root, $validator, $groupManager, $translator, $translationDomain); - } -} diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php deleted file mode 100644 index a2dd04d7dccd5..0000000000000 --- a/src/Symfony/Component/Validator/Context/ExecutionContextManagerInterface.php +++ /dev/null @@ -1,86 +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\Context; - -/** - * Manages the creation and deletion of {@link ExecutionContextInterface} - * instances. - * - * Start a new context with {@link startContext()}. You can retrieve the context - * with {@link getCurrentContext()} and stop it again with {@link stopContext()}. - * - * $contextManager->startContext(); - * $context = $contextManager->getCurrentContext(); - * $contextManager->stopContext(); - * - * You can also start several nested contexts. The {@link getCurrentContext()} - * method will always return the most recently started context. - * - * // Start context 1 - * $contextManager->startContext(); - * - * // Start context 2 - * $contextManager->startContext(); - * - * // Returns context 2 - * $context = $contextManager->getCurrentContext(); - * - * // Stop context 2 - * $contextManager->stopContext(); - * - * // Returns context 1 - * $context = $contextManager->getCurrentContext(); - * - * See also {@link ExecutionContextInterface} for more information. - * - * @since 2.5 - * @author Bernhard Schussek - * - * @see ExecutionContextInterface - */ -interface ExecutionContextManagerInterface -{ - /** - * Starts a new context. - * - * The newly started context is returned. You can subsequently access the - * context with {@link getCurrentContext()}. - * - * @param mixed $root The root value of the object graph in the new context - * - * @return ExecutionContextInterface The started context - */ - public function startContext($root); - - /** - * Stops the current context. - * - * If multiple contexts have been started, the most recently started context - * is stopped. The stopped context is returned from this method. - * - * After calling this method, {@link getCurrentContext()} will return the - * context that was started before the stopped context. - * - * @return ExecutionContextInterface The stopped context - */ - public function stopContext(); - - /** - * Returns the current context. - * - * If multiple contexts have been started, the current context refers to the - * most recently started context. - * - * @return ExecutionContextInterface The current context - */ - public function getCurrentContext(); -} diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index 685f06931bb61..2d85ab7905a19 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -39,8 +39,11 @@ class LegacyExecutionContext extends ExecutionContext implements LegacyExecution * it does not, an {@link InvalidArgumentException} is thrown. * * @see ExecutionContext::__construct() + * + * @internal Called by {@link LegacyExecutionContextFactory}. Should not be used + * in user code. */ - public function __construct($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) + public function __construct(ValidatorInterface $validator, $root, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain = null) { if (!$validator instanceof LegacyValidatorInterface) { throw new InvalidArgumentException( @@ -49,7 +52,13 @@ public function __construct($root, ValidatorInterface $validator, GroupManagerIn ); } - parent::__construct($root, $validator, $groupManager, $translator, $translationDomain); + 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 new file mode 100644 index 0000000000000..181b4f47fd00d --- /dev/null +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Context; + +use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +/** + * Creates new {@link LegacyExecutionContext} instances. + * + * @since 2.5 + * @author Bernhard Schussek + * + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. To be + * removed in 3.0. + */ +class LegacyExecutionContextFactory implements ExecutionContextFactoryInterface +{ + /** + * @var GroupManagerInterface + */ + private $groupManager; + + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var string|null + */ + private $translationDomain; + + /** + * 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) + { + $this->groupManager = $groupManager; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + } + + /** + * {@inheritdoc} + */ + public function createContext(ValidatorInterface $validator, $root) + { + return new LegacyExecutionContext( + $validator, + $root, + $this->groupManager, + $this->translator, + $this->translationDomain + ); + } +} diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContextManager.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContextManager.php deleted file mode 100644 index 361445bbfc080..0000000000000 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContextManager.php +++ /dev/null @@ -1,36 +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\Context; - -use Symfony\Component\Translation\TranslatorInterface; -use Symfony\Component\Validator\Group\GroupManagerInterface; -use Symfony\Component\Validator\Validator\ValidatorInterface; - -/** - * A context manager that creates contexts compatible to the API < Symfony 2.5. - * - * @since 2.5 - * @author Bernhard Schussek - * - * @see ExecutionContextManagerInterface - * @see \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface - */ -class LegacyExecutionContextManager extends ExecutionContextManager -{ - /** - * {@inheritdoc} - */ - protected function createContext($root, ValidatorInterface $validator, GroupManagerInterface $groupManager, TranslatorInterface $translator, $translationDomain) - { - return new LegacyExecutionContext($root, $validator, $groupManager, $translator, $translationDomain); - } -} diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index eacfd85c2c994..9d6f4d639496f 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\NodeTraverser; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Mapping\CascadingStrategy; @@ -59,7 +60,7 @@ public function removeVisitor(NodeVisitorInterface $visitor) /** * {@inheritdoc} */ - public function traverse(array $nodes) + public function traverse(array $nodes, ExecutionContextInterface $context) { $isTopLevelCall = !$this->traversalStarted; @@ -68,39 +69,34 @@ public function traverse(array $nodes) foreach ($this->visitors as $visitor) { /** @var NodeVisitorInterface $visitor */ - $visitor->beforeTraversal($nodes); + $visitor->beforeTraversal($nodes, $context); } } foreach ($nodes as $node) { if ($node instanceof ClassNode) { - $this->traverseClassNode($node); + $this->traverseClassNode($node, $context); } else { - $this->traverseNode($node); + $this->traverseNode($node, $context); } } if ($isTopLevelCall) { foreach ($this->visitors as $visitor) { /** @var NodeVisitorInterface $visitor */ - $visitor->afterTraversal($nodes); + $visitor->afterTraversal($nodes, $context); } $this->traversalStarted = false; } } - /** - * @param Node $node - * - * @return Boolean - */ - private function enterNode(Node $node) + private function enterNode(Node $node, ExecutionContextInterface $context) { $continueTraversal = true; foreach ($this->visitors as $visitor) { - if (false === $visitor->enterNode($node)) { + if (false === $visitor->enterNode($node, $context)) { $continueTraversal = false; // Continue, so that the enterNode() method of all visitors @@ -111,19 +107,16 @@ private function enterNode(Node $node) return $continueTraversal; } - /** - * @param Node $node - */ - private function leaveNode(Node $node) + private function leaveNode(Node $node, ExecutionContextInterface $context) { foreach ($this->visitors as $visitor) { - $visitor->leaveNode($node); + $visitor->leaveNode($node, $context); } } - private function traverseNode(Node $node) + private function traverseNode(Node $node, ExecutionContextInterface $context) { - $continue = $this->enterNode($node); + $continue = $this->enterNode($node, $context); // Visitors have two possibilities to influence the traversal: // @@ -133,13 +126,13 @@ private function traverseNode(Node $node) // that group will be skipped in the subtree of that node. if (false === $continue) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } if (null === $node->value) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } @@ -151,7 +144,7 @@ private function traverseNode(Node $node) : $node->groups; if (0 === count($cascadedGroups)) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } @@ -166,11 +159,12 @@ private function traverseNode(Node $node) $this->cascadeEachObjectIn( $node->value, $node->propertyPath, - $node->cascadedGroups, - $traversalStrategy + $cascadedGroups, + $traversalStrategy, + $context ); - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } @@ -182,25 +176,26 @@ private function traverseNode(Node $node) $this->cascadeObject( $node->value, $node->propertyPath, - $node->cascadedGroups, - $traversalStrategy + $cascadedGroups, + $traversalStrategy, + $context ); - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } // Traverse only if the TRAVERSE bit is set if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } if (!$node->value instanceof \Traversable) { if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } @@ -215,16 +210,17 @@ private function traverseNode(Node $node) $this->cascadeEachObjectIn( $node->value, $node->propertyPath, - $node->groups, - $traversalStrategy + $cascadedGroups, + $traversalStrategy, + $context ); - $this->leaveNode($node); + $this->leaveNode($node, $context); } - private function traverseClassNode(ClassNode $node, $traversalStrategy = TraversalStrategy::IMPLICIT) + private function traverseClassNode(ClassNode $node, ExecutionContextInterface $context, $traversalStrategy = TraversalStrategy::IMPLICIT) { - $continue = $this->enterNode($node); + $continue = $this->enterNode($node, $context); // Visitors have two possibilities to influence the traversal: // @@ -234,28 +230,30 @@ private function traverseClassNode(ClassNode $node, $traversalStrategy = Travers // that group will be skipped in the subtree of that node. if (false === $continue) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } if (0 === count($node->groups)) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } foreach ($node->metadata->getConstrainedProperties() as $propertyName) { foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { - $this->traverseNode(new PropertyNode( - $propertyMetadata->getPropertyValue($node->value), - $propertyMetadata, - $node->propertyPath - ? $node->propertyPath.'.'.$propertyName - : $propertyName, - $node->groups, - $node->cascadedGroups - )); + $propertyNode = new PropertyNode( + $propertyMetadata->getPropertyValue($node->value), + $propertyMetadata, + $node->propertyPath + ? $node->propertyPath.'.'.$propertyName + : $propertyName, + $node->groups, + $node->cascadedGroups + ); + + $this->traverseNode($propertyNode, $context); } } @@ -267,14 +265,14 @@ private function traverseClassNode(ClassNode $node, $traversalStrategy = Travers // Traverse only if the TRAVERSE bit is set if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } if (!$node->value instanceof \Traversable) { if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { - $this->leaveNode($node); + $this->leaveNode($node, $context); return; } @@ -290,13 +288,14 @@ private function traverseClassNode(ClassNode $node, $traversalStrategy = Travers $node->value, $node->propertyPath, $node->groups, - $traversalStrategy + $traversalStrategy, + $context ); - $this->leaveNode($node); + $this->leaveNode($node, $context); } - private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy) + private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { try { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -312,7 +311,7 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal $groups ); - $this->traverseClassNode($classNode, $traversalStrategy); + $this->traverseClassNode($classNode, $context, $traversalStrategy); } catch (NoSuchMetadataException $e) { // Rethrow if the TRAVERSE bit is not set if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { @@ -329,12 +328,13 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal $object, $propertyPath, $groups, - $traversalStrategy + $traversalStrategy, + $context ); } } - private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy) + private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { if ($traversalStrategy & TraversalStrategy::RECURSIVE) { // Try to traverse nested objects, but ignore if they do not @@ -356,7 +356,8 @@ private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $value, $propertyPath.'['.$key.']', $groups, - $traversalStrategy + $traversalStrategy, + $context ); continue; @@ -369,7 +370,8 @@ private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $value, $propertyPath.'['.$key.']', $groups, - $traversalStrategy + $traversalStrategy, + $context ); } } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php index d9ce42f025a8b..6084403d9bb16 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Validator\NodeTraverser; -use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; /** @@ -24,8 +24,5 @@ public function addVisitor(NodeVisitorInterface $visitor); public function removeVisitor(NodeVisitorInterface $visitor); - /** - * @param Node[] $nodes - */ - public function traverse(array $nodes); + public function traverse(array $nodes, ExecutionContextInterface $context); } diff --git a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php index 31b49250eb8c9..47631556f0d67 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\NodeVisitor; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Node\Node; /** @@ -19,19 +20,19 @@ */ abstract class AbstractVisitor implements NodeVisitorInterface { - public function beforeTraversal(array $nodes) + public function beforeTraversal(array $nodes, ExecutionContextInterface $context) { } - public function afterTraversal(array $nodes) + public function afterTraversal(array $nodes, ExecutionContextInterface $context) { } - public function enterNode(Node $node) + public function enterNode(Node $node, ExecutionContextInterface $context) { } - public function leaveNode(Node $node) + public function leaveNode(Node $node, ExecutionContextInterface $context) { } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php new file mode 100644 index 0000000000000..e647e7c1adfe1 --- /dev/null +++ b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\NodeVisitor; + +use Symfony\Component\Validator\Context\ExecutionContext; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\RuntimeException; +use Symfony\Component\Validator\Node\Node; + +/** + * Updates the current context with the current node of the validation + * traversal. + * + * @since 2.5 + * @author Bernhard Schussek + */ +class ContextRefresher extends AbstractVisitor +{ + public function enterNode(Node $node, ExecutionContextInterface $context) + { + if (!$context instanceof ExecutionContext) { + throw new RuntimeException(sprintf( + 'The ContextRefresher only supports instances of class '. + '"Symfony\Component\Validator\Context\ExecutionContext". '. + 'An instance of class "%s" was given.', + get_class($context) + )); + } + + $context->pushNode($node); + } + + /** + * {@inheritdoc} + * + * @throws RuntimeException If {@link initialize()} wasn't called + */ + public function leaveNode(Node $node, ExecutionContextInterface $context) + { + if (!$context instanceof ExecutionContext) { + throw new RuntimeException(sprintf( + 'The ContextRefresher only supports instances of class '. + '"Symfony\Component\Validator\Context\ExecutionContext". '. + 'An instance of class "%s" was given.', + get_class($context) + )); + } + + $context->popNode(); + } +} diff --git a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php index 868d6fc428749..b5887ba4b0db0 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php +++ b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; @@ -22,7 +23,7 @@ */ class GroupSequenceResolver extends AbstractVisitor { - public function enterNode(Node $node) + public function enterNode(Node $node, ExecutionContextInterface $context) { if (!$node instanceof ClassNode) { return; diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php index 6ee8b7f8260db..e410cb259e02e 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php @@ -13,7 +13,7 @@ use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; -use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; @@ -35,11 +35,6 @@ class NodeValidator extends AbstractVisitor implements GroupManagerInterface */ private $validatorFactory; - /** - * @var ExecutionContextManagerInterface - */ - private $contextManager; - /** * @var NodeTraverserInterface */ @@ -56,19 +51,14 @@ public function __construct(NodeTraverserInterface $nodeTraverser, ConstraintVal $this->objectHashStack = new \SplStack(); } - public function initialize(ExecutionContextManagerInterface $contextManager) - { - $this->contextManager = $contextManager; - } - - public function afterTraversal(array $nodes) + public function afterTraversal(array $nodes, ExecutionContextInterface $context) { $this->validatedObjects = array(); $this->validatedConstraints = array(); $this->objectHashStack = new \SplStack(); } - public function enterNode(Node $node) + public function enterNode(Node $node, ExecutionContextInterface $context) { if ($node instanceof ClassNode) { $objectHash = spl_object_hash($node->value); @@ -105,7 +95,7 @@ public function enterNode(Node $node) // Validate normal group if (!$group instanceof GroupSequence) { - $this->validateNodeForGroup($objectHash, $node, $group); + $this->validateNodeForGroup($objectHash, $node, $group, $context); continue; } @@ -114,7 +104,7 @@ public function enterNode(Node $node) unset($node->groups[$key]); // Traverse group sequence until a violation is generated - $this->traverseGroupSequence($node, $group); + $this->traverseGroupSequence($node, $group, $context); // Optimization: If the groups only contain the group sequence, // we can skip the traversal for the properties of the object @@ -126,7 +116,7 @@ public function enterNode(Node $node) return true; } - public function leaveNode(Node $node) + public function leaveNode(Node $node, ExecutionContextInterface $context) { if ($node instanceof ClassNode) { $this->objectHashStack->pop(); @@ -138,9 +128,8 @@ public function getCurrentGroup() return $this->currentGroup; } - private function traverseGroupSequence(Node $node, GroupSequence $groupSequence) + private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, ExecutionContextInterface $context) { - $context = $this->contextManager->getCurrentContext(); $violationCount = count($context->getViolations()); foreach ($groupSequence->groups as $groupInSequence) { @@ -151,7 +140,7 @@ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence) $node->cascadedGroups = array($groupSequence->cascadedGroup); } - $this->nodeTraverser->traverse(array($node)); + $this->nodeTraverser->traverse(array($node), $context); // Abort sequence validation if a violation was generated if (count($context->getViolations()) > $violationCount) { @@ -160,14 +149,7 @@ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence) } } - /** - * @param $objectHash - * @param Node $node - * @param $group - * - * @throws \Exception - */ - private function validateNodeForGroup($objectHash, Node $node, $group) + private function validateNodeForGroup($objectHash, Node $node, $group, ExecutionContextInterface $context) { try { $this->currentGroup = $group; @@ -187,7 +169,7 @@ private function validateNodeForGroup($objectHash, Node $node, $group) } $validator = $this->validatorFactory->getInstance($constraint); - $validator->initialize($this->contextManager->getCurrentContext()); + $validator->initialize($context); $validator->validate($node->value, $constraint); } diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php index a7542d515a73f..6736cc23ba966 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\NodeVisitor; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Node\Node; /** @@ -19,11 +20,11 @@ */ interface NodeVisitorInterface { - public function beforeTraversal(array $nodes); + public function beforeTraversal(array $nodes, ExecutionContextInterface $context); - public function afterTraversal(array $nodes); + public function afterTraversal(array $nodes, ExecutionContextInterface $context); - public function enterNode(Node $node); + public function enterNode(Node $node, ExecutionContextInterface $context); - public function leaveNode(Node $node); + public function leaveNode(Node $node, ExecutionContextInterface $context); } diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php index bd000366c518e..8d44d39ee0be7 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\NodeVisitor; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\ObjectInitializerInterface; @@ -42,7 +43,7 @@ public function __construct(array $initializers) $this->initializers = $initializers; } - public function enterNode(Node $node) + public function enterNode(Node $node, ExecutionContextInterface $context) { if (!$node instanceof ClassNode) { return; diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextManagerTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextManagerTest.php deleted file mode 100644 index 548a11170d57d..0000000000000 --- a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextManagerTest.php +++ /dev/null @@ -1,95 +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\ExecutionContextManager; - -/** - * @since 2.5 - * @author Bernhard Schussek - */ -class ExecutionContextManagerTest extends \PHPUnit_Framework_TestCase -{ - 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 ExecutionContextManager - */ - private $contextManager; - - protected function setUp() - { - $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->contextManager = new ExecutionContextManager( - $this->groupManager, - $this->translator, - self::TRANSLATION_DOMAIN - ); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\RuntimeException - */ - public function testInitializeMustBeCalledBeforeStartContext() - { - $this->contextManager->startContext('root'); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\RuntimeException - */ - public function testCannotStopContextIfNoneWasStarted() - { - $this->contextManager->stopContext(); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\RuntimeException - */ - public function testCannotEnterNodeWithoutActiveContext() - { - $node = $this->getMockBuilder('Symfony\Component\Validator\Node\Node') - ->disableOriginalConstructor() - ->getMock(); - - $this->contextManager->enterNode($node); - } - - /** - * @expectedException \Symfony\Component\Validator\Exception\RuntimeException - */ - public function testCannotLeaveNodeWithoutActiveContext() - { - $node = $this->getMockBuilder('Symfony\Component\Validator\Node\Node') - ->disableOriginalConstructor() - ->getMock(); - - $this->contextManager->leaveNode($node); - } -} diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php index 1133f413a7cd6..8019aecea1ae4 100644 --- a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php +++ b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php @@ -51,11 +51,7 @@ protected function setUp() $this->translator = $this->getMock('Symfony\Component\Translation\TranslatorInterface'); $this->context = new ExecutionContext( - self::ROOT, - $this->validator, - $this->groupManager, - $this->translator, - self::TRANSLATION_DOMAIN + $this->validator, self::ROOT, $this->groupManager, $this->translator, self::TRANSLATION_DOMAIN ); } diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 48d8f07141d07..ad529f3750e92 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -87,7 +87,7 @@ public function testNoDuplicateValidationIfConstraintInMultipleGroups() 'groups' => array('Group 1', 'Group 2'), ))); - $violations = $this->validateObject($entity, array('Group 1', 'Group 2')); + $violations = $this->validator->validateObject($entity, array('Group 1', 'Group 2')); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -118,7 +118,7 @@ public function testGroupSequenceAbortsAfterFailedGroup() ))); $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3')); - $violations = $this->validateObject($entity, $sequence); + $violations = $this->validator->validateObject($entity, $sequence); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -148,7 +148,7 @@ public function testGroupSequenceIncludesReferences() ))); $sequence = new GroupSequence(array('Group 1', 'Entity')); - $violations = $this->validateObject($entity, $sequence); + $violations = $this->validator->validateObject($entity, $sequence); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -207,7 +207,7 @@ public function testValidateInSeparateContext() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validator->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -251,7 +251,7 @@ public function testValidateInContext() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validator->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -302,7 +302,7 @@ public function testValidateArrayInContext() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validator->validateObject($entity, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -361,7 +361,7 @@ public function testExpectTraversableIfTraverse() { $entity = new Entity(); - $this->validate($entity, new Traverse()); + $this->validator->validate($entity, new Traverse()); } /** @@ -373,7 +373,7 @@ public function testExpectTraversableIfTraverseOnClass() $this->metadata->addConstraint(new Traverse()); - $this->validateObject($entity); + $this->validator->validateObject($entity); } public function testAddCustomizedViolation() @@ -391,7 +391,7 @@ public function testAddCustomizedViolation() $this->metadata->addConstraint(new Callback($callback)); - $violations = $this->validateObject($entity); + $violations = $this->validator->validateObject($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index e3715731ce081..1cecafdedf9e0 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -13,8 +13,9 @@ use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; -use Symfony\Component\Validator\Context\LegacyExecutionContextManager; +use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeVisitor\ContextRefresher; use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; use Symfony\Component\Validator\NodeVisitor\NodeValidator; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; @@ -26,20 +27,13 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); - $contextManager = new LegacyExecutionContextManager($nodeValidator, new DefaultTranslator()); - $validator = new LegacyValidator($nodeTraverser, $metadataFactory, $contextManager); + $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); + $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); $groupSequenceResolver = new GroupSequenceResolver(); - - // The context manager needs the validator for passing it to created - // contexts - $contextManager->initialize($validator); - - // The node validator needs the context manager for passing the current - // context to the constraint validators - $nodeValidator->initialize($contextManager); + $contextRefresher = new ContextRefresher(); $nodeTraverser->addVisitor($groupSequenceResolver); - $nodeTraverser->addVisitor($contextManager); + $nodeTraverser->addVisitor($contextRefresher); $nodeTraverser->addVisitor($nodeValidator); return $validator; diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index c106d765c12c3..94900e4497f52 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -13,8 +13,9 @@ use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; -use Symfony\Component\Validator\Context\LegacyExecutionContextManager; +use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeVisitor\ContextRefresher; use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; use Symfony\Component\Validator\NodeVisitor\NodeValidator; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; @@ -26,20 +27,13 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); - $contextManager = new LegacyExecutionContextManager($nodeValidator, new DefaultTranslator()); - $validator = new LegacyValidator($nodeTraverser, $metadataFactory, $contextManager); + $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); + $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); $groupSequenceResolver = new GroupSequenceResolver(); - - // The context manager needs the validator for passing it to created - // contexts - $contextManager->initialize($validator); - - // The node validator needs the context manager for passing the current - // context to the constraint validators - $nodeValidator->initialize($contextManager); + $contextRefresher = new ContextRefresher(); $nodeTraverser->addVisitor($groupSequenceResolver); - $nodeTraverser->addVisitor($contextManager); + $nodeTraverser->addVisitor($contextRefresher); $nodeTraverser->addVisitor($nodeValidator); return $validator; diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php index 2bf4693dde361..4d58f0a13e4c8 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php @@ -13,8 +13,9 @@ use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; -use Symfony\Component\Validator\Context\ExecutionContextManager; +use Symfony\Component\Validator\Context\ExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeVisitor\ContextRefresher; use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; use Symfony\Component\Validator\NodeVisitor\NodeValidator; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; @@ -26,20 +27,13 @@ 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); + $contextFactory = new ExecutionContextFactory($nodeValidator, new DefaultTranslator()); + $validator = new Validator($contextFactory, $nodeTraverser, $metadataFactory); $groupSequenceResolver = new GroupSequenceResolver(); - - // The context manager needs the validator for passing it to created - // contexts - $contextManager->initialize($validator); - - // The node validator needs the context manager for passing the current - // context to the constraint validators - $nodeValidator->initialize($contextManager); + $contextRefresher = new ContextRefresher(); $nodeTraverser->addVisitor($groupSequenceResolver); - $nodeTraverser->addVisitor($contextManager); + $nodeTraverser->addVisitor($contextRefresher); $nodeTraverser->addVisitor($nodeValidator); return $validator; diff --git a/src/Symfony/Component/Validator/Validator/AbstractValidator.php b/src/Symfony/Component/Validator/Validator/AbstractValidator.php deleted file mode 100644 index 8cd2767bd03eb..0000000000000 --- a/src/Symfony/Component/Validator/Validator/AbstractValidator.php +++ /dev/null @@ -1,188 +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\Validator; - -use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Exception\ValidatorException; -use Symfony\Component\Validator\Mapping\ClassMetadataInterface; -use Symfony\Component\Validator\Mapping\GenericMetadata; -use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\PropertyNode; -use Symfony\Component\Validator\Node\GenericNode; -use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; -use Symfony\Component\Validator\Util\PropertyPath; - -/** - * @since %%NextVersion%% - * @author Bernhard Schussek - */ -abstract class AbstractValidator implements ValidatorInterface -{ - /** - * @var NodeTraverserInterface - */ - protected $nodeTraverser; - - /** - * @var MetadataFactoryInterface - */ - protected $metadataFactory; - - /** - * @var string - */ - protected $defaultPropertyPath = ''; - - protected $defaultGroups = array(Constraint::DEFAULT_GROUP); - - public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory) - { - $this->nodeTraverser = $nodeTraverser; - $this->metadataFactory = $metadataFactory; - } - - /** - * @param ExecutionContextInterface $context - * - * @return ContextualValidatorInterface - */ - public function inContext(ExecutionContextInterface $context) - { - return new ContextualValidator($this->nodeTraverser, $this->metadataFactory, $context); - } - - public function getMetadataFor($object) - { - return $this->metadataFactory->getMetadataFor($object); - } - - public function hasMetadataFor($object) - { - return $this->metadataFactory->hasMetadataFor($object); - } - - protected function traverse($value, $constraints, $groups = null) - { - if (!is_array($constraints)) { - $constraints = array($constraints); - } - - $metadata = new GenericMetadata(); - $metadata->addConstraints($constraints); - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - - $this->nodeTraverser->traverse(array(new GenericNode( - $value, - $metadata, - $this->defaultPropertyPath, - $groups, - $groups - ))); - } - - protected function traverseObject($object, $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) - )); - } - - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - - $this->nodeTraverser->traverse(array(new ClassNode( - $object, - $classMetadata, - $this->defaultPropertyPath, - $groups, - $groups - ))); - } - - protected function traverseProperty($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; - $nodes = array(); - - foreach ($propertyMetadatas as $propertyMetadata) { - $propertyValue = $propertyMetadata->getPropertyValue($object); - - $nodes[] = new PropertyNode( - $propertyValue, - $propertyMetadata, - PropertyPath::append($this->defaultPropertyPath, $propertyName), - $groups, - $groups - ); - } - - $this->nodeTraverser->traverse($nodes); - } - - protected function traversePropertyValue($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; - $nodes = array(); - - foreach ($propertyMetadatas as $propertyMetadata) { - $nodes[] = new PropertyNode( - $value, - $propertyMetadata, - PropertyPath::append($this->defaultPropertyPath, $propertyName), - $groups, - $groups - ); - } - - $this->nodeTraverser->traverse($nodes); - } - - protected function normalizeGroups($groups) - { - if (is_array($groups)) { - return $groups; - } - - return array($groups); - } -} diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index b975ef4bde1d3..cb9951bf771da 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -13,30 +13,45 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Traverse; -use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; +use Symfony\Component\Validator\Exception\ValidatorException; +use Symfony\Component\Validator\Mapping\ClassMetadataInterface; +use Symfony\Component\Validator\Mapping\GenericMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\GenericNode; +use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; +use Symfony\Component\Validator\Util\PropertyPath; /** * @since %%NextVersion%% * @author Bernhard Schussek */ -class ContextualValidator extends AbstractValidator implements ContextualValidatorInterface +class ContextualValidator implements ContextualValidatorInterface { /** - * @var ExecutionContextManagerInterface + * @var ExecutionContextInterface */ private $context; - public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory, ExecutionContextInterface $context) - { - parent::__construct($nodeTraverser, $metadataFactory); + /** + * @var NodeTraverserInterface + */ + private $nodeTraverser; + + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + public function __construct(ExecutionContextInterface $context, NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory) + { $this->context = $context; $this->defaultPropertyPath = $context->getPropertyPath(); - $this->defaultGroups = array($context->getGroup()); + $this->defaultGroups = array($context->getGroup() ?: Constraint::DEFAULT_GROUP); + $this->nodeTraverser = $nodeTraverser; + $this->metadataFactory = $metadataFactory; } public function atPath($subPath) @@ -46,40 +61,53 @@ public function atPath($subPath) return $this; } - /** - * Validates a value against a constraint or a list of constraints. - * - * @param mixed $value The value to validate. - * @param Constraint|Constraint[] $constraints The constraint(s) to validate against. - * @param array|null $groups The validation groups to validate. - * - * @return ConstraintViolationListInterface A list of constraint violations. If the - * list is empty, validation succeeded. - */ public function validate($value, $constraints, $groups = null) { - $this->traverse($value, $constraints, $groups); + if (!is_array($constraints)) { + $constraints = array($constraints); + } - return $this->context->getViolations(); + $metadata = new GenericMetadata(); + $metadata->addConstraints($constraints); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + $node = new GenericNode( + $value, + $metadata, + $this->defaultPropertyPath, + $groups + ); + + $this->nodeTraverser->traverse(array($node), $this->context); + + return $this; } - /** - * Validates a value. - * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. - * - * @param mixed $object The value to validate - * @param array|null $groups The validation groups to validate. - * - * @return ConstraintViolationListInterface A list of constraint violations. If the - * list is empty, validation succeeded. - */ public function validateObject($object, $groups = null) { - $this->traverseObject($object, $groups); + $classMetadata = $this->metadataFactory->getMetadataFor($object); - return $this->context->getViolations(); + 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) + )); + } + + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + $node = new ClassNode( + $object, + $classMetadata, + $this->defaultPropertyPath, + $groups + ); + + $this->nodeTraverser->traverse(array($node), $this->context); + + return $this; } public function validateCollection($collection, $groups = null, $deep = false) @@ -89,50 +117,85 @@ public function validateCollection($collection, $groups = null, $deep = false) 'deep' => $deep, )); - $this->traverse($collection, $constraint, $groups); - - return $this->context->getViolations(); + return $this->validate($collection, $constraint, $groups); } - /** - * Validates a property of a value against its current value. - * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. - * - * @param mixed $object The value containing the property. - * @param string $propertyName The name of the property to validate. - * @param array|null $groups The validation groups to validate. - * - * @return ConstraintViolationListInterface A list of constraint violations. If the - * list is empty, validation succeeded. - */ public function validateProperty($object, $propertyName, $groups = null) { - $this->traverseProperty($object, $propertyName, $groups); + $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; + $nodes = array(); + + foreach ($propertyMetadatas as $propertyMetadata) { + $propertyValue = $propertyMetadata->getPropertyValue($object); + + $nodes[] = new PropertyNode( + $propertyValue, + $propertyMetadata, + PropertyPath::append($this->defaultPropertyPath, $propertyName), + $groups + ); + } + + $this->nodeTraverser->traverse($nodes, $this->context); - return $this->context->getViolations(); + return $this; } - /** - * Validate a property of a value against a potential value. - * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. - * - * @param string $object The value containing the property. - * @param string $propertyName The name of the property to validate - * @param string $value The value to validate against the - * constraints of the property. - * @param array|null $groups The validation groups to validate. - * - * @return ConstraintViolationListInterface A list of constraint violations. If the - * list is empty, validation succeeded. - */ public function validatePropertyValue($object, $propertyName, $value, $groups = null) { - $this->traversePropertyValue($object, $propertyName, $value, $groups); + $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; + $nodes = array(); + + foreach ($propertyMetadatas as $propertyMetadata) { + $nodes[] = new PropertyNode( + $value, + $propertyMetadata, + PropertyPath::append($this->defaultPropertyPath, $propertyName), + $groups, + $groups + ); + } + + $this->nodeTraverser->traverse($nodes, $this->context); + return $this; + } + + protected function normalizeGroups($groups) + { + if (is_array($groups)) { + return $groups; + } + + return array($groups); + } + + public function getViolations() + { return $this->context->getViolations(); } } diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php index 61de8900f1431..69a224cb199b3 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php @@ -11,16 +11,80 @@ namespace Symfony\Component\Validator\Validator; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintViolationListInterface; + /** * @since %%NextVersion%% * @author Bernhard Schussek */ -interface ContextualValidatorInterface extends ValidatorInterface +interface ContextualValidatorInterface { /** - * @param $subPath + * @param string $subPath * - * @return ContextualValidatorInterface + * @return ContextualValidatorInterface This validator */ public function atPath($subPath); + + /** + * Validates a value against a constraint or a list of constraints. + * + * @param mixed $value The value to validate. + * @param Constraint|Constraint[] $constraints The constraint(s) to validate against. + * @param array|null $groups The validation groups to validate. + * + * @return ContextualValidatorInterface This validator + */ + public function validate($value, $constraints, $groups = null); + + /** + * Validates a value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param mixed $object The value to validate + * @param array|null $groups The validation groups to validate. + * + * @return ContextualValidatorInterface This validator + */ + public function validateObject($object, $groups = null); + + public function validateCollection($collection, $groups = null, $deep = false); + + /** + * Validates a property of a value against its current value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param mixed $object The value containing the property. + * @param string $propertyName The name of the property to validate. + * @param array|null $groups The validation groups to validate. + * + * @return ContextualValidatorInterface This validator + */ + public function validateProperty($object, $propertyName, $groups = null); + + /** + * Validate a property of a value against a potential value. + * + * The accepted values depend on the {@link MetadataFactoryInterface} + * implementation. + * + * @param string $object The value containing the property. + * @param string $propertyName The name of the property to validate + * @param string $value The value to validate against the + * constraints of the property. + * @param array|null $groups The validation groups to validate. + * + * @return ContextualValidatorInterface This validator + */ + public function validatePropertyValue($object, $propertyName, $value, $groups = null); + + /** + * @return ConstraintViolationListInterface + */ + public function getViolations(); } diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index 94857a3a1b555..820b01a149887 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Validator\Validator; -use Symfony\Component\Validator\Constraints\Traverse; -use Symfony\Component\Validator\Context\ExecutionContextManagerInterface; +use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; use Symfony\Component\Validator\MetadataFactoryInterface; @@ -20,67 +20,102 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class Validator extends AbstractValidator +class Validator implements ValidatorInterface { /** - * @var ExecutionContextManagerInterface + * @var ExecutionContextFactoryInterface */ - protected $contextManager; + protected $contextFactory; - public function __construct(NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory, ExecutionContextManagerInterface $contextManager) - { - parent::__construct($nodeTraverser, $metadataFactory); + /** + * @var NodeTraverserInterface + */ + protected $nodeTraverser; - $this->contextManager = $contextManager; + /** + * @var MetadataFactoryInterface + */ + protected $metadataFactory; + + public function __construct(ExecutionContextFactoryInterface $contextFactory, NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory) + { + $this->contextFactory = $contextFactory; + $this->nodeTraverser = $nodeTraverser; + $this->metadataFactory = $metadataFactory; } - public function validate($value, $constraints, $groups = null) + /** + * {@inheritdoc} + */ + public function startContext($root = null) { - $this->contextManager->startContext($value); + return new ContextualValidator( + $this->contextFactory->createContext($this, $root), + $this->nodeTraverser, + $this->metadataFactory + ); + } - $this->traverse($value, $constraints, $groups); + /** + * {@inheritdoc} + */ + public function inContext(ExecutionContextInterface $context) + { + return new ContextualValidator( + $context, + $this->nodeTraverser, + $this->metadataFactory + ); + } - return $this->contextManager->stopContext()->getViolations(); + /** + * {@inheritdoc} + */ + public function getMetadataFor($object) + { + return $this->metadataFactory->getMetadataFor($object); } - public function validateObject($object, $groups = null) + /** + * {@inheritdoc} + */ + public function hasMetadataFor($object) { - $this->contextManager->startContext($object); + return $this->metadataFactory->hasMetadataFor($object); + } - $this->traverseObject($object, $groups); + public function validate($value, $constraints, $groups = null) + { + return $this->startContext($value) + ->validate($value, $constraints, $groups) + ->getViolations(); + } - return $this->contextManager->stopContext()->getViolations(); + public function validateObject($object, $groups = null) + { + return $this->startContext($object) + ->validateObject($object, $groups) + ->getViolations(); } public function validateCollection($collection, $groups = null, $deep = false) { - $this->contextManager->startContext($collection); - - $constraint = new Traverse(array( - 'traverse' => true, - 'deep' => $deep, - )); - - $this->traverse($collection, $constraint, $groups); - - return $this->contextManager->stopContext()->getViolations(); + return $this->startContext($collection) + ->validateCollection($collection, $groups, $deep) + ->getViolations(); } public function validateProperty($object, $propertyName, $groups = null) { - $this->contextManager->startContext($object); - - $this->traverseProperty($object, $propertyName, $groups); - - return $this->contextManager->stopContext()->getViolations(); + return $this->startContext($object) + ->validateProperty($object, $propertyName, $groups) + ->getViolations(); } public function validatePropertyValue($object, $propertyName, $value, $groups = null) { - $this->contextManager->startContext($object); - - $this->traversePropertyValue($object, $propertyName, $value, $groups); - - return $this->contextManager->stopContext()->getViolations(); + return $this->startContext($object) + ->validatePropertyValue($object, $propertyName, $value, $groups) + ->getViolations(); } } diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index 8cecaf3dc4b49..f69395284affd 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -81,6 +81,11 @@ public function validateProperty($object, $propertyName, $groups = null); */ public function validatePropertyValue($object, $propertyName, $value, $groups = null); + /** + * @return ContextualValidatorInterface + */ + public function startContext(); + /** * @param ExecutionContextInterface $context * From e440690bb496c5d32bb892ef23714d6d9e407e3e Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 20:20:50 +0100 Subject: [PATCH 31/86] [Validator] Renamed validateCollection() to validateObjects() --- .../Tests/Validator/Abstract2Dot5ApiTest.php | 6 +++--- .../Tests/Validator/AbstractLegacyApiTest.php | 4 ++-- .../Tests/Validator/AbstractValidatorTest.php | 12 ++++++------ .../Validator/Validator/ContextualValidator.php | 4 ++-- .../Validator/ContextualValidatorInterface.php | 2 +- .../Component/Validator/Validator/Validator.php | 6 +++--- .../Validator/Validator/ValidatorInterface.php | 2 +- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index ad529f3750e92..97c982d116e2b 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -59,9 +59,9 @@ protected function validateObject($object, $groups = null) return $this->validator->validateObject($object, $groups); } - protected function validateCollection($collection, $groups = null, $deep = false) + protected function validateObjects($objects, $groups = null, $deep = false) { - return $this->validator->validateCollection($collection, $groups, $deep); + return $this->validator->validateObjects($objects, $groups, $deep); } protected function validateProperty($object, $propertyName, $groups = null) @@ -276,7 +276,7 @@ public function testValidateArrayInContext() ->getValidator() ->inContext($context) ->atPath('subpath') - ->validateCollection(array('key' => $value->reference)) + ->validateObjects(array('key' => $value->reference)) ; }; diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php index 4d76fa38c18aa..956537d22f90a 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php @@ -57,9 +57,9 @@ protected function validateObject($object, $groups = null) return $this->validator->validate($object, $groups); } - protected function validateCollection($collection, $groups = null, $deep = false) + protected function validateObjects($objects, $groups = null, $deep = false) { - return $this->validator->validate($collection, $groups, true, $deep); + return $this->validator->validate($objects, $groups, true, $deep); } protected function validateProperty($object, $propertyName, $groups = null) diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index 99d250fb5a587..f4751e47c42a6 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -67,7 +67,7 @@ abstract protected function validate($value, $constraints, $groups = null); abstract protected function validateObject($object, $groups = null); - abstract protected function validateCollection($collection, $groups = null, $deep = false); + abstract protected function validateObjects($objects, $groups = null, $deep = false); abstract protected function validateProperty($object, $propertyName, $groups = null); @@ -249,7 +249,7 @@ public function testArray() 'groups' => 'Group', ))); - $violations = $this->validateCollection($array, 'Group'); + $violations = $this->validateObjects($array, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -287,7 +287,7 @@ public function testRecursiveArray() 'groups' => 'Group', ))); - $violations = $this->validateCollection($array, 'Group'); + $violations = $this->validateObjects($array, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -325,7 +325,7 @@ public function testTraversable() 'groups' => 'Group', ))); - $violations = $this->validateCollection($traversable, 'Group'); + $violations = $this->validateObjects($traversable, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -359,7 +359,7 @@ public function testRecursiveTraversableRecursiveTraversalDisabled() 'groups' => 'Group', ))); - $this->validateCollection($traversable, 'Group'); + $this->validateObjects($traversable, 'Group'); } public function testRecursiveTraversableRecursiveTraversalEnabled() @@ -388,7 +388,7 @@ public function testRecursiveTraversableRecursiveTraversalEnabled() 'groups' => 'Group', ))); - $violations = $this->validateCollection($traversable, 'Group', true); + $violations = $this->validateObjects($traversable, 'Group', true); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index cb9951bf771da..588beaa42361e 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -110,14 +110,14 @@ public function validateObject($object, $groups = null) return $this; } - public function validateCollection($collection, $groups = null, $deep = false) + public function validateObjects($objects, $groups = null, $deep = false) { $constraint = new Traverse(array( 'traverse' => true, 'deep' => $deep, )); - return $this->validate($collection, $constraint, $groups); + return $this->validate($objects, $constraint, $groups); } public function validateProperty($object, $propertyName, $groups = null) diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php index 69a224cb199b3..4e1a9a680b928 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php @@ -51,7 +51,7 @@ public function validate($value, $constraints, $groups = null); */ public function validateObject($object, $groups = null); - public function validateCollection($collection, $groups = null, $deep = false); + public function validateObjects($objects, $groups = null, $deep = false); /** * Validates a property of a value against its current value. diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index 820b01a149887..fe7b80c817cda 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -98,10 +98,10 @@ public function validateObject($object, $groups = null) ->getViolations(); } - public function validateCollection($collection, $groups = null, $deep = false) + public function validateObjects($objects, $groups = null, $deep = false) { - return $this->startContext($collection) - ->validateCollection($collection, $groups, $deep) + return $this->startContext($objects) + ->validateObjects($objects, $groups, $deep) ->getViolations(); } diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index f69395284affd..5eccf3f739491 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -47,7 +47,7 @@ public function validate($value, $constraints, $groups = null); */ public function validateObject($object, $groups = null); - public function validateCollection($collection, $groups = null, $deep = false); + public function validateObjects($objects, $groups = null, $deep = false); /** * Validates a property of a value against its current value. From e057b19964f831da36c5aaadc9b92b0855bc1441 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 20:35:17 +0100 Subject: [PATCH 32/86] [Validator] Decoupled ContextRefresher from ExecutionContext --- .../Validator/Context/ExecutionContext.php | 3 ++- .../NodeVisitor/ContextRefresher.php | 6 ++--- .../Validator/Util/NodeStackInterface.php | 25 +++++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Component/Validator/Util/NodeStackInterface.php diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 187de382fb84e..b4694492e636e 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -20,6 +20,7 @@ use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\Util\NodeStackInterface; use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; @@ -32,7 +33,7 @@ * * @see ExecutionContextInterface */ -class ExecutionContext implements ExecutionContextInterface, LegacyExecutionContextInterface +class ExecutionContext implements ExecutionContextInterface, LegacyExecutionContextInterface, NodeStackInterface { /** * @var ValidatorInterface diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php index e647e7c1adfe1..56e8549942018 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Validator\NodeVisitor; -use Symfony\Component\Validator\Context\ExecutionContext; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Exception\RuntimeException; use Symfony\Component\Validator\Node\Node; +use Symfony\Component\Validator\Util\NodeStackInterface; /** * Updates the current context with the current node of the validation @@ -27,7 +27,7 @@ class ContextRefresher extends AbstractVisitor { public function enterNode(Node $node, ExecutionContextInterface $context) { - if (!$context instanceof ExecutionContext) { + if (!$context instanceof NodeStackInterface) { throw new RuntimeException(sprintf( 'The ContextRefresher only supports instances of class '. '"Symfony\Component\Validator\Context\ExecutionContext". '. @@ -46,7 +46,7 @@ public function enterNode(Node $node, ExecutionContextInterface $context) */ public function leaveNode(Node $node, ExecutionContextInterface $context) { - if (!$context instanceof ExecutionContext) { + if (!$context instanceof NodeStackInterface) { throw new RuntimeException(sprintf( 'The ContextRefresher only supports instances of class '. '"Symfony\Component\Validator\Context\ExecutionContext". '. diff --git a/src/Symfony/Component/Validator/Util/NodeStackInterface.php b/src/Symfony/Component/Validator/Util/NodeStackInterface.php new file mode 100644 index 0000000000000..f330fa747e89f --- /dev/null +++ b/src/Symfony/Component/Validator/Util/NodeStackInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Util; + +use Symfony\Component\Validator\Node\Node; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +interface NodeStackInterface +{ + public function pushNode(Node $node); + + public function popNode(); +} From 230f2a72fab5d0ad92eafc846bdb504827379020 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Feb 2014 20:36:09 +0100 Subject: [PATCH 33/86] [Validator] Fixed exception message --- .../Component/Validator/NodeVisitor/ContextRefresher.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php index 56e8549942018..3166d38285bc0 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php @@ -30,7 +30,7 @@ public function enterNode(Node $node, ExecutionContextInterface $context) if (!$context instanceof NodeStackInterface) { throw new RuntimeException(sprintf( 'The ContextRefresher only supports instances of class '. - '"Symfony\Component\Validator\Context\ExecutionContext". '. + '"Symfony\Component\Validator\Context\NodeStackInterface". '. 'An instance of class "%s" was given.', get_class($context) )); @@ -39,17 +39,12 @@ public function enterNode(Node $node, ExecutionContextInterface $context) $context->pushNode($node); } - /** - * {@inheritdoc} - * - * @throws RuntimeException If {@link initialize()} wasn't called - */ public function leaveNode(Node $node, ExecutionContextInterface $context) { if (!$context instanceof NodeStackInterface) { throw new RuntimeException(sprintf( 'The ContextRefresher only supports instances of class '. - '"Symfony\Component\Validator\Context\ExecutionContext". '. + '"Symfony\Component\Validator\Context\NodeStackInterface". '. 'An instance of class "%s" was given.', get_class($context) )); From cf1281feefd59560c77bbb17cb980f20c4bb8161 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 11:36:21 +0100 Subject: [PATCH 34/86] [Validator] Added "Visitor" suffix to all node visitors --- .../Validator/NodeTraverser/NodeTraverser.php | 2 +- ...textRefresher.php => ContextRefresherVisitor.php} | 6 +++--- ...Resolver.php => GroupSequenceResolverVisitor.php} | 2 +- .../{NodeValidator.php => NodeValidatorVisitor.php} | 2 +- ...tInitializer.php => ObjectInitializerVisitor.php} | 2 +- .../Tests/Validator/LegacyValidator2Dot5ApiTest.php | 12 ++++++------ .../Tests/Validator/LegacyValidatorLegacyApiTest.php | 12 ++++++------ .../Tests/Validator/Validator2Dot5ApiTest.php | 12 ++++++------ 8 files changed, 25 insertions(+), 25 deletions(-) rename src/Symfony/Component/Validator/NodeVisitor/{ContextRefresher.php => ContextRefresherVisitor.php} (87%) rename src/Symfony/Component/Validator/NodeVisitor/{GroupSequenceResolver.php => GroupSequenceResolverVisitor.php} (96%) rename src/Symfony/Component/Validator/NodeVisitor/{NodeValidator.php => NodeValidatorVisitor.php} (98%) rename src/Symfony/Component/Validator/NodeVisitor/{ObjectInitializer.php => ObjectInitializerVisitor.php} (96%) diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index 9d6f4d639496f..fa4433564d801 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -137,7 +137,7 @@ private function traverseNode(Node $node, ExecutionContextInterface $context) return; } - // The "cascadedGroups" property is set by the NodeValidator when + // The "cascadedGroups" property is set by the NodeValidatorVisitor when // traversing group sequences $cascadedGroups = null !== $node->cascadedGroups ? $node->cascadedGroups diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php similarity index 87% rename from src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php rename to src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php index 3166d38285bc0..3b940043a6848 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresher.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php @@ -23,13 +23,13 @@ * @since 2.5 * @author Bernhard Schussek */ -class ContextRefresher extends AbstractVisitor +class ContextRefresherVisitor extends AbstractVisitor { public function enterNode(Node $node, ExecutionContextInterface $context) { if (!$context instanceof NodeStackInterface) { throw new RuntimeException(sprintf( - 'The ContextRefresher only supports instances of class '. + 'The ContextRefresherVisitor only supports instances of class '. '"Symfony\Component\Validator\Context\NodeStackInterface". '. 'An instance of class "%s" was given.', get_class($context) @@ -43,7 +43,7 @@ public function leaveNode(Node $node, ExecutionContextInterface $context) { if (!$context instanceof NodeStackInterface) { throw new RuntimeException(sprintf( - 'The ContextRefresher only supports instances of class '. + 'The ContextRefresherVisitor only supports instances of class '. '"Symfony\Component\Validator\Context\NodeStackInterface". '. 'An instance of class "%s" was given.', get_class($context) diff --git a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php similarity index 96% rename from src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php rename to src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php index b5887ba4b0db0..c03f7a64c3801 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolver.php +++ b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php @@ -21,7 +21,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class GroupSequenceResolver extends AbstractVisitor +class GroupSequenceResolverVisitor extends AbstractVisitor { public function enterNode(Node $node, ExecutionContextInterface $context) { diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php similarity index 98% rename from src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php rename to src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php index e410cb259e02e..6261791070d3f 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidator.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php @@ -24,7 +24,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class NodeValidator extends AbstractVisitor implements GroupManagerInterface +class NodeValidatorVisitor extends AbstractVisitor implements GroupManagerInterface { private $validatedObjects = array(); diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php similarity index 96% rename from src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php rename to src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php index 8d44d39ee0be7..1bef7f3189216 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializer.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php @@ -20,7 +20,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class ObjectInitializer extends AbstractVisitor +class ObjectInitializerVisitor extends AbstractVisitor { /** * @var ObjectInitializerInterface[] diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index 1cecafdedf9e0..600a297947f02 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -15,9 +15,9 @@ use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\ContextRefresher; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; -use Symfony\Component\Validator\NodeVisitor\NodeValidator; +use Symfony\Component\Validator\NodeVisitor\ContextRefresherVisitor; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolverVisitor; +use Symfony\Component\Validator\NodeVisitor\NodeValidatorVisitor; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; @@ -26,11 +26,11 @@ class LegacyValidator2Dot5ApiTest extends Abstract2Dot5ApiTest protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); - $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); + $nodeValidator = new NodeValidatorVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolver(); - $contextRefresher = new ContextRefresher(); + $groupSequenceResolver = new GroupSequenceResolverVisitor(); + $contextRefresher = new ContextRefresherVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); $nodeTraverser->addVisitor($contextRefresher); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index 94900e4497f52..fc2fe0bb38203 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -15,9 +15,9 @@ use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\ContextRefresher; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; -use Symfony\Component\Validator\NodeVisitor\NodeValidator; +use Symfony\Component\Validator\NodeVisitor\ContextRefresherVisitor; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolverVisitor; +use Symfony\Component\Validator\NodeVisitor\NodeValidatorVisitor; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; @@ -26,11 +26,11 @@ class LegacyValidatorLegacyApiTest extends AbstractLegacyApiTest protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); - $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); + $nodeValidator = new NodeValidatorVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolver(); - $contextRefresher = new ContextRefresher(); + $groupSequenceResolver = new GroupSequenceResolverVisitor(); + $contextRefresher = new ContextRefresherVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); $nodeTraverser->addVisitor($contextRefresher); diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php index 4d58f0a13e4c8..e3f92b879930a 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php @@ -15,9 +15,9 @@ use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\ExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\ContextRefresher; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolver; -use Symfony\Component\Validator\NodeVisitor\NodeValidator; +use Symfony\Component\Validator\NodeVisitor\ContextRefresherVisitor; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolverVisitor; +use Symfony\Component\Validator\NodeVisitor\NodeValidatorVisitor; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; use Symfony\Component\Validator\Validator\Validator; @@ -26,11 +26,11 @@ class Validator2Dot5ApiTest extends Abstract2Dot5ApiTest protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); - $nodeValidator = new NodeValidator($nodeTraverser, new ConstraintValidatorFactory()); + $nodeValidator = new NodeValidatorVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new ExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new Validator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolver(); - $contextRefresher = new ContextRefresher(); + $groupSequenceResolver = new GroupSequenceResolverVisitor(); + $contextRefresher = new ContextRefresherVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); $nodeTraverser->addVisitor($contextRefresher); From 94583a92891560edcdf89fe5f42252c63fb54a86 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 16:28:28 +0100 Subject: [PATCH 35/86] [Validator] Changed NodeTraverser to traverse nodes iteratively, not recursively In this way, the deep method call chains are avoided. Also, it is possible to avoid the many calls to leaveNode(), which are currently not really needed. --- .../Validator/Context/ExecutionContext.php | 44 +------ .../Component/Validator/Node/PropertyNode.php | 18 ++- .../Validator/NodeTraverser/NodeTraverser.php | 116 ++++++------------ .../Validator/NodeTraverser/Traversal.php | 31 +++++ .../Validator/NodeVisitor/AbstractVisitor.php | 6 +- .../NodeVisitor/ContextRefresherVisitor.php | 23 +--- .../GroupSequenceResolverVisitor.php | 2 +- .../NodeObserverInterface.php} | 8 +- .../NodeVisitor/NodeValidatorVisitor.php | 18 +-- .../NodeVisitor/NodeVisitorInterface.php | 4 +- .../NodeVisitor/ObjectInitializerVisitor.php | 2 +- .../Tests/Context/ExecutionContextTest.php | 35 ------ .../Validator/ContextualValidator.php | 2 + 13 files changed, 107 insertions(+), 202 deletions(-) create mode 100644 src/Symfony/Component/Validator/NodeTraverser/Traversal.php rename src/Symfony/Component/Validator/{Util/NodeStackInterface.php => NodeVisitor/NodeObserverInterface.php} (71%) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index b4694492e636e..b58d92350e440 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -20,7 +20,7 @@ use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\Util\NodeStackInterface; +use Symfony\Component\Validator\NodeVisitor\NodeObserverInterface; use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; @@ -33,7 +33,7 @@ * * @see ExecutionContextInterface */ -class ExecutionContext implements ExecutionContextInterface, LegacyExecutionContextInterface, NodeStackInterface +class ExecutionContext implements ExecutionContextInterface, LegacyExecutionContextInterface, NodeObserverInterface { /** * @var ValidatorInterface @@ -76,13 +76,6 @@ class ExecutionContext implements ExecutionContextInterface, LegacyExecutionCont */ private $node; - /** - * The trace of nodes from the root node to the current node. - * - * @var \SplStack - */ - private $nodeStack; - /** * Creates a new execution context. * @@ -108,49 +101,18 @@ public function __construct(ValidatorInterface $validator, $root, GroupManagerIn $this->translator = $translator; $this->translationDomain = $translationDomain; $this->violations = new ConstraintViolationList(); - $this->nodeStack = new \SplStack(); } /** * Sets the values of the context to match the given node. * - * Internally, all nodes are stored on a stack and can be removed from that - * stack using {@link popNode()}. - * * @param Node $node The currently validated node */ - public function pushNode(Node $node) + public function setCurrentNode(Node $node) { - $this->nodeStack->push($node); $this->node = $node; } - /** - * Sets the values of the context to match the previous node. - * - * The current node is removed from the internal stack and returned. - * - * @return Node|null The currently validated node or null, if no node was - * on the stack - */ - public function popNode() - { - // Nothing to do if the stack is empty - if (0 === count($this->nodeStack)) { - return null; - } - - // Remove the current node from the stack - $poppedNode = $this->nodeStack->pop(); - - // Adjust the current node to the previous node - $this->node = count($this->nodeStack) > 0 - ? $this->nodeStack->top() - : null; - - return $poppedNode; - } - /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php index 313da63ab7487..16136863a5b18 100644 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Node; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; /** @@ -32,6 +33,11 @@ */ class PropertyNode extends Node { + /** + * @var object + */ + public $object; + /** * @var PropertyMetadataInterface */ @@ -40,6 +46,8 @@ class PropertyNode extends Node /** * Creates a new property node. * + * @param object $object The object the property + * belongs to * @param mixed $value The property value * @param PropertyMetadataInterface $metadata The property's metadata * @param string $propertyPath The property path leading @@ -49,9 +57,15 @@ class PropertyNode extends Node * @param string[]|null $cascadedGroups The groups in which * cascaded objects should * be validated + * + * @throws UnexpectedTypeException If $object is not an object */ - public function __construct($value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) + public function __construct($object, $value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) { + if (!is_object($object)) { + throw new UnexpectedTypeException($object, 'object'); + } + parent::__construct( $value, $metadata, @@ -59,6 +73,8 @@ public function __construct($value, PropertyMetadataInterface $metadata, $proper $groups, $cascadedGroups ); + + $this->object = $object; } } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index fa4433564d801..b2c4355b769b4 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -44,6 +44,7 @@ class NodeTraverser implements NodeTraverserInterface public function __construct(MetadataFactoryInterface $metadataFactory) { $this->visitors = new \SplObjectStorage(); + $this->nodeQueue = new \SplQueue(); $this->metadataFactory = $metadataFactory; } @@ -73,11 +74,19 @@ public function traverse(array $nodes, ExecutionContextInterface $context) } } + $traversal = new Traversal($context); + foreach ($nodes as $node) { - if ($node instanceof ClassNode) { - $this->traverseClassNode($node, $context); - } else { - $this->traverseNode($node, $context); + $traversal->nodeQueue->enqueue($node); + + while (!$traversal->nodeQueue->isEmpty()) { + $node = $traversal->nodeQueue->dequeue(); + + if ($node instanceof ClassNode) { + $this->traverseClassNode($node, $traversal); + } else { + $this->traverseNode($node, $traversal); + } } } @@ -91,49 +100,31 @@ public function traverse(array $nodes, ExecutionContextInterface $context) } } - private function enterNode(Node $node, ExecutionContextInterface $context) + private function visit(Node $node, ExecutionContextInterface $context) { - $continueTraversal = true; - foreach ($this->visitors as $visitor) { - if (false === $visitor->enterNode($node, $context)) { - $continueTraversal = false; - - // Continue, so that the enterNode() method of all visitors - // is called + if (false === $visitor->visit($node, $context)) { + return false; } } - return $continueTraversal; + return true; } - private function leaveNode(Node $node, ExecutionContextInterface $context) + private function traverseNode(Node $node, Traversal $traversal) { - foreach ($this->visitors as $visitor) { - $visitor->leaveNode($node, $context); + if (false === $this->visit($node, $traversal->context)) { + return; } - } - - private function traverseNode(Node $node, ExecutionContextInterface $context) - { - $continue = $this->enterNode($node, $context); // Visitors have two possibilities to influence the traversal: // - // 1. If a visitor's enterNode() method returns false, the traversal is + // 1. If a visitor's visit() method returns false, the traversal is // skipped entirely. - // 2. If a visitor's enterNode() method removes a group from the node, + // 2. If a visitor's visit() method removes a group from the node, // that group will be skipped in the subtree of that node. - if (false === $continue) { - $this->leaveNode($node, $context); - - return; - } - if (null === $node->value) { - $this->leaveNode($node, $context); - return; } @@ -144,8 +135,6 @@ private function traverseNode(Node $node, ExecutionContextInterface $context) : $node->groups; if (0 === count($cascadedGroups)) { - $this->leaveNode($node, $context); - return; } @@ -161,11 +150,9 @@ private function traverseNode(Node $node, ExecutionContextInterface $context) $node->propertyPath, $cascadedGroups, $traversalStrategy, - $context + $traversal ); - $this->leaveNode($node, $context); - return; } @@ -178,25 +165,19 @@ private function traverseNode(Node $node, ExecutionContextInterface $context) $node->propertyPath, $cascadedGroups, $traversalStrategy, - $context + $traversal ); - $this->leaveNode($node, $context); - return; } // Traverse only if the TRAVERSE bit is set if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { - $this->leaveNode($node, $context); - return; } if (!$node->value instanceof \Traversable) { if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { - $this->leaveNode($node, $context); - return; } @@ -212,15 +193,15 @@ private function traverseNode(Node $node, ExecutionContextInterface $context) $node->propertyPath, $cascadedGroups, $traversalStrategy, - $context + $traversal ); - - $this->leaveNode($node, $context); } - private function traverseClassNode(ClassNode $node, ExecutionContextInterface $context, $traversalStrategy = TraversalStrategy::IMPLICIT) + private function traverseClassNode(ClassNode $node, Traversal $traversal, $traversalStrategy = TraversalStrategy::IMPLICIT) { - $continue = $this->enterNode($node, $context); + if (false === $this->visit($node, $traversal->context)) { + return; + } // Visitors have two possibilities to influence the traversal: // @@ -229,21 +210,14 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c // 2. If a visitor's enterNode() method removes a group from the node, // that group will be skipped in the subtree of that node. - if (false === $continue) { - $this->leaveNode($node, $context); - - return; - } - if (0 === count($node->groups)) { - $this->leaveNode($node, $context); - return; } foreach ($node->metadata->getConstrainedProperties() as $propertyName) { foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { - $propertyNode = new PropertyNode( + $traversal->nodeQueue->enqueue(new PropertyNode( + $node->value, $propertyMetadata->getPropertyValue($node->value), $propertyMetadata, $node->propertyPath @@ -251,9 +225,7 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c : $propertyName, $node->groups, $node->cascadedGroups - ); - - $this->traverseNode($propertyNode, $context); + )); } } @@ -265,15 +237,11 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c // Traverse only if the TRAVERSE bit is set if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { - $this->leaveNode($node, $context); - return; } if (!$node->value instanceof \Traversable) { if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { - $this->leaveNode($node, $context); - return; } @@ -289,13 +257,11 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c $node->propertyPath, $node->groups, $traversalStrategy, - $context + $traversal ); - - $this->leaveNode($node, $context); } - private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) + private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, Traversal $traversal) { try { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -304,14 +270,12 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal // error } - $classNode = new ClassNode( + $traversal->nodeQueue->enqueue(new ClassNode( $object, $classMetadata, $propertyPath, $groups - ); - - $this->traverseClassNode($classNode, $context, $traversalStrategy); + )); } catch (NoSuchMetadataException $e) { // Rethrow if the TRAVERSE bit is not set if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { @@ -329,12 +293,12 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal $propertyPath, $groups, $traversalStrategy, - $context + $traversal ); } } - private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) + private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy, Traversal $traversal) { if ($traversalStrategy & TraversalStrategy::RECURSIVE) { // Try to traverse nested objects, but ignore if they do not @@ -357,7 +321,7 @@ private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $propertyPath.'['.$key.']', $groups, $traversalStrategy, - $context + $traversal ); continue; @@ -371,7 +335,7 @@ private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $propertyPath.'['.$key.']', $groups, $traversalStrategy, - $context + $traversal ); } } diff --git a/src/Symfony/Component/Validator/NodeTraverser/Traversal.php b/src/Symfony/Component/Validator/NodeTraverser/Traversal.php new file mode 100644 index 0000000000000..ab3a17e36c3b2 --- /dev/null +++ b/src/Symfony/Component/Validator/NodeTraverser/Traversal.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\NodeTraverser; + +use Symfony\Component\Validator\Context\ExecutionContextInterface; + +/** + * @since %%NextVersion%% + * @author Bernhard Schussek + */ +class Traversal +{ + public $context; + + public $nodeQueue; + + public function __construct(ExecutionContextInterface $context) + { + $this->context = $context; + $this->nodeQueue = new \SplQueue(); + } +} diff --git a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php index 47631556f0d67..a47783ab0c8a7 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php @@ -28,11 +28,7 @@ public function afterTraversal(array $nodes, ExecutionContextInterface $context) { } - public function enterNode(Node $node, ExecutionContextInterface $context) - { - } - - public function leaveNode(Node $node, ExecutionContextInterface $context) + public function visit(Node $node, ExecutionContextInterface $context) { } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php index 3b940043a6848..b28b2f542fdad 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php @@ -14,7 +14,6 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Exception\RuntimeException; use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\Util\NodeStackInterface; /** * Updates the current context with the current node of the validation @@ -25,31 +24,17 @@ */ class ContextRefresherVisitor extends AbstractVisitor { - public function enterNode(Node $node, ExecutionContextInterface $context) + public function visit(Node $node, ExecutionContextInterface $context) { - if (!$context instanceof NodeStackInterface) { + if (!$context instanceof NodeObserverInterface) { throw new RuntimeException(sprintf( 'The ContextRefresherVisitor only supports instances of class '. - '"Symfony\Component\Validator\Context\NodeStackInterface". '. + '"Symfony\Component\Validator\NodeVisitor\NodeObserverInterface". '. 'An instance of class "%s" was given.', get_class($context) )); } - $context->pushNode($node); - } - - public function leaveNode(Node $node, ExecutionContextInterface $context) - { - if (!$context instanceof NodeStackInterface) { - throw new RuntimeException(sprintf( - 'The ContextRefresherVisitor only supports instances of class '. - '"Symfony\Component\Validator\Context\NodeStackInterface". '. - 'An instance of class "%s" was given.', - get_class($context) - )); - } - - $context->popNode(); + $context->setCurrentNode($node); } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php index c03f7a64c3801..6b9330ec8d317 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php @@ -23,7 +23,7 @@ */ class GroupSequenceResolverVisitor extends AbstractVisitor { - public function enterNode(Node $node, ExecutionContextInterface $context) + public function visit(Node $node, ExecutionContextInterface $context) { if (!$node instanceof ClassNode) { return; diff --git a/src/Symfony/Component/Validator/Util/NodeStackInterface.php b/src/Symfony/Component/Validator/NodeVisitor/NodeObserverInterface.php similarity index 71% rename from src/Symfony/Component/Validator/Util/NodeStackInterface.php rename to src/Symfony/Component/Validator/NodeVisitor/NodeObserverInterface.php index f330fa747e89f..6588c09c90b90 100644 --- a/src/Symfony/Component/Validator/Util/NodeStackInterface.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeObserverInterface.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Validator\Util; +namespace Symfony\Component\Validator\NodeVisitor; use Symfony\Component\Validator\Node\Node; @@ -17,9 +17,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -interface NodeStackInterface +interface NodeObserverInterface { - public function pushNode(Node $node); - - public function popNode(); + public function setCurrentNode(Node $node); } diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php index 6261791070d3f..bded8c38a1798 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php @@ -42,29 +42,24 @@ class NodeValidatorVisitor extends AbstractVisitor implements GroupManagerInterf private $currentGroup; - private $objectHashStack; - public function __construct(NodeTraverserInterface $nodeTraverser, ConstraintValidatorFactoryInterface $validatorFactory) { $this->validatorFactory = $validatorFactory; $this->nodeTraverser = $nodeTraverser; - $this->objectHashStack = new \SplStack(); } public function afterTraversal(array $nodes, ExecutionContextInterface $context) { $this->validatedObjects = array(); $this->validatedConstraints = array(); - $this->objectHashStack = new \SplStack(); } - public function enterNode(Node $node, ExecutionContextInterface $context) + public function visit(Node $node, ExecutionContextInterface $context) { if ($node instanceof ClassNode) { $objectHash = spl_object_hash($node->value); - $this->objectHashStack->push($objectHash); - } elseif ($node instanceof PropertyNode && count($this->objectHashStack) > 0) { - $objectHash = $this->objectHashStack->top(); + } elseif ($node instanceof PropertyNode) { + $objectHash = spl_object_hash($node->object); } else { $objectHash = null; } @@ -116,13 +111,6 @@ public function enterNode(Node $node, ExecutionContextInterface $context) return true; } - public function leaveNode(Node $node, ExecutionContextInterface $context) - { - if ($node instanceof ClassNode) { - $this->objectHashStack->pop(); - } - } - public function getCurrentGroup() { return $this->currentGroup; diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php index 6736cc23ba966..012a64191c292 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php @@ -24,7 +24,5 @@ public function beforeTraversal(array $nodes, ExecutionContextInterface $context public function afterTraversal(array $nodes, ExecutionContextInterface $context); - public function enterNode(Node $node, ExecutionContextInterface $context); - - public function leaveNode(Node $node, ExecutionContextInterface $context); + public function visit(Node $node, ExecutionContextInterface $context); } diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php index 1bef7f3189216..01a2a07b5eb6b 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php @@ -43,7 +43,7 @@ public function __construct(array $initializers) $this->initializers = $initializers; } - public function enterNode(Node $node, ExecutionContextInterface $context) + public function visit(Node $node, ExecutionContextInterface $context) { if (!$node instanceof ClassNode) { return; diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php index 8019aecea1ae4..45d3a70c12437 100644 --- a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php +++ b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php @@ -55,41 +55,6 @@ protected function setUp() ); } - public function testPushAndPop() - { - $metadata = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); - $node = new GenericNode('value', $metadata, '', array(), array()); - - $this->context->pushNode($node); - - $this->assertSame('value', $this->context->getValue()); - // the other methods are covered in AbstractValidatorTest - - $this->assertSame($node, $this->context->popNode()); - - $this->assertNull($this->context->getValue()); - } - - public function testPushTwiceAndPop() - { - $metadata1 = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); - $node1 = new GenericNode('value', $metadata1, '', array(), array()); - $metadata2 = $this->getMock('Symfony\Component\Validator\Mapping\MetadataInterface'); - $node2 = new GenericNode('other value', $metadata2, '', array(), array()); - - $this->context->pushNode($node1); - $this->context->pushNode($node2); - - $this->assertSame($node2, $this->context->popNode()); - - $this->assertSame('value', $this->context->getValue()); - } - - public function testPopWithoutPush() - { - $this->assertNull($this->context->popNode()); - } - public function testGetGroup() { $this->groupManager->expects($this->once()) diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index 588beaa42361e..2027240d29790 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -141,6 +141,7 @@ public function validateProperty($object, $propertyName, $groups = null) $propertyValue = $propertyMetadata->getPropertyValue($object); $nodes[] = new PropertyNode( + $object, $propertyValue, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), @@ -172,6 +173,7 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = foreach ($propertyMetadatas as $propertyMetadata) { $nodes[] = new PropertyNode( + $object, $value, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), From 117b1b9a17edba7b0724bc6ce52e3957e1229887 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 17:12:00 +0100 Subject: [PATCH 36/86] [Validator] Wrapped collections into CollectionNode instances --- .../Validator/Mapping/CollectionMetadata.php | 48 +++++++ .../Validator/Node/CollectionNode.php | 56 ++++++++ .../Validator/NodeTraverser/NodeTraverser.php | 124 +++++++++--------- 3 files changed, 169 insertions(+), 59 deletions(-) create mode 100644 src/Symfony/Component/Validator/Mapping/CollectionMetadata.php create mode 100644 src/Symfony/Component/Validator/Node/CollectionNode.php diff --git a/src/Symfony/Component/Validator/Mapping/CollectionMetadata.php b/src/Symfony/Component/Validator/Mapping/CollectionMetadata.php new file mode 100644 index 0000000000000..f1235af899263 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/CollectionMetadata.php @@ -0,0 +1,48 @@ + + * + * 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 CollectionMetadata implements MetadataInterface +{ + private $traversalStrategy; + + public function __construct($traversalStrategy) + { + $this->traversalStrategy = $traversalStrategy; + } + + /** + * 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) + { + return array(); + } + + public function getCascadingStrategy() + { + return CascadingStrategy::NONE; + } + + public function getTraversalStrategy() + { + return $this->traversalStrategy; + } +} diff --git a/src/Symfony/Component/Validator/Node/CollectionNode.php b/src/Symfony/Component/Validator/Node/CollectionNode.php new file mode 100644 index 0000000000000..848c7a6c929d7 --- /dev/null +++ b/src/Symfony/Component/Validator/Node/CollectionNode.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Node; + +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Mapping\MetadataInterface; + +/** + * Represents an traversable collection in the validation graph. + * + * @since 2.5 + * @author Bernhard Schussek + */ +class CollectionNode extends Node +{ + /** + * Creates a new collection node. + * + * @param array|\Traversable $collection The validated collection + * @param MetadataInterface $metadata The class metadata of that + * object + * @param string $propertyPath The property path leading + * to this node + * @param string[] $groups The groups in which this + * node should be validated + * @param string[]|null $cascadedGroups The groups in which + * cascaded objects should be + * validated + * + * @throws UnexpectedTypeException If the given value is not an array or + * an instance of {@link \Traversable} + */ + public function __construct($collection, MetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) + { + if (!is_array($collection) && !$collection instanceof \Traversable) { + throw new UnexpectedTypeException($collection, 'object'); + } + + parent::__construct( + $collection, + $metadata, + $propertyPath, + $groups, + $cascadedGroups + ); + } +} diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index b2c4355b769b4..7be878d713ff2 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -16,9 +16,11 @@ use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Mapping\CascadingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; +use Symfony\Component\Validator\Mapping\CollectionMetadata; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\CollectionNode; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; @@ -84,6 +86,8 @@ public function traverse(array $nodes, ExecutionContextInterface $context) if ($node instanceof ClassNode) { $this->traverseClassNode($node, $traversal); + } elseif ($node instanceof CollectionNode) { + $this->traverseCollectionNode($node, $traversal); } else { $this->traverseNode($node, $traversal); } @@ -145,13 +149,12 @@ private function traverseNode(Node $node, Traversal $traversal) // Arrays are always traversed, independent of the specified // traversal strategy // (BC with Symfony < 2.5) - $this->cascadeEachObjectIn( + $traversal->nodeQueue->enqueue(new CollectionNode( $node->value, + new CollectionMetadata($traversalStrategy), $node->propertyPath, - $cascadedGroups, - $traversalStrategy, - $traversal - ); + $cascadedGroups + )); return; } @@ -188,13 +191,13 @@ private function traverseNode(Node $node, Traversal $traversal) )); } - $this->cascadeEachObjectIn( + $traversal->nodeQueue->enqueue(new CollectionNode( $node->value, + new CollectionMetadata($traversalStrategy), $node->propertyPath, - $cascadedGroups, - $traversalStrategy, - $traversal - ); + $node->groups, + $node->cascadedGroups + )); } private function traverseClassNode(ClassNode $node, Traversal $traversal, $traversalStrategy = TraversalStrategy::IMPLICIT) @@ -252,54 +255,23 @@ private function traverseClassNode(ClassNode $node, Traversal $traversal, $trave )); } - $this->cascadeEachObjectIn( + $traversal->nodeQueue->enqueue(new CollectionNode( $node->value, + new CollectionMetadata($traversalStrategy), $node->propertyPath, $node->groups, - $traversalStrategy, - $traversal - ); + $node->cascadedGroups + )); } - private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, Traversal $traversal) + private function traverseCollectionNode(CollectionNode $node, Traversal $traversal) { - try { - $classMetadata = $this->metadataFactory->getMetadataFor($object); - - if (!$classMetadata instanceof ClassMetadataInterface) { - // error - } - - $traversal->nodeQueue->enqueue(new ClassNode( - $object, - $classMetadata, - $propertyPath, - $groups - )); - } catch (NoSuchMetadataException $e) { - // Rethrow if the TRAVERSE bit is not set - if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { - throw $e; - } - - // Rethrow if the object does not implement Traversable - if (!$object instanceof \Traversable) { - throw $e; - } - - // In that case, iterate the object and cascade each entry - $this->cascadeEachObjectIn( - $object, - $propertyPath, - $groups, - $traversalStrategy, - $traversal - ); + if (false === $this->visit($node, $traversal->context)) { + return; } - } - private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy, Traversal $traversal) - { + $traversalStrategy = $node->metadata->getTraversalStrategy(); + if ($traversalStrategy & TraversalStrategy::RECURSIVE) { // Try to traverse nested objects, but ignore if they do not // implement Traversable @@ -311,18 +283,17 @@ private function cascadeEachObjectIn($collection, $propertyPath, array $groups, $traversalStrategy = TraversalStrategy::IMPLICIT; } - foreach ($collection as $key => $value) { + 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->cascadeEachObjectIn( + $traversal->nodeQueue->enqueue(new CollectionNode( $value, - $propertyPath.'['.$key.']', - $groups, - $traversalStrategy, - $traversal - ); + new CollectionMetadata($traversalStrategy), + $node->propertyPath.'['.$key.']', + $node->groups + )); continue; } @@ -332,12 +303,47 @@ private function cascadeEachObjectIn($collection, $propertyPath, array $groups, if (is_object($value)) { $this->cascadeObject( $value, - $propertyPath.'['.$key.']', - $groups, + $node->propertyPath.'['.$key.']', + $node->groups, $traversalStrategy, $traversal ); } } } + + private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, Traversal $traversal) + { + try { + $classMetadata = $this->metadataFactory->getMetadataFor($object); + + if (!$classMetadata instanceof ClassMetadataInterface) { + // error + } + + $traversal->nodeQueue->enqueue(new ClassNode( + $object, + $classMetadata, + $propertyPath, + $groups + )); + } catch (NoSuchMetadataException $e) { + // Rethrow if the TRAVERSE bit is not set + if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { + throw $e; + } + + // Rethrow if the object does not implement Traversable + if (!$object instanceof \Traversable) { + throw $e; + } + + $traversal->nodeQueue->enqueue(new CollectionNode( + $object, + new CollectionMetadata($traversalStrategy), + $propertyPath, + $groups + )); + } + } } From 51197f68a398ae63c97e0527d43be30b77afdf12 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 19:10:26 +0100 Subject: [PATCH 37/86] [Validator] Made traversal of Traversables consistent If the traversal strategy is IMPLICIT (the default), the validator will now traverse any object that implements \Traversable and any array --- .../Validator/Constraints/Traverse.php | 2 +- .../Component/Validator/Constraints/Valid.php | 8 -- .../Validator/Mapping/ClassMetadata.php | 22 +++ .../Validator/Mapping/CollectionMetadata.php | 48 ------- .../Validator/Mapping/GenericMetadata.php | 22 +-- .../Validator/Mapping/MemberMetadata.php | 15 +-- .../Validator/Mapping/TraversalStrategy.php | 13 +- .../Component/Validator/Node/ClassNode.php | 8 +- .../Validator/Node/CollectionNode.php | 29 ++-- src/Symfony/Component/Validator/Node/Node.php | 28 ++-- .../Component/Validator/Node/PropertyNode.php | 7 +- .../Validator/NodeTraverser/NodeTraverser.php | 125 +++++++++--------- .../NodeVisitor/NodeValidatorVisitor.php | 5 + .../Tests/Validator/Abstract2Dot5ApiTest.php | 40 +++++- .../Tests/Validator/AbstractLegacyApiTest.php | 23 ++++ .../Tests/Validator/AbstractValidatorTest.php | 25 +--- .../Validator/ContextualValidator.php | 28 +++- .../ContextualValidatorInterface.php | 2 +- .../Validator/Validator/LegacyValidator.php | 16 +-- .../Validator/Validator/Validator.php | 4 +- .../Validator/ValidatorInterface.php | 2 +- 21 files changed, 244 insertions(+), 228 deletions(-) delete mode 100644 src/Symfony/Component/Validator/Mapping/CollectionMetadata.php diff --git a/src/Symfony/Component/Validator/Constraints/Traverse.php b/src/Symfony/Component/Validator/Constraints/Traverse.php index d9afe60c0745b..6c220561917f9 100644 --- a/src/Symfony/Component/Validator/Constraints/Traverse.php +++ b/src/Symfony/Component/Validator/Constraints/Traverse.php @@ -51,6 +51,6 @@ public function getDefaultOption() */ public function getTargets() { - return array(self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT); + return self::CLASS_CONSTRAINT; } } diff --git a/src/Symfony/Component/Validator/Constraints/Valid.php b/src/Symfony/Component/Validator/Constraints/Valid.php index 9f15fdb04e1db..218f265f4f166 100644 --- a/src/Symfony/Component/Validator/Constraints/Valid.php +++ b/src/Symfony/Component/Validator/Constraints/Valid.php @@ -23,16 +23,8 @@ */ class Valid extends Constraint { - /** - * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. - * Use the {@link Traverse} constraint instead. - */ public $traverse = true; - /** - * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. - * Use the {@link Traverse} constraint instead. - */ public $deep = false; public function __construct($options = null) diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index d3c775b579cf1..5a2782f854041 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\PropertyMetadataContainerInterface; @@ -190,6 +191,27 @@ public function addConstraint(Constraint $constraint) )); } + if ($constraint instanceof Traverse) { + if (true === $constraint->traverse) { + // If traverse is true, traversal should be explicitly enabled + $this->traversalStrategy = TraversalStrategy::TRAVERSE; + + if (!$constraint->deep) { + $this->traversalStrategy |= TraversalStrategy::STOP_RECURSION; + } + } elseif (false === $constraint->traverse) { + // If traverse is false, traversal should be explicitly disabled + $this->traversalStrategy = TraversalStrategy::NONE; + } else { + // Else, traverse depending on the contextual information that + // is available during validation + $this->traversalStrategy = TraversalStrategy::IMPLICIT; + } + + // The constraint is not added + return $this; + } + $constraint->addImplicitGroupName($this->getDefaultGroup()); parent::addConstraint($constraint); diff --git a/src/Symfony/Component/Validator/Mapping/CollectionMetadata.php b/src/Symfony/Component/Validator/Mapping/CollectionMetadata.php deleted file mode 100644 index f1235af899263..0000000000000 --- a/src/Symfony/Component/Validator/Mapping/CollectionMetadata.php +++ /dev/null @@ -1,48 +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\Mapping; - -/** - * @since %%NextVersion%% - * @author Bernhard Schussek - */ -class CollectionMetadata implements MetadataInterface -{ - private $traversalStrategy; - - public function __construct($traversalStrategy) - { - $this->traversalStrategy = $traversalStrategy; - } - - /** - * 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) - { - return array(); - } - - public function getCascadingStrategy() - { - return CascadingStrategy::NONE; - } - - public function getTraversalStrategy() - { - return $this->traversalStrategy; - } -} diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index 369276f220e89..69064a2cf494f 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -77,27 +77,17 @@ public function addConstraint(Constraint $constraint) if ($constraint instanceof Valid) { $this->cascadingStrategy = CascadingStrategy::CASCADE; - return $this; - } - - if ($constraint instanceof Traverse) { - if (true === $constraint->traverse) { - // If traverse is true, traversal should be explicitly enabled - $this->traversalStrategy = TraversalStrategy::TRAVERSE; + if ($constraint->traverse) { + // Traverse unless the value is not traversable + $this->traversalStrategy = TraversalStrategy::IMPLICIT; - if ($constraint->deep) { - $this->traversalStrategy |= TraversalStrategy::RECURSIVE; + if (!$constraint->deep) { + $this->traversalStrategy |= TraversalStrategy::STOP_RECURSION; } - } elseif (false === $constraint->traverse) { - // If traverse is false, traversal should be explicitly disabled - $this->traversalStrategy = TraversalStrategy::NONE; } else { - // Else, traverse depending on the contextual information that - // is available during validation - $this->traversalStrategy = TraversalStrategy::IMPLICIT; + $this->traversalStrategy = TraversalStrategy::NONE; } - // The constraint is not added return $this; } diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 230af7d89e416..f23bdef04c7dd 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -60,17 +60,14 @@ public function addConstraint(Constraint $constraint) } // BC with Symfony < 2.5 - // Only process if the traversal strategy was not already set by the - // Traverse constraint - if ($constraint instanceof Valid && !$this->traversalStrategy) { + if ($constraint instanceof Valid) { if (true === $constraint->traverse) { // Try to traverse cascaded objects, but ignore if they do not // implement Traversable - $this->traversalStrategy = TraversalStrategy::TRAVERSE - | TraversalStrategy::IGNORE_NON_TRAVERSABLE; + $this->traversalStrategy = TraversalStrategy::IMPLICIT; - if ($constraint->deep) { - $this->traversalStrategy |= TraversalStrategy::RECURSIVE; + if (!$constraint->deep) { + $this->traversalStrategy |= TraversalStrategy::STOP_RECURSION; } } elseif (false === $constraint->traverse) { $this->traversalStrategy = TraversalStrategy::NONE; @@ -180,7 +177,7 @@ public function isCascaded() */ public function isCollectionCascaded() { - return (boolean) ($this->traversalStrategy & TraversalStrategy::TRAVERSE); + return (boolean) ($this->traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE)); } /** @@ -191,7 +188,7 @@ public function isCollectionCascaded() */ public function isCollectionCascadedDeeply() { - return (boolean) ($this->traversalStrategy & TraversalStrategy::RECURSIVE); + return !($this->traversalStrategy & TraversalStrategy::STOP_RECURSION); } /** diff --git a/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php index 951ec6058d87a..22b7f534550a2 100644 --- a/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php +++ b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php @@ -17,15 +17,16 @@ */ class TraversalStrategy { - const IMPLICIT = 0; + /** + * @var integer + */ + const IMPLICIT = 1; - const NONE = 1; + const NONE = 2; - const TRAVERSE = 2; + const TRAVERSE = 4; - const RECURSIVE = 4; - - const IGNORE_NON_TRAVERSABLE = 8; + const STOP_RECURSION = 8; private function __construct() { diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php index 904e8651fdbc6..7c82eead9adec 100644 --- a/src/Symfony/Component/Validator/Node/ClassNode.php +++ b/src/Symfony/Component/Validator/Node/ClassNode.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; +use Symfony\Component\Validator\Mapping\TraversalStrategy; /** * Represents an object and its class metadata in the validation graph. @@ -40,10 +41,11 @@ class ClassNode extends Node * @param string[]|null $cascadedGroups The groups in which * cascaded objects should be * validated + * @param integer $traversalStrategy * - * @throws UnexpectedTypeException If the given value is not an object + * @throws \Symfony\Component\Validator\Exception\UnexpectedTypeException */ - public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) + public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { if (!is_object($object)) { throw new UnexpectedTypeException($object, 'object'); @@ -56,5 +58,7 @@ public function __construct($object, ClassMetadataInterface $metadata, $property $groups, $cascadedGroups ); + + $this->traversalStrategy = $traversalStrategy; } } diff --git a/src/Symfony/Component/Validator/Node/CollectionNode.php b/src/Symfony/Component/Validator/Node/CollectionNode.php index 848c7a6c929d7..2df442a4fc699 100644 --- a/src/Symfony/Component/Validator/Node/CollectionNode.php +++ b/src/Symfony/Component/Validator/Node/CollectionNode.php @@ -11,8 +11,10 @@ namespace Symfony\Component\Validator\Node; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Mapping\MetadataInterface; +use Symfony\Component\Validator\Mapping\TraversalStrategy; /** * Represents an traversable collection in the validation graph. @@ -25,32 +27,35 @@ class CollectionNode extends Node /** * Creates a new collection node. * - * @param array|\Traversable $collection The validated collection - * @param MetadataInterface $metadata The class metadata of that - * object - * @param string $propertyPath The property path leading + * @param array|\Traversable $collection The validated collection + * @param string $propertyPath The property path leading * to this node - * @param string[] $groups The groups in which this + * @param string[] $groups The groups in which this * node should be validated - * @param string[]|null $cascadedGroups The groups in which + * @param string[]|null $cascadedGroups The groups in which * cascaded objects should be * validated + * @param integer $traversalStrategy The traversal strategy * - * @throws UnexpectedTypeException If the given value is not an array or - * an instance of {@link \Traversable} + * @throws \Symfony\Component\Validator\Exception\ConstraintDefinitionException */ - public function __construct($collection, MetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) + public function __construct($collection, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::TRAVERSE) { if (!is_array($collection) && !$collection instanceof \Traversable) { - throw new UnexpectedTypeException($collection, 'object'); + throw new ConstraintDefinitionException(sprintf( + 'Traversal was enabled for "%s", but this class '. + 'does not implement "\Traversable".', + get_class($collection) + )); } parent::__construct( $collection, - $metadata, + null, $propertyPath, $groups, - $cascadedGroups + $cascadedGroups, + $traversalStrategy ); } } diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php index b301db8dda696..038217bf512f9 100644 --- a/src/Symfony/Component/Validator/Node/Node.php +++ b/src/Symfony/Component/Validator/Node/Node.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Mapping\MetadataInterface; +use Symfony\Component\Validator\Mapping\TraversalStrategy; /** * A node in the validated graph. @@ -32,7 +33,7 @@ abstract class Node /** * The metadata specifying how the value should be validated. * - * @var MetadataInterface + * @var MetadataInterface|null */ public $metadata; @@ -57,21 +58,27 @@ abstract class Node */ public $cascadedGroups; + /** + * @var integer + */ + public $traversalStrategy; + /** * Creates a new property node. * - * @param mixed $value The property value - * @param MetadataInterface $metadata The property's metadata - * @param string $propertyPath The property path leading to - * this node - * @param string[] $groups The groups in which this node - * should be validated - * @param string[]|null $cascadedGroups The groups in which cascaded - * objects should be validated + * @param mixed $value The property value + * @param MetadataInterface|null $metadata The property's metadata + * @param string $propertyPath The property path leading to + * this node + * @param string[] $groups The groups in which this node + * should be validated + * @param string[]|null $cascadedGroups The groups in which cascaded + * objects should be validated + * @param integer $traversalStrategy * * @throws UnexpectedTypeException If $cascadedGroups is invalid */ - public function __construct($value, MetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) + public function __construct($value, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { if (null !== $cascadedGroups && !is_array($cascadedGroups)) { throw new UnexpectedTypeException($cascadedGroups, 'null or array'); @@ -82,5 +89,6 @@ public function __construct($value, MetadataInterface $metadata, $propertyPath, $this->propertyPath = $propertyPath; $this->groups = $groups; $this->cascadedGroups = $cascadedGroups; + $this->traversalStrategy = $traversalStrategy; } } diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php index 16136863a5b18..14dedb320356e 100644 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; +use Symfony\Component\Validator\Mapping\TraversalStrategy; /** * Represents the value of a property and its associated metadata. @@ -57,10 +58,11 @@ class PropertyNode extends Node * @param string[]|null $cascadedGroups The groups in which * cascaded objects should * be validated + * @param integer $traversalStrategy * * @throws UnexpectedTypeException If $object is not an object */ - public function __construct($object, $value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null) + public function __construct($object, $value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { if (!is_object($object)) { throw new UnexpectedTypeException($object, 'object'); @@ -71,7 +73,8 @@ public function __construct($object, $value, PropertyMetadataInterface $metadata $metadata, $propertyPath, $groups, - $cascadedGroups + $cascadedGroups, + $traversalStrategy ); $this->object = $object; diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index 7be878d713ff2..44ab2eaa27b72 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -117,17 +117,17 @@ private function visit(Node $node, ExecutionContextInterface $context) private function traverseNode(Node $node, Traversal $traversal) { - if (false === $this->visit($node, $traversal->context)) { - return; - } - // Visitors have two possibilities to influence the traversal: // - // 1. If a visitor's visit() method returns false, the traversal is + // 1. If a visitor's enterNode() method returns false, the traversal is // skipped entirely. - // 2. If a visitor's visit() method removes a group from the node, + // 2. If a visitor's enterNode() method removes a group from the node, // that group will be skipped in the subtree of that node. + if (false === $this->visit($node, $traversal->context)) { + return; + } + if (null === $node->value) { return; } @@ -151,9 +151,10 @@ private function traverseNode(Node $node, Traversal $traversal) // (BC with Symfony < 2.5) $traversal->nodeQueue->enqueue(new CollectionNode( $node->value, - new CollectionMetadata($traversalStrategy), $node->propertyPath, - $cascadedGroups + $cascadedGroups, + null, + $traversalStrategy )); return; @@ -174,38 +175,28 @@ private function traverseNode(Node $node, Traversal $traversal) return; } - // Traverse only if the TRAVERSE bit is set - if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { + // Traverse only if IMPLICIT or TRAVERSE + if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { return; } - if (!$node->value instanceof \Traversable) { - if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { - return; - } - - throw new ConstraintDefinitionException(sprintf( - 'Traversal was enabled for "%s", but this class '. - 'does not implement "\Traversable".', - get_class($node->value) - )); + // 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 $traversal->nodeQueue->enqueue(new CollectionNode( $node->value, - new CollectionMetadata($traversalStrategy), $node->propertyPath, - $node->groups, - $node->cascadedGroups + $cascadedGroups, + null, + $traversalStrategy )); } - private function traverseClassNode(ClassNode $node, Traversal $traversal, $traversalStrategy = TraversalStrategy::IMPLICIT) + private function traverseClassNode(ClassNode $node, Traversal $traversal) { - if (false === $this->visit($node, $traversal->context)) { - return; - } - // Visitors have two possibilities to influence the traversal: // // 1. If a visitor's enterNode() method returns false, the traversal is @@ -213,6 +204,10 @@ private function traverseClassNode(ClassNode $node, Traversal $traversal, $trave // 2. If a visitor's enterNode() method removes a group from the node, // that group will be skipped in the subtree of that node. + if (false === $this->visit($node, $traversal->context)) { + return; + } + if (0 === count($node->groups)) { return; } @@ -232,54 +227,58 @@ private function traverseClassNode(ClassNode $node, Traversal $traversal, $trave } } + $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::IMPLICIT === $traversalStrategy) { - $traversalStrategy = $node->metadata->getTraversalStrategy(); + if ($traversalStrategy & TraversalStrategy::IMPLICIT) { + // Keep the STOP_RECURSION flag, if it was set + $traversalStrategy = $node->metadata->getTraversalStrategy() + | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); } - // Traverse only if the TRAVERSE bit is set - if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { + // Traverse only if IMPLICIT or TRAVERSE + if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { return; } - if (!$node->value instanceof \Traversable) { - if ($traversalStrategy & TraversalStrategy::IGNORE_NON_TRAVERSABLE) { - return; - } - - throw new ConstraintDefinitionException(sprintf( - 'Traversal was enabled for "%s", but this class '. - 'does not implement "\Traversable".', - get_class($node->value) - )); + // 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 $traversal->nodeQueue->enqueue(new CollectionNode( $node->value, - new CollectionMetadata($traversalStrategy), $node->propertyPath, $node->groups, - $node->cascadedGroups + $node->cascadedGroups, + $traversalStrategy )); } private function traverseCollectionNode(CollectionNode $node, Traversal $traversal) { + // Visitors have two possibilities to influence the traversal: + // + // 1. If a visitor's enterNode() method returns false, the traversal is + // skipped entirely. + // 2. If a visitor's enterNode() method removes a group from the node, + // that group will be skipped in the subtree of that node. + if (false === $this->visit($node, $traversal->context)) { return; } - $traversalStrategy = $node->metadata->getTraversalStrategy(); + if (0 === count($node->groups)) { + return; + } + + $traversalStrategy = $node->traversalStrategy; - if ($traversalStrategy & TraversalStrategy::RECURSIVE) { - // Try to traverse nested objects, but ignore if they do not - // implement Traversable - $traversalStrategy |= TraversalStrategy::IGNORE_NON_TRAVERSABLE; + if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) { + $traversalStrategy = TraversalStrategy::NONE; } else { - // If the RECURSIVE bit is not set, change the strategy to IMPLICIT - // in order to respect the metadata's traversal strategy of each entry - // in the collection $traversalStrategy = TraversalStrategy::IMPLICIT; } @@ -290,9 +289,10 @@ private function traverseCollectionNode(CollectionNode $node, Traversal $travers // (BC with Symfony < 2.5) $traversal->nodeQueue->enqueue(new CollectionNode( $value, - new CollectionMetadata($traversalStrategy), $node->propertyPath.'['.$key.']', - $node->groups + $node->groups, + null, + $traversalStrategy )); continue; @@ -325,24 +325,27 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal $object, $classMetadata, $propertyPath, - $groups + $groups, + null, + $traversalStrategy )); } catch (NoSuchMetadataException $e) { - // Rethrow if the TRAVERSE bit is not set - if (!($traversalStrategy & TraversalStrategy::TRAVERSE)) { + // Rethrow if not Traversable + if (!$object instanceof \Traversable) { throw $e; } - // Rethrow if the object does not implement Traversable - if (!$object instanceof \Traversable) { + // Rethrow unless IMPLICIT or TRAVERSE + if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { throw $e; } $traversal->nodeQueue->enqueue(new CollectionNode( $object, - new CollectionMetadata($traversalStrategy), $propertyPath, - $groups + $groups, + null, + $traversalStrategy )); } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php index bded8c38a1798..6cab8a3caced1 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php @@ -16,6 +16,7 @@ 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; use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; @@ -56,6 +57,10 @@ public function afterTraversal(array $nodes, ExecutionContextInterface $context) public function visit(Node $node, ExecutionContextInterface $context) { + if ($node instanceof CollectionNode) { + return true; + } + if ($node instanceof ClassNode) { $objectHash = spl_object_hash($node->value); } elseif ($node instanceof PropertyNode) { diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 97c982d116e2b..04f42480cedcb 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Tests\Fixtures\Reference; @@ -354,14 +355,43 @@ public function testValidateAcceptsValid() $this->assertNull($violations[0]->getCode()); } - /** - * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException - */ - public function testExpectTraversableIfTraverse() + public function testTraverseTraversableByDefault() { + $test = $this; $entity = new Entity(); + $traversable = new \ArrayIterator(array('key' => $entity)); + + $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity, $traversable) { + $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); + $test->assertNull($context->getPropertyName()); + $test->assertSame('[key]', $context->getPropertyPath()); + $test->assertSame('Group', $context->getGroup()); + $test->assertSame($test->metadata, $context->getMetadata()); + $test->assertSame($traversable, $context->getRoot()); + $test->assertSame($entity, $context->getValue()); + $test->assertSame($entity, $value); + + $context->addViolation('Message %param%', array('%param%' => 'value')); + }; - $this->validator->validate($entity, new Traverse()); + $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validateObject($traversable, 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + $this->assertSame('Message value', $violations[0]->getMessage()); + $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); + $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); + $this->assertSame('[key]', $violations[0]->getPropertyPath()); + $this->assertSame($traversable, $violations[0]->getRoot()); + $this->assertSame($entity, $violations[0]->getInvalidValue()); + $this->assertNull($violations[0]->getMessagePluralization()); + $this->assertNull($violations[0]->getCode()); } /** diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php index 956537d22f90a..d3b5f1ba6cb20 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php @@ -93,6 +93,29 @@ public function testTraversableTraverseDisabled() $this->validator->validate($traversable, 'Group'); } + /** + * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException + */ + public function testRecursiveTraversableRecursiveTraversalDisabled() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => $entity)), + )); + + $callback = function () use ($test) { + $test->fail('Should not be called'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $this->validator->validate($traversable, 'Group'); + } + public function testValidateInContext() { $test = $this; diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index f4751e47c42a6..c413f0a44afb4 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -339,30 +339,7 @@ public function testTraversable() $this->assertNull($violations[0]->getCode()); } - /** - * @expectedException \Symfony\Component\Validator\Exception\NoSuchMetadataException - */ - public function testRecursiveTraversableRecursiveTraversalDisabled() - { - $test = $this; - $entity = new Entity(); - $traversable = new \ArrayIterator(array( - 2 => new \ArrayIterator(array('key' => $entity)), - )); - - $callback = function () use ($test) { - $test->fail('Should not be called'); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - $this->validateObjects($traversable, 'Group'); - } - - public function testRecursiveTraversableRecursiveTraversalEnabled() + public function testRecursiveTraversable() { $test = $this; $entity = new Entity(); diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index 2027240d29790..74850d8ce638b 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -14,11 +14,15 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\NoSuchMetadataException; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\GenericMetadata; +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\PropertyNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; @@ -110,14 +114,26 @@ public function validateObject($object, $groups = null) return $this; } - public function validateObjects($objects, $groups = null, $deep = false) + public function validateObjects($objects, $groups = null) { - $constraint = new Traverse(array( - 'traverse' => true, - 'deep' => $deep, - )); + if (!is_array($objects) && !$objects instanceof \Traversable) { + throw new UnexpectedTypeException($objects, 'array or \Traversable'); + } + + $traversalStrategy = TraversalStrategy::TRAVERSE; + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - return $this->validate($objects, $constraint, $groups); + $node = new CollectionNode( + $objects, + $this->defaultPropertyPath, + $groups, + null, + $traversalStrategy + ); + + $this->nodeTraverser->traverse(array($node), $this->context); + + return $this; } public function validateProperty($object, $propertyName, $groups = null) diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php index 4e1a9a680b928..23aece1f53101 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php @@ -51,7 +51,7 @@ public function validate($value, $constraints, $groups = null); */ public function validateObject($object, $groups = null); - public function validateObjects($objects, $groups = null, $deep = false); + public function validateObjects($objects, $groups = null); /** * Validates a property of a value against its current value. diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 73d6948e3ece4..eb17013675882 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -29,24 +29,12 @@ public function validate($value, $groups = null, $traverse = false, $deep = fals return parent::validate($value, $groups, $traverse); } - if (is_array($value)) { - $constraint = new Traverse(array( - 'traverse' => true, - 'deep' => $deep, - )); + if (is_array($value) || ($traverse && $value instanceof \Traversable)) { + $constraint = new Valid(array('deep' => $deep)); return parent::validate($value, $constraint, $groups); } - if ($traverse && $value instanceof \Traversable) { - $constraints = array( - new Valid(), - new Traverse(array('traverse' => true, 'deep' => $deep)), - ); - - return parent::validate($value, $constraints, $groups); - } - return $this->validateObject($value, $groups); } diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index fe7b80c817cda..1484dae715ac4 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -98,10 +98,10 @@ public function validateObject($object, $groups = null) ->getViolations(); } - public function validateObjects($objects, $groups = null, $deep = false) + public function validateObjects($objects, $groups = null) { return $this->startContext($objects) - ->validateObjects($objects, $groups, $deep) + ->validateObjects($objects, $groups) ->getViolations(); } diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index 5eccf3f739491..95a1342af4fb4 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -47,7 +47,7 @@ public function validate($value, $constraints, $groups = null); */ public function validateObject($object, $groups = null); - public function validateObjects($objects, $groups = null, $deep = false); + public function validateObjects($objects, $groups = null); /** * Validates a property of a value against its current value. From 08172bfe7bd4ef4b22e129c953552179ed7b4c84 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 19:57:14 +0100 Subject: [PATCH 38/86] [Validator] Merged validate(), validateObject() and validateObjects() to simplify usage --- .../Component/Validator/Constraints/Valid.php | 2 +- .../Context/LegacyExecutionContext.php | 2 +- .../Tests/Validator/Abstract2Dot5ApiTest.php | 74 +++------------ .../Tests/Validator/AbstractLegacyApiTest.php | 21 ++--- .../Tests/Validator/AbstractValidatorTest.php | 91 +++++++------------ .../Validator/ContextualValidator.php | 56 +----------- .../ContextualValidatorInterface.php | 20 +--- .../Validator/Validator/LegacyValidator.php | 29 ++++-- .../Validator/Validator/Validator.php | 16 +--- .../Validator/ValidatorInterface.php | 21 +---- 10 files changed, 93 insertions(+), 239 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/Valid.php b/src/Symfony/Component/Validator/Constraints/Valid.php index 218f265f4f166..6ad09624b57d4 100644 --- a/src/Symfony/Component/Validator/Constraints/Valid.php +++ b/src/Symfony/Component/Validator/Constraints/Valid.php @@ -25,7 +25,7 @@ class Valid extends Constraint { public $traverse = true; - public $deep = false; + public $deep = true; public function __construct($options = null) { diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index 2d85ab7905a19..7e3b5971aacdb 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -143,7 +143,7 @@ public function validate($value, $subPath = '', $groups = null, $traverse = fals ->getValidator() ->inContext($this) ->atPath($subPath) - ->validateObject($value, $groups) + ->validate($value, null, $groups) ; } diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 04f42480cedcb..973283c84e88c 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -50,21 +50,11 @@ protected function setUp() $this->validator = $this->createValidator($this->metadataFactory); } - protected function validate($value, $constraints, $groups = null) + protected function validate($value, $constraints = null, $groups = null) { return $this->validator->validate($value, $constraints, $groups); } - protected function validateObject($object, $groups = null) - { - return $this->validator->validateObject($object, $groups); - } - - protected function validateObjects($objects, $groups = null, $deep = false) - { - return $this->validator->validateObjects($objects, $groups, $deep); - } - protected function validateProperty($object, $propertyName, $groups = null) { return $this->validator->validateProperty($object, $propertyName, $groups); @@ -88,7 +78,7 @@ public function testNoDuplicateValidationIfConstraintInMultipleGroups() 'groups' => array('Group 1', 'Group 2'), ))); - $violations = $this->validator->validateObject($entity, array('Group 1', 'Group 2')); + $violations = $this->validator->validate($entity, new Valid(), array('Group 1', 'Group 2')); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -119,7 +109,7 @@ public function testGroupSequenceAbortsAfterFailedGroup() ))); $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3')); - $violations = $this->validator->validateObject($entity, $sequence); + $violations = $this->validator->validate($entity, new Valid(), $sequence); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -149,7 +139,7 @@ public function testGroupSequenceIncludesReferences() ))); $sequence = new GroupSequence(array('Group 1', 'Entity')); - $violations = $this->validator->validateObject($entity, $sequence); + $violations = $this->validator->validate($entity, new Valid(), $sequence); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -167,7 +157,7 @@ public function testValidateInSeparateContext() ->getValidator() // Since the validator is not context aware, the group must // be passed explicitly - ->validateObject($value->reference, 'Group') + ->validate($value->reference, new Valid(), 'Group') ; /** @var ConstraintViolationInterface[] $violations */ @@ -208,7 +198,7 @@ public function testValidateInSeparateContext() 'groups' => 'Group', ))); - $violations = $this->validator->validateObject($entity, 'Group'); + $violations = $this->validator->validate($entity, new Valid(), 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -226,7 +216,7 @@ public function testValidateInContext() ->getValidator() ->inContext($context) ->atPath('subpath') - ->validateObject($value->reference) + ->validate($value->reference) ; }; @@ -252,7 +242,7 @@ public function testValidateInContext() 'groups' => 'Group', ))); - $violations = $this->validator->validateObject($entity, 'Group'); + $violations = $this->validator->validate($entity, new Valid(), 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -277,7 +267,7 @@ public function testValidateArrayInContext() ->getValidator() ->inContext($context) ->atPath('subpath') - ->validateObjects(array('key' => $value->reference)) + ->validate(array('key' => $value->reference)) ; }; @@ -303,7 +293,7 @@ public function testValidateArrayInContext() 'groups' => 'Group', ))); - $violations = $this->validator->validateObject($entity, 'Group'); + $violations = $this->validator->validate($entity, new Valid(), 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -317,44 +307,6 @@ public function testValidateArrayInContext() $this->assertNull($violations[0]->getCode()); } - public function testValidateAcceptsValid() - { - $test = $this; - $entity = new Entity(); - - $callback = function ($value, ExecutionContextInterface $context) use ($test, $entity) { - $test->assertSame($test::ENTITY_CLASS, $context->getClassName()); - $test->assertNull($context->getPropertyName()); - $test->assertSame('', $context->getPropertyPath()); - $test->assertSame('Group', $context->getGroup()); - $test->assertSame($test->metadata, $context->getMetadata()); - $test->assertSame($entity, $context->getRoot()); - $test->assertSame($entity, $context->getValue()); - $test->assertSame($entity, $value); - - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => 'Group', - ))); - - // This is the same as when calling validateObject() - $violations = $this->validator->validate($entity, new Valid(), 'Group'); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - $this->assertSame('Message value', $violations[0]->getMessage()); - $this->assertSame('Message %param%', $violations[0]->getMessageTemplate()); - $this->assertSame(array('%param%' => 'value'), $violations[0]->getMessageParameters()); - $this->assertSame('', $violations[0]->getPropertyPath()); - $this->assertSame($entity, $violations[0]->getRoot()); - $this->assertSame($entity, $violations[0]->getInvalidValue()); - $this->assertNull($violations[0]->getMessagePluralization()); - $this->assertNull($violations[0]->getCode()); - } - public function testTraverseTraversableByDefault() { $test = $this; @@ -380,7 +332,7 @@ public function testTraverseTraversableByDefault() 'groups' => 'Group', ))); - $violations = $this->validateObject($traversable, 'Group'); + $violations = $this->validate($traversable, new Valid(), 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -403,7 +355,7 @@ public function testExpectTraversableIfTraverseOnClass() $this->metadata->addConstraint(new Traverse()); - $this->validator->validateObject($entity); + $this->validator->validate($entity); } public function testAddCustomizedViolation() @@ -421,7 +373,7 @@ public function testAddCustomizedViolation() $this->metadata->addConstraint(new Callback($callback)); - $violations = $this->validator->validateObject($entity); + $violations = $this->validator->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php index d3b5f1ba6cb20..7bca5de9b6866 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Tests\Validator; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ConstraintViolationInterface; @@ -47,19 +48,17 @@ protected function setUp() $this->validator = $this->createValidator($this->metadataFactory); } - protected function validate($value, $constraints, $groups = null) + protected function validate($value, $constraints = null, $groups = null) { - return $this->validator->validateValue($value, $constraints, $groups); - } + if (null === $constraints) { + $constraints = new Valid(); + } - protected function validateObject($object, $groups = null) - { - return $this->validator->validate($object, $groups); - } + if ($constraints instanceof Valid) { + return $this->validator->validate($value, $groups, $constraints->traverse, $constraints->deep); + } - protected function validateObjects($objects, $groups = null, $deep = false) - { - return $this->validator->validate($objects, $groups, true, $deep); + return $this->validator->validateValue($value, $constraints, $groups); } protected function validateProperty($object, $propertyName, $groups = null) @@ -226,7 +225,7 @@ public function testAddCustomizedViolation() $this->metadata->addConstraint(new Callback($callback)); - $violations = $this->validateObject($entity); + $violations = $this->validator->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index c413f0a44afb4..4c830d25d4bff 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -63,11 +63,7 @@ protected function tearDown() $this->referenceMetadata = null; } - abstract protected function validate($value, $constraints, $groups = null); - - abstract protected function validateObject($object, $groups = null); - - abstract protected function validateObjects($objects, $groups = null, $deep = false); + abstract protected function validate($value, $constraints = null, $groups = null); abstract protected function validateProperty($object, $propertyName, $groups = null); @@ -131,7 +127,7 @@ public function testClassConstraint() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -171,7 +167,7 @@ public function testPropertyConstraint() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -211,7 +207,7 @@ public function testGetterConstraint() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -249,7 +245,7 @@ public function testArray() 'groups' => 'Group', ))); - $violations = $this->validateObjects($array, 'Group'); + $violations = $this->validate($array, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -287,7 +283,7 @@ public function testRecursiveArray() 'groups' => 'Group', ))); - $violations = $this->validateObjects($array, 'Group'); + $violations = $this->validate($array, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -325,7 +321,7 @@ public function testTraversable() 'groups' => 'Group', ))); - $violations = $this->validateObjects($traversable, 'Group'); + $violations = $this->validate($traversable, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -365,7 +361,7 @@ public function testRecursiveTraversable() 'groups' => 'Group', ))); - $violations = $this->validateObjects($traversable, 'Group', true); + $violations = $this->validate($traversable, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -404,7 +400,7 @@ public function testReferenceClassConstraint() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -446,7 +442,7 @@ public function testReferencePropertyConstraint() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -488,7 +484,7 @@ public function testReferenceGetterConstraint() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -509,7 +505,7 @@ public function testsIgnoreNullReference() $this->metadata->addPropertyConstraint('reference', new Valid()); - $violations = $this->validateObject($entity); + $violations = $this->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -525,7 +521,7 @@ public function testFailOnScalarReferences() $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->validateObject($entity); + $this->validate($entity); } public function testArrayReference() @@ -553,7 +549,7 @@ public function testArrayReference() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -593,7 +589,7 @@ public function testRecursiveArrayReference() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -621,7 +617,7 @@ public function testArrayTraversalCannotBeDisabled() ))); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validateObject($entity); + $violations = $this->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -641,7 +637,7 @@ public function testRecursiveArrayTraversalCannotBeDisabled() ))); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validateObject($entity); + $violations = $this->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -654,7 +650,7 @@ public function testIgnoreScalarsDuringArrayTraversal() $this->metadata->addPropertyConstraint('reference', new Valid()); - $violations = $this->validateObject($entity); + $violations = $this->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -667,7 +663,7 @@ public function testIgnoreNullDuringArrayTraversal() $this->metadata->addPropertyConstraint('reference', new Valid()); - $violations = $this->validateObject($entity); + $violations = $this->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -698,7 +694,7 @@ public function testTraversableReference() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -727,7 +723,7 @@ public function testDisableTraversableTraversal() ))); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validateObject($entity); + $violations = $this->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(0, $violations); @@ -745,28 +741,7 @@ public function testMetadataMustExistIfTraversalIsDisabled() 'traverse' => false, ))); - $this->validateObject($entity, 'Default', ''); - } - - public function testNoRecursiveTraversableTraversal() - { - $entity = new Entity(); - $entity->reference = new \ArrayIterator(array( - 2 => new \ArrayIterator(array('key' => new Reference())), - )); - - $callback = function ($value, ExecutionContextInterface $context) { - $context->addViolation('Message %param%', array('%param%' => 'value')); - }; - - $this->metadataFactory->addMetadata(new ClassMetadata('ArrayIterator')); - $this->metadata->addPropertyConstraint('reference', new Valid()); - $this->referenceMetadata->addConstraint(new Callback($callback)); - - $violations = $this->validateObject($entity); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(0, $violations); + $this->validate($entity); } public function testEnableRecursiveTraversableTraversal() @@ -798,7 +773,7 @@ public function testEnableRecursiveTraversableTraversal() 'groups' => 'Group', ))); - $violations = $this->validateObject($entity, 'Group'); + $violations = $this->validate($entity, null, 'Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -954,7 +929,7 @@ public function testValidateObjectOnlyOncePerGroup() $this->metadata->addPropertyConstraint('reference2', new Valid()); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validateObject($entity); + $violations = $this->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -974,7 +949,7 @@ public function testValidateDifferentObjectsSeparately() $this->metadata->addPropertyConstraint('reference2', new Valid()); $this->referenceMetadata->addConstraint(new Callback($callback)); - $violations = $this->validateObject($entity); + $violations = $this->validate($entity); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(2, $violations); @@ -997,7 +972,7 @@ public function testValidateSingleGroup() 'groups' => 'Group 2', ))); - $violations = $this->validateObject($entity, 'Group 2'); + $violations = $this->validate($entity, null, 'Group 2'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1020,7 +995,7 @@ public function testValidateMultipleGroups() 'groups' => 'Group 2', ))); - $violations = $this->validateObject($entity, array('Group 1', 'Group 2')); + $violations = $this->validate($entity, null, array('Group 1', 'Group 2')); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(2, $violations); @@ -1053,7 +1028,7 @@ public function testReplaceDefaultGroupByGroupSequenceObject() $sequence = new GroupSequence(array('Group 1', 'Group 2', 'Group 3', 'Entity')); $this->metadata->setGroupSequence($sequence); - $violations = $this->validateObject($entity, 'Default'); + $violations = $this->validate($entity, null, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1087,7 +1062,7 @@ public function testReplaceDefaultGroupByGroupSequenceArray() $sequence = array('Group 1', 'Group 2', 'Group 3', 'Entity'); $this->metadata->setGroupSequence($sequence); - $violations = $this->validateObject($entity, 'Default'); + $violations = $this->validate($entity, null, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1119,7 +1094,7 @@ public function testPropagateDefaultGroupToReferenceWhenReplacingDefaultGroup() $sequence = new GroupSequence(array('Group 1', 'Entity')); $this->metadata->setGroupSequence($sequence); - $violations = $this->validateObject($entity, 'Default'); + $violations = $this->validate($entity, null, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1149,7 +1124,7 @@ public function testValidateCustomGroupWhenDefaultGroupWasReplaced() $sequence = new GroupSequence(array('Group 1', 'Entity')); $this->metadata->setGroupSequence($sequence); - $violations = $this->validateObject($entity, 'Other Group'); + $violations = $this->validate($entity, null, 'Other Group'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1185,7 +1160,7 @@ public function testReplaceDefaultGroupWithObjectFromGroupSequenceProvider() $this->metadataFactory->addMetadata($metadata); - $violations = $this->validateObject($entity, 'Default'); + $violations = $this->validate($entity, null, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); @@ -1221,7 +1196,7 @@ public function testReplaceDefaultGroupWithArrayFromGroupSequenceProvider() $this->metadataFactory->addMetadata($metadata); - $violations = $this->validateObject($entity, 'Default'); + $violations = $this->validate($entity, null, 'Default'); /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index 74850d8ce638b..e9b208eae316e 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Traverse; +use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; @@ -65,9 +66,11 @@ public function atPath($subPath) return $this; } - public function validate($value, $constraints, $groups = null) + public function validate($value, $constraints = null, $groups = null) { - if (!is_array($constraints)) { + if (null === $constraints) { + $constraints = array(new Valid()); + } elseif (!is_array($constraints)) { $constraints = array($constraints); } @@ -87,55 +90,6 @@ public function validate($value, $constraints, $groups = null) return $this; } - public function validateObject($object, $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) - )); - } - - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - - $node = new ClassNode( - $object, - $classMetadata, - $this->defaultPropertyPath, - $groups - ); - - $this->nodeTraverser->traverse(array($node), $this->context); - - return $this; - } - - public function validateObjects($objects, $groups = null) - { - if (!is_array($objects) && !$objects instanceof \Traversable) { - throw new UnexpectedTypeException($objects, 'array or \Traversable'); - } - - $traversalStrategy = TraversalStrategy::TRAVERSE; - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - - $node = new CollectionNode( - $objects, - $this->defaultPropertyPath, - $groups, - null, - $traversalStrategy - ); - - $this->nodeTraverser->traverse(array($node), $this->context); - - return $this; - } - public function validateProperty($object, $propertyName, $groups = null) { $classMetadata = $this->metadataFactory->getMetadataFor($object); diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php index 23aece1f53101..e384228d7c977 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php @@ -30,28 +30,16 @@ public function atPath($subPath); /** * Validates a value against a constraint or a list of constraints. * + * If no constraint is passed, the constraint + * {@link \Symfony\Component\Validator\Constraints\Valid} is assumed. + * * @param mixed $value The value to validate. * @param Constraint|Constraint[] $constraints The constraint(s) to validate against. * @param array|null $groups The validation groups to validate. * * @return ContextualValidatorInterface This validator */ - public function validate($value, $constraints, $groups = null); - - /** - * Validates a value. - * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. - * - * @param mixed $object The value to validate - * @param array|null $groups The validation groups to validate. - * - * @return ContextualValidatorInterface This validator - */ - public function validateObject($object, $groups = null); - - public function validateObjects($objects, $groups = null); + public function validate($value, $constraints = null, $groups = null); /** * Validates a property of a value against its current value. diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index eb17013675882..acebb5824da70 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; @@ -24,18 +25,20 @@ class LegacyValidator extends Validator implements LegacyValidatorInterface { public function validate($value, $groups = null, $traverse = false, $deep = false) { - // Use new signature if constraints are given in the second argument - if (func_num_args() <= 3 && ($groups instanceof Constraint || (is_array($groups) && current($groups) instanceof Constraint))) { - return parent::validate($value, $groups, $traverse); - } + $numArgs = func_num_args(); - if (is_array($value) || ($traverse && $value instanceof \Traversable)) { - $constraint = new Valid(array('deep' => $deep)); + // Use new signature if constraints are given in the second argument + if (self::testConstraints($groups) && ($numArgs < 2 || 3 === $numArgs && self::testGroups($traverse))) { + // Rename to avoid total confusion ;) + $constraints = $groups; + $groups = $traverse; - return parent::validate($value, $constraint, $groups); + return parent::validate($value, $constraints, $groups); } - return $this->validateObject($value, $groups); + $constraint = new Valid(array('traverse' => $traverse, 'deep' => $deep)); + + return parent::validate($value, $constraint, $groups); } public function validateValue($value, $constraints, $groups = null) @@ -47,4 +50,14 @@ public function getMetadataFactory() { return $this->metadataFactory; } + + private static function testConstraints($constraints) + { + return null === $constraints || $constraints instanceof Constraint || (is_array($constraints) && current($constraints) instanceof Constraint); + } + + private static function testGroups($groups) + { + return null === $groups || is_string($groups) || $groups instanceof GroupSequence || (is_array($groups) && (is_string(current($groups)) || current($groups) instanceof GroupSequence)); + } } diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index 1484dae715ac4..fad080155af5f 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -84,27 +84,13 @@ public function hasMetadataFor($object) return $this->metadataFactory->hasMetadataFor($object); } - public function validate($value, $constraints, $groups = null) + public function validate($value, $constraints = null, $groups = null) { return $this->startContext($value) ->validate($value, $constraints, $groups) ->getViolations(); } - public function validateObject($object, $groups = null) - { - return $this->startContext($object) - ->validateObject($object, $groups) - ->getViolations(); - } - - public function validateObjects($objects, $groups = null) - { - return $this->startContext($objects) - ->validateObjects($objects, $groups) - ->getViolations(); - } - public function validateProperty($object, $propertyName, $groups = null) { return $this->startContext($object) diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index 95a1342af4fb4..124514a1ffaf7 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -24,6 +24,9 @@ interface ValidatorInterface /** * Validates a value against a constraint or a list of constraints. * + * If no constraint is passed, the constraint + * {@link \Symfony\Component\Validator\Constraints\Valid} is assumed. + * * @param mixed $value The value to validate. * @param Constraint|Constraint[] $constraints The constraint(s) to validate against. * @param array|null $groups The validation groups to validate. @@ -31,23 +34,7 @@ interface ValidatorInterface * @return ConstraintViolationListInterface A list of constraint violations. If the * list is empty, validation succeeded. */ - public function validate($value, $constraints, $groups = null); - - /** - * Validates a value. - * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. - * - * @param mixed $object The value to validate - * @param array|null $groups The validation groups to validate. - * - * @return ConstraintViolationListInterface A list of constraint violations. If the - * list is empty, validation succeeded. - */ - public function validateObject($object, $groups = null); - - public function validateObjects($objects, $groups = null); + public function validate($value, $constraints = null, $groups = null); /** * Validates a property of a value against its current value. From aeb68228b1906ea82d77e0dfb6f333ff9285fe6c Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 20:05:44 +0100 Subject: [PATCH 39/86] [Validator] Improved visitor names --- .../Validator/NodeTraverser/NodeTraverser.php | 4 +--- ...RefresherVisitor.php => ContextUpdateVisitor.php} | 4 ++-- ...Visitor.php => GroupSequenceResolvingVisitor.php} | 2 +- ...alidatorVisitor.php => NodeValidationVisitor.php} | 2 +- ...erVisitor.php => ObjectInitializationVisitor.php} | 2 +- .../Tests/Validator/LegacyValidator2Dot5ApiTest.php | 12 ++++++------ .../Tests/Validator/LegacyValidatorLegacyApiTest.php | 12 ++++++------ .../Tests/Validator/Validator2Dot5ApiTest.php | 12 ++++++------ 8 files changed, 24 insertions(+), 26 deletions(-) rename src/Symfony/Component/Validator/NodeVisitor/{ContextRefresherVisitor.php => ContextUpdateVisitor.php} (88%) rename src/Symfony/Component/Validator/NodeVisitor/{GroupSequenceResolverVisitor.php => GroupSequenceResolvingVisitor.php} (96%) rename src/Symfony/Component/Validator/NodeVisitor/{NodeValidatorVisitor.php => NodeValidationVisitor.php} (98%) rename src/Symfony/Component/Validator/NodeVisitor/{ObjectInitializerVisitor.php => ObjectInitializationVisitor.php} (96%) diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index 44ab2eaa27b72..1c1e5261e8cf9 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -12,11 +12,9 @@ namespace Symfony\Component\Validator\NodeTraverser; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Mapping\CascadingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; -use Symfony\Component\Validator\Mapping\CollectionMetadata; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; @@ -132,7 +130,7 @@ private function traverseNode(Node $node, Traversal $traversal) return; } - // The "cascadedGroups" property is set by the NodeValidatorVisitor when + // The "cascadedGroups" property is set by the NodeValidationVisitor when // traversing group sequences $cascadedGroups = null !== $node->cascadedGroups ? $node->cascadedGroups diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php similarity index 88% rename from src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php rename to src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php index b28b2f542fdad..4fb7f1b33a428 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ContextRefresherVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php @@ -22,13 +22,13 @@ * @since 2.5 * @author Bernhard Schussek */ -class ContextRefresherVisitor extends AbstractVisitor +class ContextUpdateVisitor extends AbstractVisitor { public function visit(Node $node, ExecutionContextInterface $context) { if (!$context instanceof NodeObserverInterface) { throw new RuntimeException(sprintf( - 'The ContextRefresherVisitor only supports instances of class '. + 'The ContextUpdateVisitor only supports instances of class '. '"Symfony\Component\Validator\NodeVisitor\NodeObserverInterface". '. 'An instance of class "%s" was given.', get_class($context) diff --git a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolvingVisitor.php similarity index 96% rename from src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php rename to src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolvingVisitor.php index 6b9330ec8d317..2d9b68737a1d0 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolverVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolvingVisitor.php @@ -21,7 +21,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class GroupSequenceResolverVisitor extends AbstractVisitor +class GroupSequenceResolvingVisitor extends AbstractVisitor { public function visit(Node $node, ExecutionContextInterface $context) { diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php similarity index 98% rename from src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php rename to src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index 6cab8a3caced1..c688295da6ae4 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidatorVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -25,7 +25,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class NodeValidatorVisitor extends AbstractVisitor implements GroupManagerInterface +class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInterface { private $validatedObjects = array(); diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php similarity index 96% rename from src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php rename to src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php index 01a2a07b5eb6b..05226ac6d359b 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializerVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php @@ -20,7 +20,7 @@ * @since %%NextVersion%% * @author Bernhard Schussek */ -class ObjectInitializerVisitor extends AbstractVisitor +class ObjectInitializationVisitor extends AbstractVisitor { /** * @var ObjectInitializerInterface[] diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index 600a297947f02..e61daf9f939cd 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -15,9 +15,9 @@ use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\ContextRefresherVisitor; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolverVisitor; -use Symfony\Component\Validator\NodeVisitor\NodeValidatorVisitor; +use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; +use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; @@ -26,11 +26,11 @@ class LegacyValidator2Dot5ApiTest extends Abstract2Dot5ApiTest protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); - $nodeValidator = new NodeValidatorVisitor($nodeTraverser, new ConstraintValidatorFactory()); + $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolverVisitor(); - $contextRefresher = new ContextRefresherVisitor(); + $groupSequenceResolver = new GroupSequenceResolvingVisitor(); + $contextRefresher = new ContextUpdateVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); $nodeTraverser->addVisitor($contextRefresher); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index fc2fe0bb38203..672f2ebf42c9a 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -15,9 +15,9 @@ use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\ContextRefresherVisitor; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolverVisitor; -use Symfony\Component\Validator\NodeVisitor\NodeValidatorVisitor; +use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; +use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; @@ -26,11 +26,11 @@ class LegacyValidatorLegacyApiTest extends AbstractLegacyApiTest protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); - $nodeValidator = new NodeValidatorVisitor($nodeTraverser, new ConstraintValidatorFactory()); + $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolverVisitor(); - $contextRefresher = new ContextRefresherVisitor(); + $groupSequenceResolver = new GroupSequenceResolvingVisitor(); + $contextRefresher = new ContextUpdateVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); $nodeTraverser->addVisitor($contextRefresher); diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php index e3f92b879930a..b49c380c98708 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php @@ -15,9 +15,9 @@ use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\ExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeVisitor\ContextRefresherVisitor; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolverVisitor; -use Symfony\Component\Validator\NodeVisitor\NodeValidatorVisitor; +use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; +use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; +use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeTraverser\NodeTraverser; use Symfony\Component\Validator\Validator\Validator; @@ -26,11 +26,11 @@ class Validator2Dot5ApiTest extends Abstract2Dot5ApiTest protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); - $nodeValidator = new NodeValidatorVisitor($nodeTraverser, new ConstraintValidatorFactory()); + $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new ExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new Validator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolverVisitor(); - $contextRefresher = new ContextRefresherVisitor(); + $groupSequenceResolver = new GroupSequenceResolvingVisitor(); + $contextRefresher = new ContextUpdateVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); $nodeTraverser->addVisitor($contextRefresher); From 416137165eaa7d48425ead91158b726899857a47 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 20:12:57 +0100 Subject: [PATCH 40/86] [Validator] Removed unused use statements --- src/Symfony/Component/Validator/Mapping/GenericMetadata.php | 1 - src/Symfony/Component/Validator/Node/CollectionNode.php | 2 -- .../Validator/Tests/Validator/AbstractLegacyApiTest.php | 1 - .../Component/Validator/Validator/ContextualValidator.php | 6 ------ .../Component/Validator/Validator/LegacyValidator.php | 1 - 5 files changed, 11 deletions(-) diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index 69064a2cf494f..a87903dec0258 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; /** diff --git a/src/Symfony/Component/Validator/Node/CollectionNode.php b/src/Symfony/Component/Validator/Node/CollectionNode.php index 2df442a4fc699..763ed63072c00 100644 --- a/src/Symfony/Component/Validator/Node/CollectionNode.php +++ b/src/Symfony/Component/Validator/Node/CollectionNode.php @@ -12,8 +12,6 @@ namespace Symfony\Component\Validator\Node; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; -use Symfony\Component\Validator\Exception\UnexpectedTypeException; -use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Mapping\TraversalStrategy; /** diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php index 7bca5de9b6866..386c250a285e6 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Validator\Tests\Validator; -use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ConstraintViolationInterface; diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index e9b208eae316e..cd969cc5b152e 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -12,18 +12,12 @@ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Exception\NoSuchMetadataException; -use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\GenericMetadata; -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\PropertyNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index acebb5824da70..5bc570737625e 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -13,7 +13,6 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\GroupSequence; -use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; From 76d8c9a1f718c902adb7f886107db42e9a1ffc97 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 20:15:27 +0100 Subject: [PATCH 41/86] [Validator] Fixed typos --- .../Validator/NodeTraverser/NodeTraverser.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index 1c1e5261e8cf9..574179648061a 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -117,9 +117,9 @@ private function traverseNode(Node $node, Traversal $traversal) { // Visitors have two possibilities to influence the traversal: // - // 1. If a visitor's enterNode() method returns false, the traversal is + // 1. If a visitor's visit() method returns false, the traversal is // skipped entirely. - // 2. If a visitor's enterNode() method removes a group from the node, + // 2. If a visitor's visit() method removes a group from the node, // that group will be skipped in the subtree of that node. if (false === $this->visit($node, $traversal->context)) { @@ -197,9 +197,9 @@ private function traverseClassNode(ClassNode $node, Traversal $traversal) { // Visitors have two possibilities to influence the traversal: // - // 1. If a visitor's enterNode() method returns false, the traversal is + // 1. If a visitor's visit() method returns false, the traversal is // skipped entirely. - // 2. If a visitor's enterNode() method removes a group from the node, + // 2. If a visitor's visit() method removes a group from the node, // that group will be skipped in the subtree of that node. if (false === $this->visit($node, $traversal->context)) { @@ -259,9 +259,9 @@ private function traverseCollectionNode(CollectionNode $node, Traversal $travers { // Visitors have two possibilities to influence the traversal: // - // 1. If a visitor's enterNode() method returns false, the traversal is + // 1. If a visitor's visit() method returns false, the traversal is // skipped entirely. - // 2. If a visitor's enterNode() method removes a group from the node, + // 2. If a visitor's visit() method removes a group from the node, // that group will be skipped in the subtree of that node. if (false === $this->visit($node, $traversal->context)) { From 778ec2458b802b5a5569b5db13c0cdc2220870b3 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 20:23:55 +0100 Subject: [PATCH 42/86] [Validator] Removed helper class Traversal --- .../Validator/NodeTraverser/NodeTraverser.php | 46 +++++++++---------- .../Validator/NodeTraverser/Traversal.php | 31 ------------- 2 files changed, 23 insertions(+), 54 deletions(-) delete mode 100644 src/Symfony/Component/Validator/NodeTraverser/Traversal.php diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php index 574179648061a..be9f2d2b880e8 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php @@ -74,20 +74,20 @@ public function traverse(array $nodes, ExecutionContextInterface $context) } } - $traversal = new Traversal($context); + $nodeQueue = new \SplQueue(); foreach ($nodes as $node) { - $traversal->nodeQueue->enqueue($node); + $nodeQueue->enqueue($node); - while (!$traversal->nodeQueue->isEmpty()) { - $node = $traversal->nodeQueue->dequeue(); + while (!$nodeQueue->isEmpty()) { + $node = $nodeQueue->dequeue(); if ($node instanceof ClassNode) { - $this->traverseClassNode($node, $traversal); + $this->traverseClassNode($node, $nodeQueue, $context); } elseif ($node instanceof CollectionNode) { - $this->traverseCollectionNode($node, $traversal); + $this->traverseCollectionNode($node, $nodeQueue, $context); } else { - $this->traverseNode($node, $traversal); + $this->traverseNode($node, $nodeQueue, $context); } } } @@ -113,7 +113,7 @@ private function visit(Node $node, ExecutionContextInterface $context) return true; } - private function traverseNode(Node $node, Traversal $traversal) + private function traverseNode(Node $node, \SplQueue $nodeQueue, ExecutionContextInterface $context) { // Visitors have two possibilities to influence the traversal: // @@ -122,7 +122,7 @@ private function traverseNode(Node $node, Traversal $traversal) // 2. If a visitor's visit() method removes a group from the node, // that group will be skipped in the subtree of that node. - if (false === $this->visit($node, $traversal->context)) { + if (false === $this->visit($node, $context)) { return; } @@ -147,7 +147,7 @@ private function traverseNode(Node $node, Traversal $traversal) // Arrays are always traversed, independent of the specified // traversal strategy // (BC with Symfony < 2.5) - $traversal->nodeQueue->enqueue(new CollectionNode( + $nodeQueue->enqueue(new CollectionNode( $node->value, $node->propertyPath, $cascadedGroups, @@ -167,7 +167,7 @@ private function traverseNode(Node $node, Traversal $traversal) $node->propertyPath, $cascadedGroups, $traversalStrategy, - $traversal + $nodeQueue ); return; @@ -184,7 +184,7 @@ private function traverseNode(Node $node, Traversal $traversal) } // If TRAVERSE, the constructor will fail if we have no Traversable - $traversal->nodeQueue->enqueue(new CollectionNode( + $nodeQueue->enqueue(new CollectionNode( $node->value, $node->propertyPath, $cascadedGroups, @@ -193,7 +193,7 @@ private function traverseNode(Node $node, Traversal $traversal) )); } - private function traverseClassNode(ClassNode $node, Traversal $traversal) + private function traverseClassNode(ClassNode $node, \SplQueue $nodeQueue, ExecutionContextInterface $context) { // Visitors have two possibilities to influence the traversal: // @@ -202,7 +202,7 @@ private function traverseClassNode(ClassNode $node, Traversal $traversal) // 2. If a visitor's visit() method removes a group from the node, // that group will be skipped in the subtree of that node. - if (false === $this->visit($node, $traversal->context)) { + if (false === $this->visit($node, $context)) { return; } @@ -212,7 +212,7 @@ private function traverseClassNode(ClassNode $node, Traversal $traversal) foreach ($node->metadata->getConstrainedProperties() as $propertyName) { foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { - $traversal->nodeQueue->enqueue(new PropertyNode( + $nodeQueue->enqueue(new PropertyNode( $node->value, $propertyMetadata->getPropertyValue($node->value), $propertyMetadata, @@ -246,7 +246,7 @@ private function traverseClassNode(ClassNode $node, Traversal $traversal) } // If TRAVERSE, the constructor will fail if we have no Traversable - $traversal->nodeQueue->enqueue(new CollectionNode( + $nodeQueue->enqueue(new CollectionNode( $node->value, $node->propertyPath, $node->groups, @@ -255,7 +255,7 @@ private function traverseClassNode(ClassNode $node, Traversal $traversal) )); } - private function traverseCollectionNode(CollectionNode $node, Traversal $traversal) + private function traverseCollectionNode(CollectionNode $node, \SplQueue $nodeQueue, ExecutionContextInterface $context) { // Visitors have two possibilities to influence the traversal: // @@ -264,7 +264,7 @@ private function traverseCollectionNode(CollectionNode $node, Traversal $travers // 2. If a visitor's visit() method removes a group from the node, // that group will be skipped in the subtree of that node. - if (false === $this->visit($node, $traversal->context)) { + if (false === $this->visit($node, $context)) { return; } @@ -285,7 +285,7 @@ private function traverseCollectionNode(CollectionNode $node, Traversal $travers // Arrays are always cascaded, independent of the specified // traversal strategy // (BC with Symfony < 2.5) - $traversal->nodeQueue->enqueue(new CollectionNode( + $nodeQueue->enqueue(new CollectionNode( $value, $node->propertyPath.'['.$key.']', $node->groups, @@ -304,13 +304,13 @@ private function traverseCollectionNode(CollectionNode $node, Traversal $travers $node->propertyPath.'['.$key.']', $node->groups, $traversalStrategy, - $traversal + $nodeQueue ); } } } - private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, Traversal $traversal) + private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, \SplQueue $nodeQueue) { try { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -319,7 +319,7 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal // error } - $traversal->nodeQueue->enqueue(new ClassNode( + $nodeQueue->enqueue(new ClassNode( $object, $classMetadata, $propertyPath, @@ -338,7 +338,7 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal throw $e; } - $traversal->nodeQueue->enqueue(new CollectionNode( + $nodeQueue->enqueue(new CollectionNode( $object, $propertyPath, $groups, diff --git a/src/Symfony/Component/Validator/NodeTraverser/Traversal.php b/src/Symfony/Component/Validator/NodeTraverser/Traversal.php deleted file mode 100644 index ab3a17e36c3b2..0000000000000 --- a/src/Symfony/Component/Validator/NodeTraverser/Traversal.php +++ /dev/null @@ -1,31 +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\NodeTraverser; - -use Symfony\Component\Validator\Context\ExecutionContextInterface; - -/** - * @since %%NextVersion%% - * @author Bernhard Schussek - */ -class Traversal -{ - public $context; - - public $nodeQueue; - - public function __construct(ExecutionContextInterface $context) - { - $this->context = $context; - $this->nodeQueue = new \SplQueue(); - } -} From 6fc6ecdd8e89b0350e5fb99fafb9f8a5a8f07431 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 20:47:32 +0100 Subject: [PATCH 43/86] [Validator] Fixed tests under PHP<5.3.9 The new implementation is disabled on those PHP versions. --- .../Mapping/ClassMetadataInterface.php | 18 ++---------------- .../Validator/Mapping/GenericMetadata.php | 9 +++++++++ .../Validator/Mapping/MemberMetadata.php | 3 +-- .../Validator/Mapping/MetadataInterface.php | 13 +++---------- .../Mapping/PropertyMetadataInterface.php | 18 ++---------------- .../Tests/Constraints/CountryValidatorTest.php | 2 +- .../Constraints/CurrencyValidatorTest.php | 2 +- .../Constraints/LanguageValidatorTest.php | 2 +- .../Tests/Context/ExecutionContextTest.php | 4 ++++ .../Tests/Validator/AbstractValidatorTest.php | 6 +++--- .../Validator/LegacyValidator2Dot5ApiTest.php | 9 +++++++++ .../Validator/LegacyValidatorLegacyApiTest.php | 9 +++++++++ .../Tests/Validator/Validator2Dot5ApiTest.php | 9 +++++++++ 13 files changed, 54 insertions(+), 50 deletions(-) diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php index fea3f7f1d7423..efcfbbaa3da0c 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php @@ -12,30 +12,16 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\ClassBasedInterface; +use Symfony\Component\Validator\PropertyMetadataContainerInterface as LegacyPropertyMetadataContainerInterface;; /** * @since %%NextVersion%% * @author Bernhard Schussek */ -interface ClassMetadataInterface extends MetadataInterface, ClassBasedInterface +interface ClassMetadataInterface extends MetadataInterface, LegacyPropertyMetadataContainerInterface, ClassBasedInterface { public function getConstrainedProperties(); - public function hasPropertyMetadata($property); - - /** - * Returns all metadata instances for the given named property. - * - * If your implementation does not support properties, simply throw an - * exception in this method (for example a BadMethodCallException). - * - * @param string $property The property name. - * - * @return PropertyMetadataInterface[] A list of metadata instances. Empty if - * no metadata exists for the property. - */ - public function getPropertyMetadata($property); - public function hasGroupSequence(); public function getGroupSequence(); diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index a87903dec0258..47eda213a4109 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\ValidationVisitorInterface; /** * @since %%NextVersion%% @@ -149,4 +150,12 @@ public function getTraversalStrategy() { return $this->traversalStrategy; } + + /** + * {@inheritdoc} + */ + public function accept(ValidationVisitorInterface $visitor, $value, $group, $propertyPath) + { + // Thanks PHP < 5.3.9 + } } diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index f23bdef04c7dd..6aa0ead2ce683 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -13,11 +13,10 @@ use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidationVisitorInterface; -use Symfony\Component\Validator\PropertyMetadataInterface as LegacyPropertyMetadataInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; -abstract class MemberMetadata extends ElementMetadata implements PropertyMetadataInterface, LegacyPropertyMetadataInterface +abstract class MemberMetadata extends ElementMetadata implements PropertyMetadataInterface { public $class; public $name; diff --git a/src/Symfony/Component/Validator/Mapping/MetadataInterface.php b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php index 540c4c9d6064c..c533e4c2bd344 100644 --- a/src/Symfony/Component/Validator/Mapping/MetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Validator\Mapping; +use Symfony\Component\Validator\MetadataInterface as LegacyMetadataInterface; + /** * A container for validation metadata. * @@ -42,17 +44,8 @@ * * @author Bernhard Schussek */ -interface MetadataInterface +interface MetadataInterface extends LegacyMetadataInterface { - /** - * 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 getCascadingStrategy(); public function getTraversalStrategy(); diff --git a/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php index 78da11b9074a9..138b1c9fb59b8 100644 --- a/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\ClassBasedInterface; +use Symfony\Component\Validator\PropertyMetadataInterface as LegacyPropertyMetadataInterface; /** * A container for validation metadata of a property. @@ -26,21 +27,6 @@ * * @see MetadataInterface */ -interface PropertyMetadataInterface extends MetadataInterface, ClassBasedInterface +interface PropertyMetadataInterface extends MetadataInterface, LegacyPropertyMetadataInterface, ClassBasedInterface { - /** - * Returns the name of the property. - * - * @return string The property name. - */ - public function getPropertyName(); - - /** - * Extracts the value of the property from the given object. - * - * @param mixed $object The object to extract the property value from. - * - * @return mixed The value of the property. - */ - public function getPropertyValue($object); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CountryValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CountryValidatorTest.php index 95851e8097ad7..151fca79f4b2d 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CountryValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CountryValidatorTest.php @@ -22,7 +22,7 @@ class CountryValidatorTest extends \PHPUnit_Framework_TestCase protected function setUp() { - IntlTestHelper::requireIntl($this); + IntlTestHelper::requireFullIntl($this); $this->context = $this->getMock('Symfony\Component\Validator\ExecutionContext', array(), array(), '', false); $this->validator = new CountryValidator(); diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CurrencyValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CurrencyValidatorTest.php index ea6c2eb43ac02..00b200e1e9463 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CurrencyValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CurrencyValidatorTest.php @@ -22,7 +22,7 @@ class CurrencyValidatorTest extends \PHPUnit_Framework_TestCase protected function setUp() { - IntlTestHelper::requireIntl($this); + IntlTestHelper::requireFullIntl($this); $this->context = $this->getMock('Symfony\Component\Validator\ExecutionContext', array(), array(), '', false); $this->validator = new CurrencyValidator(); diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LanguageValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LanguageValidatorTest.php index 3588887d74998..af5a05fbcceeb 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LanguageValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LanguageValidatorTest.php @@ -22,7 +22,7 @@ class LanguageValidatorTest extends \PHPUnit_Framework_TestCase protected function setUp() { - IntlTestHelper::requireIntl($this); + IntlTestHelper::requireFullIntl($this); $this->context = $this->getMock('Symfony\Component\Validator\ExecutionContext', array(), array(), '', false); $this->validator = new LanguageValidator(); diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php index 45d3a70c12437..26fd879e5e7b5 100644 --- a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php +++ b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php @@ -46,6 +46,10 @@ class ExecutionContextTest extends \PHPUnit_Framework_TestCase 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'); diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index 4c830d25d4bff..11e4d31eb6366 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -35,17 +35,17 @@ abstract class AbstractValidatorTest extends \PHPUnit_Framework_TestCase /** * @var FakeMetadataFactory */ - protected $metadataFactory; + public $metadataFactory; /** * @var ClassMetadata */ - protected $metadata; + public $metadata; /** * @var ClassMetadata */ - protected $referenceMetadata; + public $referenceMetadata; protected function setUp() { diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index e61daf9f939cd..7447c7ce29edf 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -23,6 +23,15 @@ class LegacyValidator2Dot5ApiTest extends Abstract2Dot5ApiTest { + protected function setUp() + { + if (version_compare(PHP_VERSION, '5.3.9', '<')) { + $this->markTestSkipped('Not supported prior to PHP 5.3.9'); + } + + parent::setUp(); + } + protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index 672f2ebf42c9a..e7fd2a8ccbc6c 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -23,6 +23,15 @@ class LegacyValidatorLegacyApiTest extends AbstractLegacyApiTest { + protected function setUp() + { + if (version_compare(PHP_VERSION, '5.3.9', '<')) { + $this->markTestSkipped('Not supported prior to PHP 5.3.9'); + } + + parent::setUp(); + } + protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php index b49c380c98708..a083903d53953 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php @@ -23,6 +23,15 @@ class Validator2Dot5ApiTest extends Abstract2Dot5ApiTest { + protected function setUp() + { + if (version_compare(PHP_VERSION, '5.3.9', '<')) { + $this->markTestSkipped('Not supported prior to PHP 5.3.9'); + } + + parent::setUp(); + } + protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); From 2936d10aa4a630c279f0203f375daa2405a37f55 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 20:53:54 +0100 Subject: [PATCH 44/86] [Validator] Removed unused use statement --- .../Component/Validator/Tests/Context/ExecutionContextTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php index 26fd879e5e7b5..0f6e51f2ffc9e 100644 --- a/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php +++ b/src/Symfony/Component/Validator/Tests/Context/ExecutionContextTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Validator\Tests\Context; use Symfony\Component\Validator\Context\ExecutionContext; -use Symfony\Component\Validator\Node\GenericNode; /** * @since 2.5 From e8fa15b27cf50af29fd1666371bfdcac851ee089 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 21:06:05 +0100 Subject: [PATCH 45/86] [Validator] Fixed the new validator API under PHP < 5.3.9 --- .../Validator/Context/ExecutionContext.php | 28 ++-- .../Context/ExecutionContextInterface.php | 122 +----------------- .../Context/LegacyExecutionContext.php | 7 +- .../Tests/Validator/Validator2Dot5ApiTest.php | 9 -- 4 files changed, 22 insertions(+), 144 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index b58d92350e440..8f7c9ee6932f3 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -16,7 +16,6 @@ use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Exception\BadMethodCallException; -use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Node\Node; @@ -33,7 +32,7 @@ * * @see ExecutionContextInterface */ -class ExecutionContext implements ExecutionContextInterface, LegacyExecutionContextInterface, NodeObserverInterface +class ExecutionContext implements ExecutionContextInterface, NodeObserverInterface { /** * @var ValidatorInterface @@ -121,6 +120,13 @@ public function addViolation($message, array $parameters = array(), $invalidValu // The parameters $invalidValue and following are ignored by the new // API, as they are not present in the new interface anymore. // You should use buildViolation() instead. + if (func_num_args() > 2) { + throw new BadMethodCallException( + 'The parameters $invalidValue, $pluralization and $code are '. + 'not supported anymore as of Symfony 2.5. Please use '. + 'buildViolation() instead or enable the legacy mode.' + ); + } $this->violations->add(new ConstraintViolation( $this->translator->trans($message, $parameters, $this->translationDomain), @@ -235,8 +241,8 @@ public function getPropertyPath($subPath = '') public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) { throw new BadMethodCallException( - 'addViolationAt() is not supported anymore in the new API. '. - 'Please use buildViolation() or enable the legacy mode.' + 'addViolationAt() is not supported anymore as of Symfony 2.5. '. + 'Please use buildViolation() instead or enable the legacy mode.' ); } @@ -246,8 +252,8 @@ public function addViolationAt($subPath, $message, array $parameters = array(), public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false) { throw new BadMethodCallException( - 'validate() is not supported anymore in the new API. '. - 'Please use getValidator() or enable the legacy mode.' + 'validate() is not supported anymore as of Symfony 2.5. '. + 'Please use getValidator() instead or enable the legacy mode.' ); } @@ -257,8 +263,8 @@ public function validate($value, $subPath = '', $groups = null, $traverse = fals public function validateValue($value, $constraints, $subPath = '', $groups = null) { throw new BadMethodCallException( - 'validateValue() is not supported anymore in the new API. '. - 'Please use getValidator() or enable the legacy mode.' + 'validateValue() is not supported anymore as of Symfony 2.5. '. + 'Please use getValidator() instead or enable the legacy mode.' ); } @@ -268,9 +274,9 @@ public function validateValue($value, $constraints, $subPath = '', $groups = nul public function getMetadataFactory() { throw new BadMethodCallException( - 'getMetadataFactory() is not supported anymore in the new API. '. - 'Please use getMetadataFor() or hasMetadataFor() or enable the '. - 'legacy mode.' + 'getMetadataFactory() is not supported anymore as of Symfony 2.5. '. + 'Please use getValidator() in combination with getMetadataFor() '. + 'or hasMetadataFor() instead or enable the legacy mode.' ); } } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index cbbc3c7447112..4fda437f6cb8f 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -13,6 +13,7 @@ use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintViolationListInterface; +use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; @@ -59,16 +60,8 @@ * @since 2.5 * @author Bernhard Schussek */ -interface ExecutionContextInterface +interface ExecutionContextInterface extends LegacyExecutionContextInterface { - /** - * Adds a violation at the current node of the validation graph. - * - * @param string $message The error message - * @param array $parameters The parameters substituted in the error message - */ - public function addViolation($message, array $parameters = array()); - /** * Returns a builder for adding a violation with extended information. * @@ -88,13 +81,6 @@ public function addViolation($message, array $parameters = array()); */ public function buildViolation($message, array $parameters = array()); - /** - * Returns the violations generated in this context. - * - * @return ConstraintViolationListInterface The constraint violations - */ - public function getViolations(); - /** * Returns the validator. * @@ -114,108 +100,4 @@ public function getViolations(); * @return ValidatorInterface */ public function getValidator(); - - /** - * Returns the root value of the object graph. - * - * The validator, when given an object, traverses the properties and - * related objects and their properties. The root of the validation is the - * object at which the traversal started. - * - * The current value is returned by {@link getValue()}. - * - * @return mixed|null The root value of the validation or null, if no value - * is currently being validated - */ - public function getRoot(); - - /** - * Returns the value that the validator is currently validating. - * - * If you want to retrieve the object that was originally passed to the - * validator, use {@link getRoot()}. - * - * @return mixed|null The currently validated value or null, if no value is - * currently being validated - */ - public function getValue(); - - /** - * Returns the metadata for the currently validated value. - * - * With the core implementation, this method returns a - * {@link Mapping\ClassMetadata} instance if the current value is an object, - * a {@link Mapping\PropertyMetadata} instance if the current value is - * the value of a property and a {@link Mapping\GetterMetadata} instance if - * the validated value is the result of a getter method. The metadata can - * also be an {@link Mapping\GenericMetadata} if the current value does not - * belong to any structural element. - * - * @return MetadataInterface|null The metadata of the currently validated - * value or null, if no value is currently - * being validated - */ - public function getMetadata(); - - /** - * Returns the validation group that is currently being validated. - * - * @return string|GroupSequence|null The current validation group or null, - * if no value is currently being - * validated - */ - public function getGroup(); - - /** - * Returns the class name of the current node. - * - * If the metadata of the current node does not implement - * {@link ClassBasedInterface}, this method returns null. - * - * @return string|null The class name or null, if no class name could be - * found - */ - public function getClassName(); - - /** - * Returns the property name of the current node. - * - * If the metadata of the current node does not implement - * {@link PropertyMetadataInterface}, this method returns null. - * - * @return string|null The property name or null, if no property name could - * be found - */ - public function getPropertyName(); - - /** - * Returns the property path to the value that the validator is currently - * validating. - * - * For example, take the following object graph: - * - * (Person)---($address: Address)---($street: string) - * - * When the Person instance is passed to the validator, the - * property path is initially empty. When the $address property - * of that person is validated, the property path is "address". When - * the $street property of the related Address instance - * is validated, the property path is "address.street". - * - * Properties of objects are prefixed with a dot in the property path. - * Indices of arrays or objects implementing the {@link \ArrayAccess} - * interface are enclosed in brackets. For example, if the property in - * the previous example is $addresses and contains an array - * of Address instances, the property path generated for the - * $street property of one of these addresses is for example - * "addresses[0].street". - * - * @param string $subPath Optional. The suffix appended to the current - * property path - * - * @return string The current property path. The result may be an empty - * string if the validator is currently validating the - * root value of the validation graph - */ - public function getPropertyPath($subPath = ''); } diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index 7e3b5971aacdb..f8ab1c9981fff 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -15,7 +15,6 @@ use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; 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; @@ -29,7 +28,7 @@ * @deprecated Implemented for backwards compatibility with Symfony < 2.5. To be * removed in 3.0. */ -class LegacyExecutionContext extends ExecutionContext implements LegacyExecutionContextInterface +class LegacyExecutionContext extends ExecutionContext { /** * Creates a new context. @@ -66,7 +65,7 @@ public function __construct(ValidatorInterface $validator, $root, GroupManagerIn */ public function addViolation($message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) { - if (func_num_args() >= 3) { + if (func_num_args() > 2) { $this ->buildViolation($message, $parameters) ->setInvalidValue($invalidValue) @@ -86,7 +85,7 @@ public function addViolation($message, array $parameters = array(), $invalidValu */ public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) { - if (func_num_args() >= 3) { + if (func_num_args() > 2) { $this ->buildViolation($message, $parameters) ->atPath($subPath) diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php index a083903d53953..b49c380c98708 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php @@ -23,15 +23,6 @@ class Validator2Dot5ApiTest extends Abstract2Dot5ApiTest { - protected function setUp() - { - if (version_compare(PHP_VERSION, '5.3.9', '<')) { - $this->markTestSkipped('Not supported prior to PHP 5.3.9'); - } - - parent::setUp(); - } - protected function createValidator(MetadataFactoryInterface $metadataFactory) { $nodeTraverser = new NodeTraverser($metadataFactory); From 85583779450d3d944e26baa967713a94af61252e Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 21:25:19 +0100 Subject: [PATCH 46/86] [Validator] Added deprecation notes --- .../Validator/ClassBasedInterface.php | 3 +++ .../Validator/Constraints/GroupSequence.php | 12 ++++----- .../Context/LegacyExecutionContext.php | 4 +-- .../Context/LegacyExecutionContextFactory.php | 4 +-- .../Component/Validator/ExecutionContext.php | 3 +++ .../Validator/ExecutionContextInterface.php | 25 +++++++++++++++++++ .../GlobalExecutionContextInterface.php | 3 +++ .../Component/Validator/MetadataInterface.php | 5 ++++ .../PropertyMetadataContainerInterface.php | 3 +++ .../Validator/PropertyMetadataInterface.php | 3 +++ .../Component/Validator/ValidationVisitor.php | 3 +++ .../Validator/ValidationVisitorInterface.php | 7 ++++++ src/Symfony/Component/Validator/Validator.php | 3 +++ .../Validator/Validator/LegacyValidator.php | 5 +++- .../Validator/ValidatorInterface.php | 15 +++++++++++ 15 files changed, 87 insertions(+), 11 deletions(-) diff --git a/src/Symfony/Component/Validator/ClassBasedInterface.php b/src/Symfony/Component/Validator/ClassBasedInterface.php index c8fa25d43d796..fe532efe51702 100644 --- a/src/Symfony/Component/Validator/ClassBasedInterface.php +++ b/src/Symfony/Component/Validator/ClassBasedInterface.php @@ -15,6 +15,9 @@ * An object backed by a PHP class. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Mapping\ClassMetadataInterface} instead. */ interface ClassBasedInterface { diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index 10dec2ef83f7b..a2406c7b6e563 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -99,7 +99,7 @@ public function __construct(array $groups) * @see \IteratorAggregate::getIterator() * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. - * To be removed in 3.0. + * To be removed in Symfony 3.0. */ public function getIterator() { @@ -114,7 +114,7 @@ public function getIterator() * @return Boolean Whether the offset exists * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. - * To be removed in 3.0. + * To be removed in Symfony 3.0. */ public function offsetExists($offset) { @@ -131,7 +131,7 @@ public function offsetExists($offset) * @throws OutOfBoundsException If the object does not exist * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. - * To be removed in 3.0. + * To be removed in Symfony 3.0. */ public function offsetGet($offset) { @@ -152,7 +152,7 @@ public function offsetGet($offset) * @param string $value The group name * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. - * To be removed in 3.0. + * To be removed in Symfony 3.0. */ public function offsetSet($offset, $value) { @@ -171,7 +171,7 @@ public function offsetSet($offset, $value) * @param integer $offset The offset * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. - * To be removed in 3.0. + * To be removed in Symfony 3.0. */ public function offsetUnset($offset) { @@ -184,7 +184,7 @@ public function offsetUnset($offset) * @return integer The number of groups * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. - * To be removed in 3.0. + * To be removed in Symfony 3.0. */ public function count() { diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index f8ab1c9981fff..b84d1c379c22f 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -25,8 +25,8 @@ * @since 2.5 * @author Bernhard Schussek * - * @deprecated Implemented for backwards compatibility with Symfony < 2.5. To be - * removed in 3.0. + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in Symfony 3.0. */ class LegacyExecutionContext extends ExecutionContext { diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php index 181b4f47fd00d..7f19bb204c02b 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php @@ -21,8 +21,8 @@ * @since 2.5 * @author Bernhard Schussek * - * @deprecated Implemented for backwards compatibility with Symfony < 2.5. To be - * removed in 3.0. + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in Symfony 3.0. */ class LegacyExecutionContextFactory implements ExecutionContextFactoryInterface { diff --git a/src/Symfony/Component/Validator/ExecutionContext.php b/src/Symfony/Component/Validator/ExecutionContext.php index 00b8cca6369ee..6435bbf9d0568 100644 --- a/src/Symfony/Component/Validator/ExecutionContext.php +++ b/src/Symfony/Component/Validator/ExecutionContext.php @@ -20,6 +20,9 @@ * * @author Fabien Potencier * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Context\ExecutionContext} instead. */ class ExecutionContext implements ExecutionContextInterface { diff --git a/src/Symfony/Component/Validator/ExecutionContextInterface.php b/src/Symfony/Component/Validator/ExecutionContextInterface.php index 92f4c5690b0af..b89caa2deaee2 100644 --- a/src/Symfony/Component/Validator/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/ExecutionContextInterface.php @@ -82,6 +82,9 @@ * @author Bernhard Schussek * * @api + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Context\ExecutionContextInterface} instead. */ interface ExecutionContextInterface { @@ -95,6 +98,10 @@ interface ExecutionContextInterface * @param integer|null $code The violation code. * * @api + * + * @deprecated The parameters $invalidValue, $pluralization and $code are + * deprecated since version 2.5 and will be removed in + * Symfony 3.0. */ public function addViolation($message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null); @@ -110,6 +117,10 @@ public function addViolation($message, array $params = array(), $invalidValue = * @param integer|null $code The violation code. * * @api + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Context\ExecutionContextInterface::buildViolation()} + * instead. */ public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null); @@ -151,6 +162,10 @@ public function addViolationAt($subPath, $message, array $parameters = array(), * or an instance of \Traversable. * @param Boolean $deep Whether to traverse the value recursively if * it is a collection of collections. + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Context\ExecutionContextInterface::getValidator()} + * instead. */ public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false); @@ -180,6 +195,10 @@ public function validate($value, $subPath = '', $groups = null, $traverse = fals * @param null|string|string[] $groups The groups to validate in. If you don't pass any * groups here, the current group of the context * will be used. + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Context\ExecutionContextInterface::getValidator()} + * instead. */ public function validateValue($value, $constraints, $subPath = '', $groups = null); @@ -237,6 +256,12 @@ public function getMetadata(); * Returns the used metadata factory. * * @return MetadataFactoryInterface The metadata factory. + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Context\ExecutionContextInterface::getValidator()} + * instead and call + * {@link Validator\ValidatorInterface::getMetadataFor()} or + * {@link Validator\ValidatorInterface::hasMetadataFor()} there. */ public function getMetadataFactory(); diff --git a/src/Symfony/Component/Validator/GlobalExecutionContextInterface.php b/src/Symfony/Component/Validator/GlobalExecutionContextInterface.php index aff44b350769f..fb2aef3bd772d 100644 --- a/src/Symfony/Component/Validator/GlobalExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/GlobalExecutionContextInterface.php @@ -26,6 +26,9 @@ * * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Context\ExecutionContextInterface} instead. */ interface GlobalExecutionContextInterface { diff --git a/src/Symfony/Component/Validator/MetadataInterface.php b/src/Symfony/Component/Validator/MetadataInterface.php index a5d65048b7816..b2cb20e847b33 100644 --- a/src/Symfony/Component/Validator/MetadataInterface.php +++ b/src/Symfony/Component/Validator/MetadataInterface.php @@ -41,6 +41,9 @@ * again. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Mapping\MetadataInterface} instead. */ interface MetadataInterface { @@ -54,6 +57,8 @@ interface MetadataInterface * @param mixed $value The value to validate. * @param string|string[] $group The validation group to validate in. * @param string $propertyPath The current property path in the validation graph. + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. */ public function accept(ValidationVisitorInterface $visitor, $value, $group, $propertyPath); diff --git a/src/Symfony/Component/Validator/PropertyMetadataContainerInterface.php b/src/Symfony/Component/Validator/PropertyMetadataContainerInterface.php index 20bafb2950fd9..2bb00f2a9ab96 100644 --- a/src/Symfony/Component/Validator/PropertyMetadataContainerInterface.php +++ b/src/Symfony/Component/Validator/PropertyMetadataContainerInterface.php @@ -15,6 +15,9 @@ * A container for {@link PropertyMetadataInterface} instances. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Mapping\ClassMetadataInterface} instead. */ interface PropertyMetadataContainerInterface { diff --git a/src/Symfony/Component/Validator/PropertyMetadataInterface.php b/src/Symfony/Component/Validator/PropertyMetadataInterface.php index eaac1a7121103..c18ae83a05e81 100644 --- a/src/Symfony/Component/Validator/PropertyMetadataInterface.php +++ b/src/Symfony/Component/Validator/PropertyMetadataInterface.php @@ -23,6 +23,9 @@ * @author Bernhard Schussek * * @see MetadataInterface + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Mapping\PropertyMetadataInterface} instead. */ interface PropertyMetadataInterface extends MetadataInterface { diff --git a/src/Symfony/Component/Validator/ValidationVisitor.php b/src/Symfony/Component/Validator/ValidationVisitor.php index ddff8adc60382..302d33c9d7f43 100644 --- a/src/Symfony/Component/Validator/ValidationVisitor.php +++ b/src/Symfony/Component/Validator/ValidationVisitor.php @@ -20,6 +20,9 @@ * {@link GlobalExecutionContextInterface}. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link NodeVisitor\NodeVisitorInterface} instead. */ class ValidationVisitor implements ValidationVisitorInterface, GlobalExecutionContextInterface { diff --git a/src/Symfony/Component/Validator/ValidationVisitorInterface.php b/src/Symfony/Component/Validator/ValidationVisitorInterface.php index e4163718b30d0..513775b31f1cc 100644 --- a/src/Symfony/Component/Validator/ValidationVisitorInterface.php +++ b/src/Symfony/Component/Validator/ValidationVisitorInterface.php @@ -33,6 +33,9 @@ * * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link NodeVisitor\NodeVisitorInterface} instead. */ interface ValidationVisitorInterface { @@ -62,6 +65,8 @@ interface ValidationVisitorInterface * * @throws Exception\NoSuchMetadataException If no metadata can be found for * the given value. + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. */ public function validate($value, $group, $propertyPath, $traverse = false, $deep = false); @@ -75,6 +80,8 @@ public function validate($value, $group, $propertyPath, $traverse = false, $deep * @param mixed $value The value to validate. * @param string $group The validation group to validate. * @param string $propertyPath The current property path in the validation graph. + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. */ public function visit(MetadataInterface $metadata, $value, $group, $propertyPath); } diff --git a/src/Symfony/Component/Validator/Validator.php b/src/Symfony/Component/Validator/Validator.php index a7bcc3a0a6cfb..e80157db8123c 100644 --- a/src/Symfony/Component/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator.php @@ -20,6 +20,9 @@ * * @author Fabien Potencier * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Validator\Validator} instead. */ class Validator implements ValidatorInterface { diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 5bc570737625e..9ecf522f85633 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -17,8 +17,11 @@ use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; /** - * @since %%NextVersion%% + * @since 2.5 * @author Bernhard Schussek + * + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * To be removed in Symfony 3.0. */ class LegacyValidator extends Validator implements LegacyValidatorInterface { diff --git a/src/Symfony/Component/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/ValidatorInterface.php index 98e02d90cf4d8..062b1adc9f789 100644 --- a/src/Symfony/Component/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/ValidatorInterface.php @@ -17,6 +17,9 @@ * @author Bernhard Schussek * * @api + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Validator\ValidatorInterface} instead. */ interface ValidatorInterface { @@ -35,6 +38,10 @@ interface ValidatorInterface * list is empty, validation succeeded. * * @api + * + * @deprecated The signature changed with Symfony 2.5 (see + * {@link Validator\ValidatorInterface::validate()}. This + * signature will be disabled in Symfony 3.0. */ public function validate($value, $groups = null, $traverse = false, $deep = false); @@ -85,6 +92,9 @@ public function validatePropertyValue($containingValue, $property, $value, $grou * list is empty, validation succeeded. * * @api + * + * @deprecated Renamed to {@link Validator\ValidatorInterface::validate()} + * in Symfony 2.5. Will be removed in Symfony 3.0. */ public function validateValue($value, $constraints, $groups = null); @@ -94,6 +104,11 @@ public function validateValue($value, $constraints, $groups = null); * @return MetadataFactoryInterface The metadata factory. * * @api + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Validator\ValidatorInterface::getMetadataFor()} or + * {@link Validator\ValidatorInterface::hasMetadataFor()} + * instead. */ public function getMetadataFactory(); } From dbce5a2f6ae60d47f029314a41583fa1a3de685a Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 20 Feb 2014 21:27:30 +0100 Subject: [PATCH 47/86] [Validator] Updated outdated doc blocks --- .../Component/Validator/Constraints/GroupSequence.php | 6 +++--- .../Validator/Context/ExecutionContextInterface.php | 9 +++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index a2406c7b6e563..af3b86c1c6fb7 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -19,7 +19,7 @@ * When validating a group sequence, each group will only be validated if all * of the previous groups in the sequence succeeded. For example: * - * $validator->validateObject($address, new GroupSequence('Basic', 'Strict')); + * $validator->validate($address, new Valid(), new GroupSequence('Basic', 'Strict')); * * In the first step, all constraints that belong to the group "Basic" will be * validated. If none of the constraints fail, the validator will then validate @@ -42,12 +42,12 @@ * Whenever you validate that object in the "Default" group, the group sequence * will be validated: * - * $validator->validateObject($address); + * $validator->validate($address, new Valid()); * * If you want to execute the constraints of the "Default" group for a class * with an overridden default group, pass the class name as group name instead: * - * $validator->validateObject($address, "Address") + * $validator->validate($address, new Valid(), "Address") * * @Annotation * diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index 4fda437f6cb8f..f6fed2fbee062 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -11,10 +11,7 @@ namespace Symfony\Component\Validator\Context; -use Symfony\Component\Validator\Constraints\GroupSequence; -use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Component\Validator\ExecutionContextInterface as LegacyExecutionContextInterface; -use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; @@ -24,7 +21,7 @@ * The context collects all violations generated during the validation. By * default, validators execute all validations in a new context: * - * $violations = $validator->validateObject($object); + * $violations = $validator->validate($object); * * When you make another call to the validator, while the validation is in * progress, the violations will be isolated from each other: @@ -34,7 +31,7 @@ * $validator = $this->context->getValidator(); * * // The violations are not added to $this->context - * $violations = $validator->validateObject($value); + * $violations = $validator->validate($value); * } * * However, if you want to add the violations to the current context, use the @@ -47,7 +44,7 @@ * // The violations are added to $this->context * $validator * ->inContext($this->context) - * ->validateObject($value) + * ->validate($value) * ; * } * From 822fe4712f4c4173c1fdf6f4fa286aec319e2e2a Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 13:47:18 +0100 Subject: [PATCH 48/86] [Validator] Completed inline documentation of the Node classes and the NodeTraverser --- .../Component/Validator/Node/ClassNode.php | 34 +- .../Validator/Node/CollectionNode.php | 12 +- .../Component/Validator/Node/GenericNode.php | 3 +- src/Symfony/Component/Validator/Node/Node.php | 6 +- .../Component/Validator/Node/PropertyNode.php | 17 +- .../NodeTraverser/NodeTraverserInterface.php | 70 +++- ...rser.php => NonRecursiveNodeTraverser.php} | 358 +++++++++++++----- .../Validator/LegacyValidator2Dot5ApiTest.php | 4 +- .../LegacyValidatorLegacyApiTest.php | 4 +- .../Tests/Validator/Validator2Dot5ApiTest.php | 4 +- 10 files changed, 382 insertions(+), 130 deletions(-) rename src/Symfony/Component/Validator/NodeTraverser/{NodeTraverser.php => NonRecursiveNodeTraverser.php} (52%) diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php index 7c82eead9adec..54e22e2d97403 100644 --- a/src/Symfony/Component/Validator/Node/ClassNode.php +++ b/src/Symfony/Component/Validator/Node/ClassNode.php @@ -18,6 +18,13 @@ /** * Represents an object and its class metadata in the validation graph. * + * If the object is a collection which should be traversed, a new + * {@link CollectionNode} instance will be created for that object: + * + * (TagList:ClassNode) + * \ + * (TagList:CollectionNode) + * * @since 2.5 * @author Bernhard Schussek */ @@ -31,19 +38,22 @@ class ClassNode extends Node /** * Creates a new class node. * - * @param object $object The validated object - * @param ClassMetadataInterface $metadata The class metadata of that - * object - * @param string $propertyPath The property path leading - * to this node - * @param string[] $groups The groups in which this - * node should be validated - * @param string[]|null $cascadedGroups The groups in which - * cascaded objects should be - * validated - * @param integer $traversalStrategy + * @param object $object The validated object + * @param ClassMetadataInterface $metadata The class metadata of + * that object + * @param string $propertyPath The property path leading + * to this node + * @param string[] $groups The groups in which this + * node should be validated + * @param string[]|null $cascadedGroups The groups in which + * cascaded objects should + * be validated + * @param integer $traversalStrategy The strategy used for + * traversing the object + * + * @throws UnexpectedTypeException If $object is not an object * - * @throws \Symfony\Component\Validator\Exception\UnexpectedTypeException + * @see \Symfony\Component\Validator\Mapping\TraversalStrategy */ public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { diff --git a/src/Symfony/Component/Validator/Node/CollectionNode.php b/src/Symfony/Component/Validator/Node/CollectionNode.php index 763ed63072c00..ddca97a5c7f09 100644 --- a/src/Symfony/Component/Validator/Node/CollectionNode.php +++ b/src/Symfony/Component/Validator/Node/CollectionNode.php @@ -15,7 +15,7 @@ use Symfony\Component\Validator\Mapping\TraversalStrategy; /** - * Represents an traversable collection in the validation graph. + * Represents a traversable value in the validation graph. * * @since 2.5 * @author Bernhard Schussek @@ -33,13 +33,19 @@ class CollectionNode extends Node * @param string[]|null $cascadedGroups The groups in which * cascaded objects should be * validated - * @param integer $traversalStrategy The traversal strategy + * @param integer $traversalStrategy The strategy used for + * traversing the collection * - * @throws \Symfony\Component\Validator\Exception\ConstraintDefinitionException + * @throws ConstraintDefinitionException If $collection is not an array or a + * \Traversable + * + * @see \Symfony\Component\Validator\Mapping\TraversalStrategy */ public function __construct($collection, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::TRAVERSE) { if (!is_array($collection) && !$collection instanceof \Traversable) { + // Must throw a ConstraintDefinitionException for backwards + // compatibility reasons with Symfony < 2.5 throw new ConstraintDefinitionException(sprintf( 'Traversal was enabled for "%s", but this class '. 'does not implement "\Traversable".', diff --git a/src/Symfony/Component/Validator/Node/GenericNode.php b/src/Symfony/Component/Validator/Node/GenericNode.php index 82ee9ac7fbaad..9c628e0a465e0 100644 --- a/src/Symfony/Component/Validator/Node/GenericNode.php +++ b/src/Symfony/Component/Validator/Node/GenericNode.php @@ -16,8 +16,7 @@ * attached to it. * * Together with {@link \Symfony\Component\Validator\Mapping\GenericMetadata}, - * this node type can be used to validate a value against some given - * constraints. + * this node type can be used to validate a value against some constraints. * * @since 2.5 * @author Bernhard Schussek diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php index 038217bf512f9..56c8145c45682 100644 --- a/src/Symfony/Component/Validator/Node/Node.php +++ b/src/Symfony/Component/Validator/Node/Node.php @@ -16,7 +16,7 @@ use Symfony\Component\Validator\Mapping\TraversalStrategy; /** - * A node in the validated graph. + * A node in the validation graph. * * @since 2.5 * @author Bernhard Schussek @@ -59,7 +59,11 @@ abstract class Node public $cascadedGroups; /** + * The strategy used for traversing the validated value. + * * @var integer + * + * @see \Symfony\Component\Validator\Mapping\TraversalStrategy */ public $traversalStrategy; diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php index 14dedb320356e..8934bf1d7330c 100644 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -19,16 +19,23 @@ * Represents the value of a property and its associated metadata. * * If the property contains an object and should be cascaded, a new - * {@link ClassNode} instance will be created for that object. - * - * Example: + * {@link ClassNode} instance will be created for that object: * * (Article:ClassNode) * \ - * (author:PropertyNode) + * (->author:PropertyNode) * \ * (Author:ClassNode) * + * If the property contains a collection which should be traversed, a new + * {@link CollectionNode} instance will be created for that collection: + * + * (Article:ClassNode) + * \ + * (->tags:PropertyNode) + * \ + * (array:CollectionNode) + * * @since 2.5 * @author Bernhard Schussek */ @@ -61,6 +68,8 @@ class PropertyNode extends Node * @param integer $traversalStrategy * * @throws UnexpectedTypeException If $object is not an object + * + * @see \Symfony\Component\Validator\Mapping\TraversalStrategy */ public function __construct($object, $value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php index 6084403d9bb16..37c7c8c22c519 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php @@ -12,17 +12,85 @@ namespace Symfony\Component\Validator\NodeTraverser; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; /** - * @since %%NextVersion%% + * Traverses the nodes of the validation graph. + * + * You can attach visitors to the traverser that are invoked during the + * traversal. Before starting the traversal, the + * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::beforeTraversal()} + * method of each visitor is called. For each node in the graph, the + * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::visit()} + * of each visitor is called. At the end of the traversal, the traverser invokes + * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::afterTraversal()} + * on each visitor. + * + * The visitors should be called in the same order in which they are added to + * the traverser. + * + * The validation graph typically contains nodes of the following types: + * + * - {@link \Symfony\Component\Validator\Node\ClassNode}: + * An object with associated class metadata + * - {@link \Symfony\Component\Validator\Node\PropertyNode}: + * A property value with associated property metadata + * - {@link \Symfony\Component\Validator\Node\GenericNode}: + * A generic value with associated constraints + * - {@link \Symfony\Component\Validator\Node\CollectionNode}: + * A traversable collection + * + * Generic nodes are mostly useful when you want to validate a value that has + * neither associated class nor property metadata. Generic nodes usually come + * with {@link \Symfony\Component\Validator\Mapping\GenericMetadata}, that + * contains the constraints that the value should be validated against. + * + * Whenever a class, property or generic node is validated that contains a + * traversable value which should be traversed (according to the + * {@link \Symfony\Component\Validator\Mapping\TraversalStrategy} specified + * in the node or its metadata), a new + * {@link \Symfony\Component\Validator\Node\CollectionNode} will be attached + * to the node graph. + * + * For example: + * + * (TagList:ClassNode) + * \ + * (TagList:CollectionNode) + * + * When writing custom visitors, be aware that collection nodes usually contain + * values that have already been passed to the visitor before through a class + * node, a property node or a generic node. + * + * @since 2.5 * @author Bernhard Schussek */ interface NodeTraverserInterface { + /** + * Adds a new visitor to the traverser. + * + * Visitors that have already been added before are ignored. + * + * @param NodeVisitorInterface $visitor The visitor to add + */ public function addVisitor(NodeVisitorInterface $visitor); + /** + * Removes a visitor from the traverser. + * + * Non-existing visitors are ignored. + * + * @param NodeVisitorInterface $visitor The visitor to remove + */ public function removeVisitor(NodeVisitorInterface $visitor); + /** + * Traverses the given nodes in the given context. + * + * @param Node[] $nodes The nodes to traverse + * @param ExecutionContextInterface $context The validation context + */ public function traverse(array $nodes, ExecutionContextInterface $context); } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php similarity index 52% rename from src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php rename to src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php index be9f2d2b880e8..ca1e2a9db5adc 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php @@ -24,10 +24,37 @@ use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; /** - * @since %%NextVersion%% + * Non-recursive implementation of {@link NodeTraverserInterface}. + * + * This implementation uses a Depth First algorithm to traverse the node + * graph. Instead of loading the complete node graph into memory before the + * traversal, the traverser only expands the successor nodes of a node once + * that node is traversed. For example, when traversing a class node, the + * nodes for all constrained properties of that class are loaded into memory. + * When the traversal of the class node is over, the node is discarded. + * + * Next, one of the class' property nodes is traversed. At that point, the + * successor nodes of that property node (a class node, if the property should + * be cascaded, or a collection node, if the property should be traversed) are + * loaded into memory. As soon as the traversal of the property node is over, + * it is discarded as well. + * + * This leads to an average memory consumption of O(log N * B), where N is the + * number of nodes in the graph and B is the average number of successor nodes + * of a node. + * + * In order to maintain a small execution stack, nodes are not validated + * recursively, but iteratively. Internally, a stack is used to store all the + * nodes that should be processed. Whenever a node is traversed, its successor + * nodes are put on the stack. The traverser keeps fetching and traversing nodes + * from the stack until the stack is empty and all nodes have been traversed. + * + * @since 2.5 * @author Bernhard Schussek + * + * @see NodeTraverserInterface */ -class NodeTraverser implements NodeTraverserInterface +class NonRecursiveNodeTraverser implements NodeTraverserInterface { /** * @var NodeVisitorInterface[] @@ -39,20 +66,34 @@ class NodeTraverser implements NodeTraverserInterface */ private $metadataFactory; + /** + * @var Boolean + */ private $traversalStarted = false; + /** + * Creates a new traverser. + * + * @param MetadataFactoryInterface $metadataFactory The metadata factory + */ public function __construct(MetadataFactoryInterface $metadataFactory) { $this->visitors = new \SplObjectStorage(); - $this->nodeQueue = new \SplQueue(); + $this->nodeStack = new \SplStack(); $this->metadataFactory = $metadataFactory; } + /** + * {@inheritdoc} + */ public function addVisitor(NodeVisitorInterface $visitor) { $this->visitors->attach($visitor); } + /** + * {@inheritdoc} + */ public function removeVisitor(NodeVisitorInterface $visitor) { $this->visitors->detach($visitor); @@ -63,45 +104,67 @@ public function removeVisitor(NodeVisitorInterface $visitor) */ public function traverse(array $nodes, ExecutionContextInterface $context) { + // beforeTraversal() and afterTraversal() are only executed for the + // top-level call of traverse() $isTopLevelCall = !$this->traversalStarted; if ($isTopLevelCall) { + // Remember that the traversal was already started for the case of + // recursive calls to traverse() $this->traversalStarted = true; foreach ($this->visitors as $visitor) { - /** @var NodeVisitorInterface $visitor */ $visitor->beforeTraversal($nodes, $context); } } - $nodeQueue = new \SplQueue(); + // This stack contains all the nodes that should be traversed + // A stack is used rather than a queue in order to traverse the graph + // in a Depth First approach (the last added node is processed first). + // In this way, the order in which the nodes are passed to the visitors + // is similar to a recursive implementation (except that the successor + // nodes of a node are traversed right-to-left instead of left-to-right). + $nodeStack = new \SplStack(); foreach ($nodes as $node) { - $nodeQueue->enqueue($node); + // Push a node to the stack and immediately process it. This way, + // the successor nodes are traversed before the next node in $nodes + $nodeStack->push($node); - while (!$nodeQueue->isEmpty()) { - $node = $nodeQueue->dequeue(); + // Fetch nodes from the stack and traverse them until no more nodes + // are left. Then continue with the next node in $nodes. + while (!$nodeStack->isEmpty()) { + $node = $nodeStack->pop(); if ($node instanceof ClassNode) { - $this->traverseClassNode($node, $nodeQueue, $context); + $this->traverseClassNode($node, $context, $nodeStack); } elseif ($node instanceof CollectionNode) { - $this->traverseCollectionNode($node, $nodeQueue, $context); + $this->traverseCollectionNode($node, $context, $nodeStack); } else { - $this->traverseNode($node, $nodeQueue, $context); + $this->traverseNode($node, $context, $nodeStack); } } } if ($isTopLevelCall) { foreach ($this->visitors as $visitor) { - /** @var NodeVisitorInterface $visitor */ $visitor->afterTraversal($nodes, $context); } + // Put the traverser back into its initial state $this->traversalStarted = false; } } + /** + * Executes the {@link NodeVisitorInterface::visit()} method of each + * visitor. + * + * @param Node $node The visited node + * @param ExecutionContextInterface $context The current execution context + * + * @return Boolean Whether to traverse the node's successor nodes + */ private function visit(Node $node, ExecutionContextInterface $context) { foreach ($this->visitors as $visitor) { @@ -113,87 +176,27 @@ private function visit(Node $node, ExecutionContextInterface $context) return true; } - private function traverseNode(Node $node, \SplQueue $nodeQueue, ExecutionContextInterface $context) - { - // Visitors have two possibilities to influence the traversal: - // - // 1. If a visitor's visit() method returns false, the traversal is - // skipped entirely. - // 2. If a visitor's visit() method removes a group from the node, - // that group will be skipped in the subtree of that node. - - if (false === $this->visit($node, $context)) { - return; - } - - if (null === $node->value) { - return; - } - - // The "cascadedGroups" property is set by the NodeValidationVisitor when - // traversing group sequences - $cascadedGroups = null !== $node->cascadedGroups - ? $node->cascadedGroups - : $node->groups; - - if (0 === count($cascadedGroups)) { - return; - } - - $cascadingStrategy = $node->metadata->getCascadingStrategy(); - $traversalStrategy = $node->metadata->getTraversalStrategy(); - - if (is_array($node->value)) { - // Arrays are always traversed, independent of the specified - // traversal strategy - // (BC with Symfony < 2.5) - $nodeQueue->enqueue(new CollectionNode( - $node->value, - $node->propertyPath, - $cascadedGroups, - null, - $traversalStrategy - )); - - return; - } - - if ($cascadingStrategy & CascadingStrategy::CASCADE) { - // If the value is a scalar, pass it anyway, because we want - // a NoSuchMetadataException to be thrown in that case - // (BC with Symfony < 2.5) - $this->cascadeObject( - $node->value, - $node->propertyPath, - $cascadedGroups, - $traversalStrategy, - $nodeQueue - ); - - return; - } - - // 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 - $nodeQueue->enqueue(new CollectionNode( - $node->value, - $node->propertyPath, - $cascadedGroups, - null, - $traversalStrategy - )); - } - - private function traverseClassNode(ClassNode $node, \SplQueue $nodeQueue, ExecutionContextInterface $context) + /** + * Traverses a class node. + * + * At first, each visitor is invoked for this node. Then, unless any + * of the visitors aborts the traversal by returning false, a property + * node is put on the node stack for each constrained property of the class. + * At last, if the class is traversable and should be traversed according + * to the selected traversal strategy, a new collection node is put on the + * stack. + * + * @param ClassNode $node The class node + * @param ExecutionContextInterface $context The current execution context + * @param \SplStack $nodeStack The stack for storing the + * successor nodes + * + * @see ClassNode + * @see PropertyNode + * @see CollectionNode + * @see TraversalStrategy + */ + private function traverseClassNode(ClassNode $node, ExecutionContextInterface $context, \SplStack $nodeStack) { // Visitors have two possibilities to influence the traversal: // @@ -212,7 +215,7 @@ private function traverseClassNode(ClassNode $node, \SplQueue $nodeQueue, Execut foreach ($node->metadata->getConstrainedProperties() as $propertyName) { foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { - $nodeQueue->enqueue(new PropertyNode( + $nodeStack->push(new PropertyNode( $node->value, $propertyMetadata->getPropertyValue($node->value), $propertyMetadata, @@ -246,7 +249,7 @@ private function traverseClassNode(ClassNode $node, \SplQueue $nodeQueue, Execut } // If TRAVERSE, the constructor will fail if we have no Traversable - $nodeQueue->enqueue(new CollectionNode( + $nodeStack->push(new CollectionNode( $node->value, $node->propertyPath, $node->groups, @@ -255,7 +258,31 @@ private function traverseClassNode(ClassNode $node, \SplQueue $nodeQueue, Execut )); } - private function traverseCollectionNode(CollectionNode $node, \SplQueue $nodeQueue, ExecutionContextInterface $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 + * @param \SplStack $nodeStack The stack for storing the + * successor nodes + * + * @see ClassNode + * @see CollectionNode + */ + private function traverseCollectionNode(CollectionNode $node, ExecutionContextInterface $context, \SplStack $nodeStack) { // Visitors have two possibilities to influence the traversal: // @@ -285,7 +312,7 @@ private function traverseCollectionNode(CollectionNode $node, \SplQueue $nodeQue // Arrays are always cascaded, independent of the specified // traversal strategy // (BC with Symfony < 2.5) - $nodeQueue->enqueue(new CollectionNode( + $nodeStack->push(new CollectionNode( $value, $node->propertyPath.'['.$key.']', $node->groups, @@ -304,13 +331,142 @@ private function traverseCollectionNode(CollectionNode $node, \SplQueue $nodeQue $node->propertyPath.'['.$key.']', $node->groups, $traversalStrategy, - $nodeQueue + $nodeStack ); } } } - private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, \SplQueue $nodeQueue) + /** + * Traverses a node that is neither a class nor a collection node. + * + * At first, each visitor is invoked for this node. Then, unless any + * of the visitors aborts the traversal by returning false, the successor + * nodes of the collection node are put on the stack: + * + * - if the node contains an object with associated class metadata, a new + * class node is put on the stack; + * - if the node contains a traversable object without associated class + * metadata and traversal is enabled according to the selected traversal + * strategy, a collection node is put on the stack; + * - if the node contains an array, a collection node is put on the stack. + * + * @param Node $node The node + * @param ExecutionContextInterface $context The current execution context + * @param \SplStack $nodeStack The stack for storing the + * successor nodes + */ + private function traverseNode(Node $node, ExecutionContextInterface $context, \SplStack $nodeStack) + { + // Visitors have two possibilities to influence the traversal: + // + // 1. If a visitor's visit() method returns false, the traversal is + // skipped entirely. + // 2. If a visitor's visit() method removes a group from the node, + // that group will be skipped in the subtree of that node. + + if (false === $this->visit($node, $context)) { + return; + } + + if (null === $node->value) { + return; + } + + // The "cascadedGroups" property is set by the NodeValidationVisitor when + // traversing group sequences + $cascadedGroups = null !== $node->cascadedGroups + ? $node->cascadedGroups + : $node->groups; + + if (0 === count($cascadedGroups)) { + return; + } + + $cascadingStrategy = $node->metadata->getCascadingStrategy(); + $traversalStrategy = $node->traversalStrategy; + + // If no specific traversal strategy was requested when this method + // was called, use the traversal strategy of the node's metadata + if ($traversalStrategy & TraversalStrategy::IMPLICIT) { + // Keep the STOP_RECURSION flag, if it was set + $traversalStrategy = $node->metadata->getTraversalStrategy() + | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); + } + + if (is_array($node->value)) { + // Arrays are always traversed, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $nodeStack->push(new CollectionNode( + $node->value, + $node->propertyPath, + $cascadedGroups, + null, + $traversalStrategy + )); + + return; + } + + if ($cascadingStrategy & CascadingStrategy::CASCADE) { + // If the value is a scalar, pass it anyway, because we want + // a NoSuchMetadataException to be thrown in that case + // (BC with Symfony < 2.5) + $this->cascadeObject( + $node->value, + $node->propertyPath, + $cascadedGroups, + $traversalStrategy, + $nodeStack + ); + + return; + } + + // Traverse only if IMPLICIT or TRAVERSE + if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { + return; + } + + // If IMPLICIT, stop unless we deal with a Traversable + if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) { + return; + } + + // If TRAVERSE, the constructor will fail if we have no Traversable + $nodeStack->push(new CollectionNode( + $node->value, + $node->propertyPath, + $cascadedGroups, + null, + $traversalStrategy + )); + } + + /** + * Executes the cascading logic for an object. + * + * If class metadata is available for the object, a class node is put on + * the node stack. Otherwise, if the selected traversal strategy allows + * traversal of the object, a new collection node is put on the stack. + * Otherwise, an exception is thrown. + * + * @param object $object The object to cascade + * @param string $propertyPath The current property path + * @param string[] $groups The validated groups + * @param integer $traversalStrategy The strategy for traversing the + * cascaded object + * @param \SplStack $nodeStack The stack for storing the successor + * nodes + * + * @throws NoSuchMetadataException If the object has no associated metadata + * and does not implement {@link \Traversable} + * or if traversal is disabled via the + * $traversalStrategy argument + * + */ + private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, \SplStack $nodeStack) { try { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -319,7 +475,7 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal // error } - $nodeQueue->enqueue(new ClassNode( + $nodeStack->push(new ClassNode( $object, $classMetadata, $propertyPath, @@ -338,7 +494,7 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal throw $e; } - $nodeQueue->enqueue(new CollectionNode( + $nodeStack->push(new CollectionNode( $object, $propertyPath, $groups, diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index 7447c7ce29edf..5468642e7d3d3 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -18,7 +18,7 @@ use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; -use Symfony\Component\Validator\NodeTraverser\NodeTraverser; +use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; class LegacyValidator2Dot5ApiTest extends Abstract2Dot5ApiTest @@ -34,7 +34,7 @@ protected function setUp() protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $nodeTraverser = new NodeTraverser($metadataFactory); + $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index e7fd2a8ccbc6c..3edd86b41054f 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -18,7 +18,7 @@ use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; -use Symfony\Component\Validator\NodeTraverser\NodeTraverser; +use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; class LegacyValidatorLegacyApiTest extends AbstractLegacyApiTest @@ -34,7 +34,7 @@ protected function setUp() protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $nodeTraverser = new NodeTraverser($metadataFactory); + $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php index b49c380c98708..277d641b7aee4 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php @@ -18,14 +18,14 @@ use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; -use Symfony\Component\Validator\NodeTraverser\NodeTraverser; +use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\Validator\Validator; class Validator2Dot5ApiTest extends Abstract2Dot5ApiTest { protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $nodeTraverser = new NodeTraverser($metadataFactory); + $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new ExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new Validator($contextFactory, $nodeTraverser, $metadataFactory); From 186c115894737c0934b830910b3ec222f94bb789 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 14:44:01 +0100 Subject: [PATCH 49/86] [Validator] Improved test coverage of NonRecursiveNodeTraverser --- .../UnsupportedMetadataException.php | 20 +++++ .../Validator/Mapping/ClassMetadata.php | 11 +++ .../Validator/Mapping/GenericMetadata.php | 78 ++++++++++++++++--- .../NonRecursiveNodeTraverser.php | 47 ++++++----- .../Tests/Fixtures/FakeClassMetadata.php | 29 +++++++ .../Tests/Fixtures/FakeMetadataFactory.php | 17 +++- .../Tests/Fixtures/LegacyClassMetadata.php | 20 +++++ .../Tests/Validator/Abstract2Dot5ApiTest.php | 55 +++++++++++++ 8 files changed, 244 insertions(+), 33 deletions(-) create mode 100644 src/Symfony/Component/Validator/Exception/UnsupportedMetadataException.php create mode 100644 src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php create mode 100644 src/Symfony/Component/Validator/Tests/Fixtures/LegacyClassMetadata.php diff --git a/src/Symfony/Component/Validator/Exception/UnsupportedMetadataException.php b/src/Symfony/Component/Validator/Exception/UnsupportedMetadataException.php new file mode 100644 index 0000000000000..c6ece50b70062 --- /dev/null +++ b/src/Symfony/Component/Validator/Exception/UnsupportedMetadataException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Exception; + +/** + * @since 2.5 + * @author Bernhard Schussek + */ +class UnsupportedMetadataException extends InvalidArgumentException +{ +} diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 5a2782f854041..30c7b1ad3b04c 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -64,6 +64,15 @@ class ClassMetadata extends ElementMetadata implements LegacyMetadataInterface, */ public $groupSequenceProvider = false; + /** + * The strategy for traversing traversable objects. + * + * By default, only instances of {@link \Traversable} are traversed. + * + * @var integer + */ + public $traversalStrategy = TraversalStrategy::IMPLICIT; + /** * @var \ReflectionClass */ @@ -215,6 +224,8 @@ public function addConstraint(Constraint $constraint) $constraint->addImplicitGroupName($this->getDefaultGroup()); parent::addConstraint($constraint); + + return $this; } /** diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index 47eda213a4109..f77e11736a464 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -13,10 +13,13 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\Exception\BadMethodCallException; use Symfony\Component\Validator\ValidationVisitorInterface; /** - * @since %%NextVersion%% + * A generic container of {@link Constraint} objects. + * + * @since 2.5 * @author Bernhard Schussek */ class GenericMetadata implements MetadataInterface @@ -31,9 +34,27 @@ class GenericMetadata implements MetadataInterface */ public $constraintsByGroup = array(); + /** + * The strategy for cascading objects. + * + * By default, objects are not cascaded. + * + * @var integer + * + * @see CascadingStrategy + */ public $cascadingStrategy = CascadingStrategy::NONE; - public $traversalStrategy = TraversalStrategy::IMPLICIT; + /** + * The strategy for traversing traversable objects. + * + * By default, traversable objects are not traversed. + * + * @var integer + * + * @see TraversalStrategy + */ + public $traversalStrategy = TraversalStrategy::NONE; /** * Returns the names of the properties that should be serialized. @@ -66,11 +87,22 @@ public function __clone() } /** - * Adds a constraint to this element. + * Adds a constraint. + * + * If the constraint {@link Valid} is added, the cascading strategy will be + * changed to {@link CascadingStrategy::CASCADE}. Depending on the + * properties $traverse and $deep of that constraint, the traversal strategy + * will be set to one of the following: * - * @param Constraint $constraint + * - {@link TraversalStrategy::IMPLICIT} if $traverse is enabled and $deep + * is enabled + * - {@link TraversalStrategy::IMPLICIT} | {@link TraversalStrategy::STOP_RECURSION} + * if $traverse is enabled, but $deep is disabled + * - {@link TraversalStrategy::NONE} if $traverse is disabled * - * @return ElementMetadata + * @param Constraint $constraint The constraint to add + * + * @return GenericMetadata This object */ public function addConstraint(Constraint $constraint) { @@ -100,17 +132,26 @@ public function addConstraint(Constraint $constraint) return $this; } + /** + * Adds an list of constraints. + * + * @param Constraint[] $constraints The constraints to add + * + * @return GenericMetadata This object + */ public function addConstraints(array $constraints) { foreach ($constraints as $constraint) { $this->addConstraint($constraint); } + + return $this; } /** * Returns all constraints of this element. * - * @return Constraint[] An array of Constraint instances + * @return Constraint[] A list of Constraint instances */ public function getConstraints() { @@ -132,30 +173,45 @@ public function hasConstraints() * * @param string $group The group name * - * @return array An array with all Constraint instances belonging to the group + * @return Constraint[] An list of all the Constraint instances belonging + * to the group */ public function findConstraints($group) { return isset($this->constraintsByGroup[$group]) - ? $this->constraintsByGroup[$group] - : array(); + ? $this->constraintsByGroup[$group] + : array(); } + /** + * {@inheritdoc} + */ public function getCascadingStrategy() { return $this->cascadingStrategy; } + /** + * {@inheritdoc} + */ public function getTraversalStrategy() { return $this->traversalStrategy; } /** - * {@inheritdoc} + * Exists for compatibility with the deprecated + * {@link Symfony\Component\Validator\MetadataInterface}. + * + * Should not be used. + * + * @throws BadMethodCallException + * + * @deprecated Implemented for backwards compatibility with Symfony < 2.5. + * Will be removed in Symfony 3.0. */ public function accept(ValidationVisitorInterface $visitor, $value, $group, $propertyPath) { - // Thanks PHP < 5.3.9 + throw new BadMethodCallException('Not supported.'); } } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php index ca1e2a9db5adc..b931b969431ad 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php @@ -13,8 +13,10 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Exception\NoSuchMetadataException; +use Symfony\Component\Validator\Exception\UnsupportedMetadataException; use Symfony\Component\Validator\Mapping\CascadingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; +use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Node\ClassNode; @@ -191,6 +193,9 @@ private function visit(Node $node, ExecutionContextInterface $context) * @param \SplStack $nodeStack The stack for storing the * successor nodes * + * @throws UnsupportedMetadataException If a property metadata does not + * implement {@link PropertyMetadataInterface} + * * @see ClassNode * @see PropertyNode * @see CollectionNode @@ -215,6 +220,15 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c foreach ($node->metadata->getConstrainedProperties() as $propertyName) { foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { + if (!$propertyMetadata instanceof PropertyMetadataInterface) { + throw new UnsupportedMetadataException(sprintf( + 'The property metadata instances should implement '. + '"Symfony\Component\Validator\Mapping\PropertyMetadataInterface", '. + 'got: "%s".', + is_object($propertyMetadata) ? get_class($propertyMetadata) : gettype($propertyMetadata) + )); + } + $nodeStack->push(new PropertyNode( $node->value, $propertyMetadata->getPropertyValue($node->value), @@ -424,24 +438,12 @@ private function traverseNode(Node $node, ExecutionContextInterface $context, \S return; } - // 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; - } + // 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. - // If TRAVERSE, the constructor will fail if we have no Traversable - $nodeStack->push(new CollectionNode( - $node->value, - $node->propertyPath, - $cascadedGroups, - null, - $traversalStrategy - )); + // see GenericMetadata::addConstraint() } /** @@ -464,7 +466,9 @@ private function traverseNode(Node $node, ExecutionContextInterface $context, \S * and does not implement {@link \Traversable} * or if traversal is disabled via the * $traversalStrategy argument - * + * @throws UnsupportedMetadataException If the metadata returned by the + * metadata factory does not implement + * {@link ClassMetadataInterface} */ private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, \SplStack $nodeStack) { @@ -472,7 +476,12 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { - // error + throw new UnsupportedMetadataException(sprintf( + 'The metadata factory should return instances of '. + '"Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) + )); } $nodeStack->push(new ClassNode( diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php b/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php new file mode 100644 index 0000000000000..c6b79f66f336d --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Fixtures; + +use Symfony\Component\Validator\ClassBasedInterface; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\MetadataInterface; +use Symfony\Component\Validator\PropertyMetadataContainerInterface; + +class FakeClassMetadata extends ClassMetadata +{ + public function addPropertyMetadata($propertyName, $metadata) + { + if (!isset($this->members[$propertyName])) { + $this->members[$propertyName] = array(); + } + + $this->members[$propertyName][] = $metadata; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php index b03eacf71e90a..09b0ca63bea53 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php @@ -22,7 +22,10 @@ class FakeMetadataFactory implements MetadataFactoryInterface public function getMetadataFor($class) { + $hash = null; + if (is_object($class)) { + $hash = spl_object_hash($class); $class = get_class($class); } @@ -31,6 +34,10 @@ public function getMetadataFor($class) } if (!isset($this->metadatas[$class])) { + if (isset($this->metadatas[$hash])) { + return $this->metadatas[$hash]; + } + throw new NoSuchMetadataException(sprintf('No metadata for "%s"', $class)); } @@ -39,24 +46,28 @@ public function getMetadataFor($class) public function hasMetadataFor($class) { + $hash = null; + if (is_object($class)) { $class = get_class($class); + $hash = spl_object_hash($hash); } if (!is_string($class)) { return false; } - return isset($this->metadatas[$class]); + return isset($this->metadatas[$class]) || isset($this->metadatas[$hash]); } - public function addMetadata(ClassMetadata $metadata) + public function addMetadata($metadata) { $this->metadatas[$metadata->getClassName()] = $metadata; } public function addMetadataForValue($value, MetadataInterface $metadata) { - $this->metadatas[$value] = $metadata; + $key = is_object($value) ? spl_object_hash($value) : $value; + $this->metadatas[$key] = $metadata; } } diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/LegacyClassMetadata.php b/src/Symfony/Component/Validator/Tests/Fixtures/LegacyClassMetadata.php new file mode 100644 index 0000000000000..6a832a109f99e --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/LegacyClassMetadata.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Fixtures; + +use Symfony\Component\Validator\ClassBasedInterface; +use Symfony\Component\Validator\MetadataInterface; +use Symfony\Component\Validator\PropertyMetadataContainerInterface; + +interface LegacyClassMetadata extends MetadataInterface, PropertyMetadataContainerInterface, ClassBasedInterface +{ +} diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 973283c84e88c..63b8c6a551060 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -20,6 +20,7 @@ use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Tests\Fixtures\Entity; +use Symfony\Component\Validator\Tests\Fixtures\FakeClassMetadata; use Symfony\Component\Validator\Tests\Fixtures\Reference; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -386,4 +387,58 @@ public function testAddCustomizedViolation() $this->assertSame(2, $violations[0]->getMessagePluralization()); $this->assertSame('Code', $violations[0]->getCode()); } + + /** + * @expectedException \Symfony\Component\Validator\Exception\UnsupportedMetadataException + */ + public function testMetadataMustImplementClassMetadataInterface() + { + $entity = new Entity(); + + $metadata = $this->getMock('Symfony\Component\Validator\Tests\Fixtures\LegacyClassMetadata'); + $metadata->expects($this->any()) + ->method('getClassName') + ->will($this->returnValue(get_class($entity))); + + $this->metadataFactory->addMetadata($metadata); + + $this->validator->validate($entity); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\UnsupportedMetadataException + */ + public function testReferenceMetadataMustImplementClassMetadataInterface() + { + $entity = new Entity(); + $entity->reference = new Reference(); + + $metadata = $this->getMock('Symfony\Component\Validator\Tests\Fixtures\LegacyClassMetadata'); + $metadata->expects($this->any()) + ->method('getClassName') + ->will($this->returnValue(get_class($entity->reference))); + + $this->metadataFactory->addMetadata($metadata); + + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $this->validator->validate($entity); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\UnsupportedMetadataException + */ + public function testPropertyMetadataMustImplementPropertyMetadataInterface() + { + $entity = new Entity(); + + // Legacy interface + $propertyMetadata = $this->getMock('Symfony\Component\Validator\MetadataInterface'); + $metadata = new FakeClassMetadata(get_class($entity)); + $metadata->addPropertyMetadata('firstName', $propertyMetadata); + + $this->metadataFactory->addMetadata($metadata); + + $this->validator->validate($entity); + } } From 299c2dca10c8d4497d51f09af8a6e03574fadc9f Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 15:26:27 +0100 Subject: [PATCH 50/86] [Validator] Improved test coverage and prevented duplicate validation of constraints --- .../Constraints/CallbackValidator.php | 30 ++++--- .../Validator/Context/ExecutionContext.php | 88 +++++++++++++++++- .../Context/ExecutionContextInterface.php | 89 +++++++++++++++++++ .../Validator/NodeVisitor/AbstractVisitor.php | 15 +++- .../NodeVisitor/ContextUpdateVisitor.php | 19 ++-- ...r.php => DefaultGroupReplacingVisitor.php} | 23 ++++- .../NodeVisitor/NodeObserverInterface.php | 23 ----- .../NodeVisitor/NodeValidationVisitor.php | 39 +++++--- .../NodeVisitor/NodeVisitorInterface.php | 11 ++- .../Constraints/CallbackValidatorTest.php | 30 +++++++ .../Tests/Validator/Abstract2Dot5ApiTest.php | 57 ++++++++---- .../Validator/LegacyValidator2Dot5ApiTest.php | 4 +- .../LegacyValidatorLegacyApiTest.php | 4 +- .../Tests/Validator/Validator2Dot5ApiTest.php | 4 +- 14 files changed, 345 insertions(+), 91 deletions(-) rename src/Symfony/Component/Validator/NodeVisitor/{GroupSequenceResolvingVisitor.php => DefaultGroupReplacingVisitor.php} (64%) delete mode 100644 src/Symfony/Component/Validator/NodeVisitor/NodeObserverInterface.php diff --git a/src/Symfony/Component/Validator/Constraints/CallbackValidator.php b/src/Symfony/Component/Validator/Constraints/CallbackValidator.php index 39da982bb13ca..57e28fb82f137 100644 --- a/src/Symfony/Component/Validator/Constraints/CallbackValidator.php +++ b/src/Symfony/Component/Validator/Constraints/CallbackValidator.php @@ -33,10 +33,6 @@ public function validate($object, Constraint $constraint) if (!$constraint instanceof Callback) { throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\Callback'); } - - if (null === $object) { - return; - } if (null !== $constraint->callback && null !== $constraint->methods) { throw new ConstraintDefinitionException( @@ -60,18 +56,24 @@ public function validate($object, Constraint $constraint) } call_user_func($method, $object, $this->context); - } else { - if (!method_exists($object, $method)) { - throw new ConstraintDefinitionException(sprintf('Method "%s" targeted by Callback constraint does not exist', $method)); - } - $reflMethod = new \ReflectionMethod($object, $method); + continue; + } - if ($reflMethod->isStatic()) { - $reflMethod->invoke(null, $object, $this->context); - } else { - $reflMethod->invoke($object, $this->context); - } + if (null === $object) { + continue; + } + + if (!method_exists($object, $method)) { + throw new ConstraintDefinitionException(sprintf('Method "%s" targeted by Callback constraint does not exist', $method)); + } + + $reflMethod = new \ReflectionMethod($object, $method); + + if ($reflMethod->isStatic()) { + $reflMethod->invoke(null, $object, $this->context); + } else { + $reflMethod->invoke($object, $this->context); } } } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 8f7c9ee6932f3..fdfddf9c4ba9e 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -19,7 +19,6 @@ use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\NodeVisitor\NodeObserverInterface; use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; @@ -32,7 +31,7 @@ * * @see ExecutionContextInterface */ -class ExecutionContext implements ExecutionContextInterface, NodeObserverInterface +class ExecutionContext implements ExecutionContextInterface { /** * @var ValidatorInterface @@ -75,6 +74,27 @@ class ExecutionContext implements ExecutionContextInterface, NodeObserverInterfa */ private $node; + /** + * Stores which objects have been validated in which group. + * + * @var array + */ + private $validatedObjects = array(); + + /** + * Stores which class constraint has been validated for which object. + * + * @var array + */ + private $validatedClassConstraints = array(); + + /** + * Stores which property constraint has been validated for which property. + * + * @var array + */ + private $validatedPropertyConstraints = array(); + /** * Creates a new execution context. * @@ -279,4 +299,68 @@ public function getMetadataFactory() 'or hasMetadataFor() instead or enable the legacy mode.' ); } + + /** + * {@inheritdoc} + */ + public function markObjectAsValidatedForGroup($objectHash, $groupHash) + { + if (!isset($this->validatedObjects[$objectHash])) { + $this->validatedObjects[$objectHash] = array(); + } + + $this->validatedObjects[$objectHash][$groupHash] = true; + } + + /** + * {@inheritdoc} + */ + public function isObjectValidatedForGroup($objectHash, $groupHash) + { + return isset($this->validatedObjects[$objectHash][$groupHash]); + } + + /** + * {@inheritdoc} + */ + public function markClassConstraintAsValidated($objectHash, $constraintHash) + { + if (!isset($this->validatedClassConstraints[$objectHash])) { + $this->validatedClassConstraints[$objectHash] = array(); + } + + $this->validatedClassConstraints[$objectHash][$constraintHash] = true; + } + + /** + * {@inheritdoc} + */ + public function isClassConstraintValidated($objectHash, $constraintHash) + { + return isset($this->validatedClassConstraints[$objectHash][$constraintHash]); + } + + /** + * {@inheritdoc} + */ + public function markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash) + { + if (!isset($this->validatedPropertyConstraints[$objectHash])) { + $this->validatedPropertyConstraints[$objectHash] = array(); + } + + if (!isset($this->validatedPropertyConstraints[$objectHash][$propertyName])) { + $this->validatedPropertyConstraints[$objectHash][$propertyName] = array(); + } + + $this->validatedPropertyConstraints[$objectHash][$propertyName][$constraintHash] = true; + } + + /** + * {@inheritdoc} + */ + public function isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash) + { + return isset($this->validatedPropertyConstraints[$objectHash][$propertyName][$constraintHash]); + } } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index f6fed2fbee062..b955a34f27655 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\Node\Node; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; @@ -97,4 +98,92 @@ public function buildViolation($message, array $parameters = array()); * @return ValidatorInterface */ public function getValidator(); + + /** + * Sets the currently traversed node. + * + * @param Node $node The current node + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function setCurrentNode(Node $node); + + /** + * Marks an object as validated in a specific validation group. + * + * @param string $objectHash The hash of the object + * @param string $groupHash The group's name or hash, if it is group + * sequence + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function markObjectAsValidatedForGroup($objectHash, $groupHash); + + /** + * Returns whether an object was validated in a specific validation group. + * + * @param string $objectHash The hash of the object + * @param string $groupHash The group's name or hash, if it is group + * sequence + * + * @return Boolean Whether the object was already validated for that + * group + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function isObjectValidatedForGroup($objectHash, $groupHash); + + /** + * Marks a constraint as validated for an object. + * + * @param string $objectHash The hash of the object + * @param string $constraintHash The hash of the constraint + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function markClassConstraintAsValidated($objectHash, $constraintHash); + + /** + * Returns whether a constraint was validated for an object. + * + * @param string $objectHash The hash of the object + * @param string $constraintHash The hash of the constraint + * + * @return Boolean Whether the constraint was already validated + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function isClassConstraintValidated($objectHash, $constraintHash); + + /** + * Marks a constraint as validated for an object and a property name. + * + * @param string $objectHash The hash of the object + * @param string $propertyName The property name + * @param string $constraintHash The hash of the constraint + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); + + /** + * Returns whether a constraint was validated for an object and a property + * name. + * + * @param string $objectHash The hash of the object + * @param string $propertyName The property name + * @param string $constraintHash The hash of the constraint + * + * @return Boolean Whether the constraint was already validated + * + * @internal Used by the validator engine. Should not be called by user + * code. + */ + public function isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash); } diff --git a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php index a47783ab0c8a7..3f15359878ed5 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php @@ -15,19 +15,32 @@ use Symfony\Component\Validator\Node\Node; /** - * @since %%NextVersion%% + * Base visitor with empty method stubs. + * + * @since 2.5 * @author Bernhard Schussek + * + * @see NodeVisitorInterface */ abstract class AbstractVisitor implements NodeVisitorInterface { + /** + * {@inheritdoc} + */ public function beforeTraversal(array $nodes, ExecutionContextInterface $context) { } + /** + * {@inheritdoc} + */ public function afterTraversal(array $nodes, ExecutionContextInterface $context) { } + /** + * {@inheritdoc} + */ public function visit(Node $node, ExecutionContextInterface $context) { } diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php index 4fb7f1b33a428..ecf0b2694c243 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php @@ -12,29 +12,24 @@ namespace Symfony\Component\Validator\NodeVisitor; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Exception\RuntimeException; use Symfony\Component\Validator\Node\Node; /** - * Updates the current context with the current node of the validation - * traversal. + * Informs the execution context about the currently validated node. * * @since 2.5 * @author Bernhard Schussek */ class ContextUpdateVisitor extends AbstractVisitor { + /** + * Updates the execution context. + * + * @param Node $node The current node + * @param ExecutionContextInterface $context The execution context + */ public function visit(Node $node, ExecutionContextInterface $context) { - if (!$context instanceof NodeObserverInterface) { - throw new RuntimeException(sprintf( - 'The ContextUpdateVisitor only supports instances of class '. - '"Symfony\Component\Validator\NodeVisitor\NodeObserverInterface". '. - 'An instance of class "%s" was given.', - get_class($context) - )); - } - $context->setCurrentNode($node); } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolvingVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/DefaultGroupReplacingVisitor.php similarity index 64% rename from src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolvingVisitor.php rename to src/Symfony/Component/Validator/NodeVisitor/DefaultGroupReplacingVisitor.php index 2d9b68737a1d0..6d152edf9c286 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/GroupSequenceResolvingVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/DefaultGroupReplacingVisitor.php @@ -18,11 +18,25 @@ use Symfony\Component\Validator\Node\Node; /** - * @since %%NextVersion%% + * Checks class nodes whether their "Default" group is replaced by a group + * sequence and adjusts the validation groups accordingly. + * + * If the "Default" group is replaced for a class node, and if the validated + * groups of the node contain the group "Default", that group is replaced by + * the group sequence specified in the class' metadata. + * + * @since 2.5 * @author Bernhard Schussek */ -class GroupSequenceResolvingVisitor extends AbstractVisitor +class DefaultGroupReplacingVisitor extends AbstractVisitor { + /** + * Replaces the "Default" group in the node's groups by the class' group + * sequence. + * + * @param Node $node The current node + * @param ExecutionContextInterface $context The execution context + */ public function visit(Node $node, ExecutionContextInterface $context) { if (!$node instanceof ClassNode) { @@ -30,16 +44,19 @@ public function visit(Node $node, ExecutionContextInterface $context) } 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(); - // TODO test if (!$groupSequence instanceof GroupSequence) { $groupSequence = new GroupSequence($groupSequence); } } else { + // The "Default" group is not overridden. Quit. return; } diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeObserverInterface.php b/src/Symfony/Component/Validator/NodeVisitor/NodeObserverInterface.php deleted file mode 100644 index 6588c09c90b90..0000000000000 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeObserverInterface.php +++ /dev/null @@ -1,23 +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\NodeVisitor; - -use Symfony\Component\Validator\Node\Node; - -/** - * @since %%NextVersion%% - * @author Bernhard Schussek - */ -interface NodeObserverInterface -{ - public function setCurrentNode(Node $node); -} diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index c688295da6ae4..e36f7880c8266 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -22,11 +22,19 @@ use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; /** - * @since %%NextVersion%% + * Validates a node's value against the constraints defined in it's metadata. + * + * @since 2.5 * @author Bernhard Schussek */ class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInterface { + /** + * Stores the hashes of each validated object together with the groups + * in which that object was already validated. + * + * @var array + */ private $validatedObjects = array(); private $validatedConstraints = array(); @@ -83,19 +91,19 @@ public function visit(Node $node, ExecutionContextInterface $context) // Use the object hash for group sequences $groupHash = is_object($group) ? spl_object_hash($group) : $group; - if (isset($this->validatedObjects[$objectHash][$groupHash])) { + if ($context->isObjectValidatedForGroup($objectHash, $groupHash)) { // Skip this group when validating properties unset($node->groups[$key]); continue; } - $this->validatedObjects[$objectHash][$groupHash] = true; + //$context->markObjectAsValidatedForGroup($objectHash, $groupHash); } // Validate normal group if (!$group instanceof GroupSequence) { - $this->validateNodeForGroup($objectHash, $node, $group, $context); + $this->validateNodeForGroup($node, $group, $context, $objectHash); continue; } @@ -142,20 +150,31 @@ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, } } - private function validateNodeForGroup($objectHash, Node $node, $group, ExecutionContextInterface $context) + private function validateNodeForGroup(Node $node, $group, ExecutionContextInterface $context, $objectHash) { try { $this->currentGroup = $group; foreach ($node->metadata->findConstraints($group) as $constraint) { - // Remember the validated constraints of each object to prevent - // duplicate validation of constraints that belong to multiple - // validated groups + // Prevent duplicate validation of constraints, in the case + // that constraints belong to multiple validated groups if (null !== $objectHash) { $constraintHash = spl_object_hash($constraint); - if (isset($this->validatedConstraints[$objectHash][$constraintHash])) { - continue; + 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); } $this->validatedConstraints[$objectHash][$constraintHash] = true; diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php index 012a64191c292..fe22a1f21969c 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php @@ -15,8 +15,17 @@ use Symfony\Component\Validator\Node\Node; /** - * @since %%NextVersion%% + * A node visitor invoked by the node traverser. + * + * At the beginning of the traversal, the method {@link beforeTraversal()} is + * called. For each traversed node, the method {@link visit()} is called. At + * last, the method {@link afterTraversal()} is called when the traversal is + * complete. + * + * @since 2.5 * @author Bernhard Schussek + * + * @see \Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface */ interface NodeVisitorInterface { diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php index e0317823d52c9..98f12cb954b35 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php @@ -129,6 +129,23 @@ public function testClosure() $this->validator->validate($object, $constraint); } + public function testClosureNullObject() + { + $constraint = new Callback(function ($object, ExecutionContext $context) { + $context->addViolation('My message', array('{{ value }}' => 'foobar'), 'invalidValue'); + + return false; + }); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('My message', array( + '{{ value }}' => 'foobar', + )); + + $this->validator->validate(null, $constraint); + } + public function testClosureExplicitName() { $object = new CallbackValidatorTest_Object(); @@ -163,6 +180,19 @@ public function testArrayCallable() $this->validator->validate($object, $constraint); } + public function testArrayCallableNullObject() + { + $constraint = new Callback(array(__CLASS__.'_Class', 'validateCallback')); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('Callback message', array( + '{{ value }}' => 'foobar', + )); + + $this->validator->validate(null, $constraint); + } + public function testArrayCallableExplicitName() { $object = new CallbackValidatorTest_Object(); diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 63b8c6a551060..2d34116e97fd0 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -66,25 +66,6 @@ protected function validatePropertyValue($object, $propertyName, $value, $groups return $this->validator->validatePropertyValue($object, $propertyName, $value, $groups); } - public function testNoDuplicateValidationIfConstraintInMultipleGroups() - { - $entity = new Entity(); - - $callback = function ($value, ExecutionContextInterface $context) { - $context->addViolation('Message'); - }; - - $this->metadata->addConstraint(new Callback(array( - 'callback' => $callback, - 'groups' => array('Group 1', 'Group 2'), - ))); - - $violations = $this->validator->validate($entity, new Valid(), array('Group 1', 'Group 2')); - - /** @var ConstraintViolationInterface[] $violations */ - $this->assertCount(1, $violations); - } - public function testGroupSequenceAbortsAfterFailedGroup() { $entity = new Entity(); @@ -441,4 +422,42 @@ public function testPropertyMetadataMustImplementPropertyMetadataInterface() $this->validator->validate($entity); } + + public function testNoDuplicateValidationIfClassConstraintInMultipleGroups() + { + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => array('Group 1', 'Group 2'), + ))); + + $violations = $this->validator->validate($entity, new Valid(), array('Group 1', 'Group 2')); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testNoDuplicateValidationIfPropertyConstraintInMultipleGroups() + { + $entity = new Entity(); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $this->metadata->addPropertyConstraint('firstName', new Callback(array( + 'callback' => $callback, + 'groups' => array('Group 1', 'Group 2'), + ))); + + $violations = $this->validator->validate($entity, new Valid(), array('Group 1', 'Group 2')); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index 5468642e7d3d3..c9a8fb509d7fa 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -16,7 +16,7 @@ use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; +use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; @@ -38,7 +38,7 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolvingVisitor(); + $groupSequenceResolver = new DefaultGroupReplacingVisitor(); $contextRefresher = new ContextUpdateVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index 3edd86b41054f..8ba44f697d2b6 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -16,7 +16,7 @@ use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; +use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\Validator\LegacyValidator; @@ -38,7 +38,7 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new LegacyExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolvingVisitor(); + $groupSequenceResolver = new DefaultGroupReplacingVisitor(); $contextRefresher = new ContextUpdateVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); diff --git a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php index 277d641b7aee4..fb6f6317c868a 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Validator2Dot5ApiTest.php @@ -16,7 +16,7 @@ use Symfony\Component\Validator\Context\ExecutionContextFactory; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; -use Symfony\Component\Validator\NodeVisitor\GroupSequenceResolvingVisitor; +use Symfony\Component\Validator\NodeVisitor\DefaultGroupReplacingVisitor; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\Validator\Validator; @@ -29,7 +29,7 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory) $nodeValidator = new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory()); $contextFactory = new ExecutionContextFactory($nodeValidator, new DefaultTranslator()); $validator = new Validator($contextFactory, $nodeTraverser, $metadataFactory); - $groupSequenceResolver = new GroupSequenceResolvingVisitor(); + $groupSequenceResolver = new DefaultGroupReplacingVisitor(); $contextRefresher = new ContextUpdateVisitor(); $nodeTraverser->addVisitor($groupSequenceResolver); From be7f055237bf877f72e1bafdd17ac4a2f9ad14b4 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 16:07:33 +0100 Subject: [PATCH 51/86] [Validator] Visitors may now abort the traversal by returning false from beforeTraversal() --- .../NodeTraverser/NodeTraverserInterface.php | 11 ++- .../NonRecursiveNodeTraverser.php | 84 ++++++++++++----- .../Validator/NodeVisitor/AbstractVisitor.php | 4 +- .../NodeVisitor/NodeValidationVisitor.php | 89 +++++++++++------- .../NodeVisitor/NodeVisitorInterface.php | 26 +++++- .../ObjectInitializationVisitor.php | 43 +++++++-- .../NonRecursiveNodeTraverserTest.php | 91 +++++++++++++++++++ 7 files changed, 280 insertions(+), 68 deletions(-) create mode 100644 src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php index 37c7c8c22c519..36e559ba6854e 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php @@ -25,10 +25,13 @@ * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::visit()} * of each visitor is called. At the end of the traversal, the traverser invokes * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::afterTraversal()} - * on each visitor. + * on each visitor. The visitors are called in the same order in which they are + * added to the traverser. * - * The visitors should be called in the same order in which they are added to - * the traverser. + * If the {@link traverse()} method is called recursively, the + * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::beforeTraversal()} + * and {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::afterTraversal()} + * methods of the visitors will be invoked for each call. * * The validation graph typically contains nodes of the following types: * @@ -92,5 +95,5 @@ public function removeVisitor(NodeVisitorInterface $visitor); * @param Node[] $nodes The nodes to traverse * @param ExecutionContextInterface $context The validation context */ - public function traverse(array $nodes, ExecutionContextInterface $context); + public function traverse($nodes, ExecutionContextInterface $context); } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php index b931b969431ad..811849929b31f 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php @@ -68,11 +68,6 @@ class NonRecursiveNodeTraverser implements NodeTraverserInterface */ private $metadataFactory; - /** - * @var Boolean - */ - private $traversalStarted = false; - /** * Creates a new traverser. * @@ -104,20 +99,20 @@ public function removeVisitor(NodeVisitorInterface $visitor) /** * {@inheritdoc} */ - public function traverse(array $nodes, ExecutionContextInterface $context) + public function traverse($nodes, ExecutionContextInterface $context) { - // beforeTraversal() and afterTraversal() are only executed for the - // top-level call of traverse() - $isTopLevelCall = !$this->traversalStarted; + if (!is_array($nodes)) { + $nodes = array($nodes); + } - if ($isTopLevelCall) { - // Remember that the traversal was already started for the case of - // recursive calls to traverse() - $this->traversalStarted = true; + $numberOfInitializedVisitors = $this->beforeTraversal($nodes, $context); - foreach ($this->visitors as $visitor) { - $visitor->beforeTraversal($nodes, $context); - } + // If any of the visitors requested to abort the traversal, do so, but + // clean up before + if ($numberOfInitializedVisitors < count($this->visitors)) { + $this->afterTraversal($nodes, $context, $numberOfInitializedVisitors); + + return; } // This stack contains all the nodes that should be traversed @@ -148,13 +143,60 @@ public function traverse(array $nodes, ExecutionContextInterface $context) } } - if ($isTopLevelCall) { - foreach ($this->visitors as $visitor) { - $visitor->afterTraversal($nodes, $context); + $this->afterTraversal($nodes, $context); + } + + /** + * Executes the {@link NodeVisitorInterface::beforeTraversal()} method of + * each visitor. + * + * @param Node[] $nodes The traversed nodes + * @param ExecutionContextInterface $context The current execution context + * + * @return integer The number of successful calls. This is lower than + * the number of visitors if any of the visitors' + * beforeTraversal() methods returned false + */ + private function beforeTraversal($nodes, ExecutionContextInterface $context) + { + $numberOfCalls = 1; + + foreach ($this->visitors as $visitor) { + if (false === $visitor->beforeTraversal($nodes, $context)) { + break; } - // Put the traverser back into its initial state - $this->traversalStarted = false; + ++$numberOfCalls; + } + + return $numberOfCalls; + } + + /** + * Executes the {@link NodeVisitorInterface::beforeTraversal()} method of + * each visitor. + * + * @param Node[] $nodes The traversed nodes + * @param ExecutionContextInterface $context The current execution context + * @param integer|null $limit Limits the number of visitors + * on which beforeTraversal() + * should be called. All visitors + * will be called by default + */ + private function afterTraversal($nodes, ExecutionContextInterface $context, $limit = null) + { + if (null === $limit) { + $limit = count($this->visitors); + } + + $numberOfCalls = 0; + + foreach ($this->visitors as $visitor) { + $visitor->afterTraversal($nodes, $context); + + if (++$numberOfCalls === $limit) { + return; + } } } diff --git a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php index 3f15359878ed5..c516f4a8d233b 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php @@ -27,14 +27,14 @@ abstract class AbstractVisitor implements NodeVisitorInterface /** * {@inheritdoc} */ - public function beforeTraversal(array $nodes, ExecutionContextInterface $context) + public function beforeTraversal($nodes, ExecutionContextInterface $context) { } /** * {@inheritdoc} */ - public function afterTraversal(array $nodes, ExecutionContextInterface $context) + public function afterTraversal($nodes, ExecutionContextInterface $context) { } diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index e36f7880c8266..cc6f5780785ff 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -29,16 +29,6 @@ */ class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInterface { - /** - * Stores the hashes of each validated object together with the groups - * in which that object was already validated. - * - * @var array - */ - private $validatedObjects = array(); - - private $validatedConstraints = array(); - /** * @var ConstraintValidatorFactoryInterface */ @@ -49,20 +39,37 @@ class NodeValidationVisitor extends AbstractVisitor implements GroupManagerInter */ private $nodeTraverser; + /** + * The currently validated group. + * + * @var string + */ private $currentGroup; + /** + * Creates a new visitor. + * + * @param NodeTraverserInterface $nodeTraverser The node traverser + * @param ConstraintValidatorFactoryInterface $validatorFactory The validator factory + */ public function __construct(NodeTraverserInterface $nodeTraverser, ConstraintValidatorFactoryInterface $validatorFactory) { $this->validatorFactory = $validatorFactory; $this->nodeTraverser = $nodeTraverser; } - public function afterTraversal(array $nodes, ExecutionContextInterface $context) - { - $this->validatedObjects = array(); - $this->validatedConstraints = array(); - } - + /** + * Validates a node's value against the constraints defined in the node's + * metadata. + * + * Objects and constraints that were validated before in the same context + * will be skipped. + * + * @param Node $node The current node + * @param ExecutionContextInterface $context The execution context + * + * @return Boolean Whether to traverse the successor nodes + */ public function visit(Node $node, ExecutionContextInterface $context) { if ($node instanceof CollectionNode) { @@ -84,21 +91,24 @@ public function visit(Node $node, ExecutionContextInterface $context) // simply continue traversal (if possible) foreach ($node->groups as $key => $group) { - // Remember which object was validated for which group - // Skip validation if the object was already validated for this - // 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 properties + // Skip this group when validating the successor nodes + // (property and/or collection nodes) unset($node->groups[$key]); continue; } - //$context->markObjectAsValidatedForGroup($objectHash, $groupHash); + $context->markObjectAsValidatedForGroup($objectHash, $groupHash); } // Validate normal group @@ -108,27 +118,34 @@ public function visit(Node $node, ExecutionContextInterface $context) continue; } - // Skip the group sequence when validating properties - unset($node->groups[$key]); - // Traverse group sequence until a violation is generated $this->traverseGroupSequence($node, $group, $context); - // Optimization: If the groups only contain the group sequence, - // we can skip the traversal for the properties of the object - if (1 === count($node->groups)) { - return false; - } + // Skip the group sequence when validating successor nodes + unset($node->groups[$key]); } return true; } + /** + * {@inheritdoc} + */ public function getCurrentGroup() { return $this->currentGroup; } + /** + * Validates a node's value in each group of a group sequence. + * + * If any of the groups' constraints generates a violation, subsequent + * groups are not validated anymore. + * + * @param Node $node The validated node + * @param GroupSequence $groupSequence The group sequence + * @param ExecutionContextInterface $context The execution context + */ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); @@ -150,6 +167,17 @@ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, } } + /** + * 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 { @@ -176,8 +204,6 @@ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterf $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); } - - $this->validatedConstraints[$objectHash][$constraintHash] = true; } $validator = $this->validatorFactory->getInstance($constraint); @@ -187,6 +213,7 @@ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterf $this->currentGroup = null; } catch (\Exception $e) { + // Should be put into a finally block once we switch to PHP 5.5 $this->currentGroup = null; throw $e; diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php index fe22a1f21969c..ec05923f1bd5e 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php @@ -29,9 +29,31 @@ */ interface NodeVisitorInterface { - public function beforeTraversal(array $nodes, ExecutionContextInterface $context); + /** + * Called at the beginning of a traversal. + * + * @param Node[] $nodes A list of Node instances + * @param ExecutionContextInterface $context The execution context + * + * @return Boolean Whether to continue the traversal + */ + public function beforeTraversal($nodes, ExecutionContextInterface $context); - public function afterTraversal(array $nodes, ExecutionContextInterface $context); + /** + * Called at the end of a traversal. + * + * @param Node[] $nodes A list of Node instances + * @param ExecutionContextInterface $context The execution context + */ + public function afterTraversal($nodes, ExecutionContextInterface $context); + /** + * Called for each node during a traversal. + * + * @param Node $node The current node + * @param ExecutionContextInterface $context The execution context + * + * @return Boolean Whether to traverse the node's successor nodes + */ public function visit(Node $node, ExecutionContextInterface $context); } diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php index 05226ac6d359b..18fe19a955d71 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php @@ -12,12 +12,18 @@ namespace Symfony\Component\Validator\NodeVisitor; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Node\ClassNode; use Symfony\Component\Validator\Node\Node; use Symfony\Component\Validator\ObjectInitializerInterface; /** - * @since %%NextVersion%% + * Initializes the objects of all class nodes. + * + * You have to pass at least one instance of {@link ObjectInitializerInterface} + * to the constructor of this visitor. + * + * @since 2.5 * @author Bernhard Schussek */ class ObjectInitializationVisitor extends AbstractVisitor @@ -27,30 +33,51 @@ class ObjectInitializationVisitor extends AbstractVisitor */ private $initializers; + /** + * Creates a new visitor. + * + * @param ObjectInitializerInterface[] $initializers The object initializers + * + * @throws InvalidArgumentException + */ public function __construct(array $initializers) { foreach ($initializers as $initializer) { if (!$initializer instanceof ObjectInitializerInterface) { - throw new \InvalidArgumentException('Validator initializers must implement ObjectInitializerInterface.'); + throw new InvalidArgumentException(sprintf( + 'Validator initializers must implement '. + '"Symfony\Component\Validator\ObjectInitializerInterface". '. + 'Got: "%s"', + is_object($initializer) ? get_class($initializer) : gettype($initializer) + )); } } // If no initializer is present, this visitor should not even be created if (0 === count($initializers)) { - throw new \InvalidArgumentException('Please pass at least one initializer.'); + throw new InvalidArgumentException('Please pass at least one initializer.'); } $this->initializers = $initializers; } + /** + * Calls the {@link ObjectInitializerInterface::initialize()} method for + * the object of each class node. + * + * @param Node $node The current node + * @param ExecutionContextInterface $context The execution context + * + * @return Boolean Always returns true + */ public function visit(Node $node, ExecutionContextInterface $context) { - if (!$node instanceof ClassNode) { - return; + if ($node instanceof ClassNode) { + foreach ($this->initializers as $initializer) { + $initializer->initialize($node->value); + } } - foreach ($this->initializers as $initializer) { - $initializer->initialize($node->value); - } + return true; } } diff --git a/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php b/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php new file mode 100644 index 0000000000000..4dfc7071248e9 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\NodeTraverser; + +use Symfony\Component\Validator\Mapping\GenericMetadata; +use Symfony\Component\Validator\Node\GenericNode; +use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; +use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; + +/** + * @since 2.5 + * @author Bernhard Schussek + */ +class NonRecursiveNodeTraverserTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var FakeMetadataFactory + */ + private $metadataFactory; + + /** + * @var NonRecursiveNodeTraverser + */ + private $traverser; + + protected function setUp() + { + $this->metadataFactory = new FakeMetadataFactory(); + $this->traverser = new NonRecursiveNodeTraverser($this->metadataFactory); + } + + public function testVisitorsMayPreventTraversal() + { + $nodes = array(new GenericNode('value', new GenericMetadata(), '', array('Default'))); + $context = $this->getMock('Symfony\Component\Validator\Context\ExecutionContextInterface'); + + $visitor1 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface'); + $visitor2 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface'); + $visitor3 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface'); + + $visitor1->expects($this->once()) + ->method('beforeTraversal') + ->with($nodes, $context); + + // abort traversal + $visitor2->expects($this->once()) + ->method('beforeTraversal') + ->with($nodes, $context) + ->will($this->returnValue(false)); + + // never called + $visitor3->expects($this->never()) + ->method('beforeTraversal'); + + $visitor1->expects($this->never()) + ->method('visit'); + $visitor2->expects($this->never()) + ->method('visit'); + $visitor2->expects($this->never()) + ->method('visit'); + + // called in order to clean up + $visitor1->expects($this->once()) + ->method('afterTraversal') + ->with($nodes, $context); + + // abort traversal + $visitor2->expects($this->once()) + ->method('afterTraversal') + ->with($nodes, $context); + + // never called, because beforeTraversal() wasn't called either + $visitor3->expects($this->never()) + ->method('afterTraversal'); + + $this->traverser->addVisitor($visitor1); + $this->traverser->addVisitor($visitor2); + $this->traverser->addVisitor($visitor3); + + $this->traverser->traverse($nodes, $context); + } +} From 9986f03ce25143bb358f3367ecb6a56b7d6d6743 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 16:10:43 +0100 Subject: [PATCH 52/86] [Validator] Added inline documentation for the PropertyPath utility class --- .../Component/Validator/Util/PropertyPath.php | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Util/PropertyPath.php b/src/Symfony/Component/Validator/Util/PropertyPath.php index bf33b50b5e4a6..c8f20e7200c26 100644 --- a/src/Symfony/Component/Validator/Util/PropertyPath.php +++ b/src/Symfony/Component/Validator/Util/PropertyPath.php @@ -12,11 +12,29 @@ namespace Symfony\Component\Validator\Util; /** - * @since %%NextVersion%% + * Contains utility methods for dealing with property paths. + * + * For more extensive functionality, use Symfony's PropertyAccess component. + * + * @since 2.5 * @author Bernhard Schussek */ class PropertyPath { + /** + * Appends a path to a given property path. + * + * If the base path is empty, the appended path will be returned unchanged. + * If the base path is not empty, and the appended path starts with a + * squared opening bracket ("["), the concatenation of the two paths is + * returned. Otherwise, the concatenation of the two paths is returned, + * separated by a dot ("."). + * + * @param string $basePath The base path + * @param string $subPath The path to append + * + * @return string The concatenation of the two property paths + */ public static function append($basePath, $subPath) { if ('' !== (string) $subPath) { @@ -30,6 +48,9 @@ public static function append($basePath, $subPath) return $basePath; } + /** + * Not instantiable. + */ private function __construct() { } From 524a9538bc8678a8af9625faaa92622cad1ded64 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 16:27:31 +0100 Subject: [PATCH 53/86] [Validator] Improved inline documentation of the validators --- .../Validator/ContextualValidator.php | 49 ++++++++-- .../ContextualValidatorInterface.php | 57 +++++++----- .../Validator/Validator/LegacyValidator.php | 5 + .../Validator/Validator/Validator.php | 23 ++++- .../Validator/ValidatorInterface.php | 91 +++++++++++++------ 5 files changed, 164 insertions(+), 61 deletions(-) diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidator.php b/src/Symfony/Component/Validator/Validator/ContextualValidator.php index cd969cc5b152e..b250278941fd8 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidator.php @@ -24,7 +24,9 @@ use Symfony\Component\Validator\Util\PropertyPath; /** - * @since %%NextVersion%% + * Default implementation of {@link ContextualValidatorInterface}. + * + * @since 2.5 * @author Bernhard Schussek */ class ContextualValidator implements ContextualValidatorInterface @@ -44,6 +46,15 @@ class ContextualValidator implements ContextualValidatorInterface */ private $metadataFactory; + /** + * Creates a validator for the given context. + * + * @param ExecutionContextInterface $context The execution context + * @param NodeTraverserInterface $nodeTraverser The node traverser + * @param MetadataFactoryInterface $metadataFactory The factory for fetching + * the metadata of validated + * objects + */ public function __construct(ExecutionContextInterface $context, NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory) { $this->context = $context; @@ -53,13 +64,19 @@ public function __construct(ExecutionContextInterface $context, NodeTraverserInt $this->metadataFactory = $metadataFactory; } - public function atPath($subPath) + /** + * {@inheritdoc} + */ + public function atPath($path) { - $this->defaultPropertyPath = $this->context->getPropertyPath($subPath); + $this->defaultPropertyPath = $this->context->getPropertyPath($path); return $this; } + /** + * {@inheritdoc} + */ public function validate($value, $constraints = null, $groups = null) { if (null === $constraints) { @@ -84,6 +101,9 @@ public function validate($value, $constraints = null, $groups = null) return $this; } + /** + * {@inheritdoc} + */ public function validateProperty($object, $propertyName, $groups = null) { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -118,6 +138,9 @@ public function validateProperty($object, $propertyName, $groups = null) return $this; } + /** + * {@inheritdoc} + */ public function validatePropertyValue($object, $propertyName, $value, $groups = null) { $classMetadata = $this->metadataFactory->getMetadataFor($object); @@ -151,6 +174,21 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = 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)) { @@ -159,9 +197,4 @@ protected function normalizeGroups($groups) return array($groups); } - - public function getViolations() - { - return $this->context->getViolations(); - } } diff --git a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php index e384228d7c977..83b5d0712098e 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ContextualValidatorInterface.php @@ -15,17 +15,24 @@ use Symfony\Component\Validator\ConstraintViolationListInterface; /** - * @since %%NextVersion%% + * A validator in a specific execution context. + * + * @since 2.5 * @author Bernhard Schussek */ interface ContextualValidatorInterface { /** - * @param string $subPath + * Appends the given path to the property path of the context. + * + * If called multiple times, the path will always be reset to the context's + * original path with the given path appended to it. + * + * @param string $path The path to append * * @return ContextualValidatorInterface This validator */ - public function atPath($subPath); + public function atPath($path); /** * Validates a value against a constraint or a list of constraints. @@ -33,46 +40,50 @@ public function atPath($subPath); * If no constraint is passed, the constraint * {@link \Symfony\Component\Validator\Constraints\Valid} is assumed. * - * @param mixed $value The value to validate. - * @param Constraint|Constraint[] $constraints The constraint(s) to validate against. - * @param array|null $groups The validation groups to validate. + * @param mixed $value The value to validate + * @param Constraint|Constraint[] $constraints The constraint(s) to validate + * against + * @param array|null $groups The validation groups to + * validate. If none is given, + * "Default" is assumed * * @return ContextualValidatorInterface This validator */ public function validate($value, $constraints = null, $groups = null); /** - * Validates a property of a value against its current value. + * Validates a property of an object against the constraints specified + * for this property. * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. - * - * @param mixed $object The value containing the property. - * @param string $propertyName The name of the property to validate. - * @param array|null $groups The validation groups to validate. + * @param object $object The object + * @param string $propertyName The name of the validated property + * @param array|null $groups The validation groups to validate. If + * none is given, "Default" is assumed * * @return ContextualValidatorInterface This validator */ public function validateProperty($object, $propertyName, $groups = null); /** - * Validate a property of a value against a potential value. - * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. + * Validates a value against the constraints specified for an object's + * property. * - * @param string $object The value containing the property. - * @param string $propertyName The name of the property to validate - * @param string $value The value to validate against the - * constraints of the property. - * @param array|null $groups The validation groups to validate. + * @param object $object The object + * @param string $propertyName The name of the property + * @param mixed $value The value to validate against the + * property's constraints + * @param array|null $groups The validation groups to validate. If + * none is given, "Default" is assumed * * @return ContextualValidatorInterface This validator */ public function validatePropertyValue($object, $propertyName, $value, $groups = null); /** - * @return ConstraintViolationListInterface + * Returns the violations that have been generated so far in the context + * of the validator. + * + * @return ConstraintViolationListInterface The constraint violations */ public function getViolations(); } diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 9ecf522f85633..eae4746e886f4 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -17,9 +17,14 @@ use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; /** + * A validator that supports both the API of Symfony < 2.5 and Symfony 2.5+. + * * @since 2.5 * @author Bernhard Schussek * + * @see \Symfony\Component\Validator\ValidatorInterface + * @see \Symfony\Component\Validator\Validator\ValidatorInterface + * * @deprecated Implemented for backwards compatibility with Symfony < 2.5. * To be removed in Symfony 3.0. */ diff --git a/src/Symfony/Component/Validator/Validator/Validator.php b/src/Symfony/Component/Validator/Validator/Validator.php index fad080155af5f..a73a72ebb5884 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/Validator.php @@ -17,7 +17,9 @@ use Symfony\Component\Validator\MetadataFactoryInterface; /** - * @since %%NextVersion%% + * Default implementation of {@link ValidatorInterface}. + * + * @since 2.5 * @author Bernhard Schussek */ class Validator implements ValidatorInterface @@ -37,6 +39,16 @@ class Validator implements ValidatorInterface */ protected $metadataFactory; + /** + * Creates a new validator. + * + * @param ExecutionContextFactoryInterface $contextFactory The factory for + * creating new contexts + * @param NodeTraverserInterface $nodeTraverser The node traverser + * @param MetadataFactoryInterface $metadataFactory The factory for + * fetching the metadata + * of validated objects + */ public function __construct(ExecutionContextFactoryInterface $contextFactory, NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory) { $this->contextFactory = $contextFactory; @@ -84,6 +96,9 @@ public function hasMetadataFor($object) return $this->metadataFactory->hasMetadataFor($object); } + /** + * {@inheritdoc} + */ public function validate($value, $constraints = null, $groups = null) { return $this->startContext($value) @@ -91,6 +106,9 @@ public function validate($value, $constraints = null, $groups = null) ->getViolations(); } + /** + * {@inheritdoc} + */ public function validateProperty($object, $propertyName, $groups = null) { return $this->startContext($object) @@ -98,6 +116,9 @@ public function validateProperty($object, $propertyName, $groups = null) ->getViolations(); } + /** + * {@inheritdoc} + */ public function validatePropertyValue($object, $propertyName, $value, $groups = null) { return $this->startContext($object) diff --git a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php index 124514a1ffaf7..163d1b6ca6b36 100644 --- a/src/Symfony/Component/Validator/Validator/ValidatorInterface.php +++ b/src/Symfony/Component/Validator/Validator/ValidatorInterface.php @@ -16,7 +16,9 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; /** - * @since %%NextVersion%% + * Validates PHP values against constraints. + * + * @since 2.5 * @author Bernhard Schussek */ interface ValidatorInterface @@ -27,60 +29,91 @@ interface ValidatorInterface * If no constraint is passed, the constraint * {@link \Symfony\Component\Validator\Constraints\Valid} is assumed. * - * @param mixed $value The value to validate. - * @param Constraint|Constraint[] $constraints The constraint(s) to validate against. - * @param array|null $groups The validation groups to validate. + * @param mixed $value The value to validate + * @param Constraint|Constraint[] $constraints The constraint(s) to validate + * against + * @param array|null $groups The validation groups to + * validate. If none is given, + * "Default" is assumed * - * @return ConstraintViolationListInterface A list of constraint violations. If the - * list is empty, validation succeeded. + * @return ConstraintViolationListInterface A list of constraint violations. + * If the list is empty, validation + * succeeded */ public function validate($value, $constraints = null, $groups = null); /** - * Validates a property of a value against its current value. - * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. + * Validates a property of an object against the constraints specified + * for this property. * - * @param mixed $object The value containing the property. - * @param string $propertyName The name of the property to validate. - * @param array|null $groups The validation groups to validate. + * @param object $object The object + * @param string $propertyName The name of the validated property + * @param array|null $groups The validation groups to validate. If + * none is given, "Default" is assumed * - * @return ConstraintViolationListInterface A list of constraint violations. If the - * list is empty, validation succeeded. + * @return ConstraintViolationListInterface A list of constraint violations. + * If the list is empty, validation + * succeeded */ public function validateProperty($object, $propertyName, $groups = null); /** - * Validate a property of a value against a potential value. + * Validates a value against the constraints specified for an object's + * property. * - * The accepted values depend on the {@link MetadataFactoryInterface} - * implementation. + * @param object $object The object + * @param string $propertyName The name of the property + * @param mixed $value The value to validate against the + * property's constraints + * @param array|null $groups The validation groups to validate. If + * none is given, "Default" is assumed * - * @param string $object The value containing the property. - * @param string $propertyName The name of the property to validate - * @param string $value The value to validate against the - * constraints of the property. - * @param array|null $groups The validation groups to validate. - * - * @return ConstraintViolationListInterface A list of constraint violations. If the - * list is empty, validation succeeded. + * @return ConstraintViolationListInterface A list of constraint violations. + * If the list is empty, validation + * succeeded */ public function validatePropertyValue($object, $propertyName, $value, $groups = null); /** - * @return ContextualValidatorInterface + * Starts a new validation context and returns a validator for that context. + * + * The returned validator collects all violations generated within its + * context. You can access these violations with the + * {@link ContextualValidatorInterface::getViolations()} method. + * + * @return ContextualValidatorInterface The validator for the new context */ public function startContext(); /** - * @param ExecutionContextInterface $context + * Returns a validator in the given execution context. + * + * The returned validator adds all generated violations to the given + * context. * - * @return ContextualValidatorInterface + * @param ExecutionContextInterface $context The execution context + * + * @return ContextualValidatorInterface The validator for that context */ public function inContext(ExecutionContextInterface $context); + /** + * Returns the metadata for an object. + * + * @param object $object The object + * + * @return \Symfony\Component\Validator\Mapping\MetadataInterface The metadata + * + * @throws \Symfony\Component\Validator\Exception\NoSuchMetadataException If no metadata exists + */ public function getMetadataFor($object); + /** + * Returns whether the validator has metadata for an object. + * + * @param object $object The object + * + * @return Boolean Whether metadata exists for that object + */ public function hasMetadataFor($object); } From 9ca61df9234afe72a7bf02a7e48646dfa74f6041 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 16:40:31 +0100 Subject: [PATCH 54/86] [Validator] Improved inline documentation of CascadingStrategy and TraversalStrategy --- .../Validator/Mapping/CascadingStrategy.php | 32 +++++++++++++++++-- .../Validator/Mapping/TraversalStrategy.php | 32 +++++++++++++++++-- .../NonRecursiveNodeTraverser.php | 2 ++ 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/Validator/Mapping/CascadingStrategy.php b/src/Symfony/Component/Validator/Mapping/CascadingStrategy.php index 1218c2d484c38..ff2853f4e0415 100644 --- a/src/Symfony/Component/Validator/Mapping/CascadingStrategy.php +++ b/src/Symfony/Component/Validator/Mapping/CascadingStrategy.php @@ -12,15 +12,41 @@ namespace Symfony\Component\Validator\Mapping; /** - * @since %%NextVersion%% + * Specifies whether an object should be cascaded. + * + * Cascading is relevant for any node type but class nodes. If such a node + * contains an object of value, and if cascading is enabled, then the node + * traverser will try to find class metadata for that object and validate the + * object against that metadata. + * + * If no metadata is found for a cascaded object, and if that object implements + * {@link \Traversable}, the node traverser will iterate over the object and + * cascade each object or collection contained within, unless iteration is + * prohibited by the specified {@link TraversalStrategy}. + * + * Although the constants currently represent a boolean switch, they are + * implemented as bit mask in order to allow future extensions. + * + * @since 2.5 * @author Bernhard Schussek + * + * @see TraversalStrategy */ class CascadingStrategy { - const NONE = 0; + /** + * Specifies that a node should not be cascaded. + */ + const NONE = 1; - const CASCADE = 1; + /** + * Specifies that a node should be cascaded. + */ + const CASCADE = 2; + /** + * Not instantiable. + */ private function __construct() { } diff --git a/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php index 22b7f534550a2..7d74be16255f9 100644 --- a/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php +++ b/src/Symfony/Component/Validator/Mapping/TraversalStrategy.php @@ -12,22 +12,50 @@ namespace Symfony\Component\Validator\Mapping; /** - * @since %%NextVersion%% + * Specifies whether and how a traversable object should be traversed. + * + * If the node traverser traverses a node whose value is an instance of + * {@link \Traversable}, and if that node is either a class node or if + * cascading is enabled, then the node's traversal strategy will be checked. + * Depending on the requested traversal strategy, the node traverser will + * iterate over the object and cascade each object or collection returned by + * the iterator. + * + * The traversal strategy is ignored for arrays. Arrays are always iterated. + * + * @since 2.1 * @author Bernhard Schussek + * + * @see CascadingStrategy */ class TraversalStrategy { /** - * @var integer + * Specifies that a node's value should be iterated only if it is an + * instance of {@link \Traversable}. */ const IMPLICIT = 1; + /** + * Specifies that a node's value should never be iterated. + */ const NONE = 2; + /** + * Specifies that a node's value should always be iterated. If the value is + * not an instance of {@link \Traversable}, an exception should be thrown. + */ const TRAVERSE = 4; + /** + * Specifies that nested instances of {@link \Traversable} should never be + * iterated. Can be combined with {@link IMPLICIT} or {@link TRAVERSE}. + */ const STOP_RECURSION = 8; + /** + * Not instantiable. + */ private function __construct() { } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php index 811849929b31f..5c904f01d47f7 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php @@ -55,6 +55,8 @@ * @author Bernhard Schussek * * @see NodeTraverserInterface + * @see CascadingStrategy + * @see TraversalStrategy */ class NonRecursiveNodeTraverser implements NodeTraverserInterface { From 01ceeda376f6e6ff43e78918474a8d2e6a8c83e6 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 17:10:39 +0100 Subject: [PATCH 55/86] [Validator] Improved test coverage of the Traverse constraint --- .../Validator/Constraints/Traverse.php | 2 - .../Component/Validator/Constraints/Valid.php | 10 +- .../Context/LegacyExecutionContext.php | 13 +- .../Validator/Mapping/ClassMetadata.php | 12 +- .../Validator/Mapping/GenericMetadata.php | 13 ++ .../Validator/Mapping/MemberMetadata.php | 15 -- .../Tests/Validator/Abstract2Dot5ApiTest.php | 194 +++++++++++++++++- 7 files changed, 214 insertions(+), 45 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/Traverse.php b/src/Symfony/Component/Validator/Constraints/Traverse.php index 6c220561917f9..4abae6c67ab34 100644 --- a/src/Symfony/Component/Validator/Constraints/Traverse.php +++ b/src/Symfony/Component/Validator/Constraints/Traverse.php @@ -24,8 +24,6 @@ class Traverse extends Constraint { public $traverse = true; - public $deep = false; - public function __construct($options = null) { if (is_array($options) && array_key_exists('groups', $options)) { diff --git a/src/Symfony/Component/Validator/Constraints/Valid.php b/src/Symfony/Component/Validator/Constraints/Valid.php index 6ad09624b57d4..fe50d2d24167b 100644 --- a/src/Symfony/Component/Validator/Constraints/Valid.php +++ b/src/Symfony/Component/Validator/Constraints/Valid.php @@ -25,6 +25,9 @@ class Valid extends Constraint { public $traverse = true; + /** + * @deprecated Deprecated as of version 2.5, to be removed in Symfony 3.0. + */ public $deep = true; public function __construct($options = null) @@ -38,11 +41,4 @@ public function __construct($options = null) parent::__construct($options); } - - public function getDefaultOption() - { - // Traverse is extended for backwards compatibility reasons - // The parent class should be removed in 3.0 - return null; - } } diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index b84d1c379c22f..1a147e441cf00 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -111,10 +111,8 @@ public function addViolationAt($subPath, $message, array $parameters = array(), public function validate($value, $subPath = '', $groups = null, $traverse = false, $deep = false) { if (is_array($value)) { - $constraint = new Traverse(array( - 'traverse' => true, - 'deep' => $deep, - )); + // The $traverse flag is ignored for arrays + $constraint = new Valid(array('traverse' => true, 'deep' => $deep)); return $this ->getValidator() @@ -125,16 +123,13 @@ public function validate($value, $subPath = '', $groups = null, $traverse = fals } if ($traverse && $value instanceof \Traversable) { - $constraints = array( - new Valid(), - new Traverse(array('traverse' => true, 'deep' => $deep)), - ); + $constraint = new Valid(array('traverse' => true, 'deep' => $deep)); return $this ->getValidator() ->inContext($this) ->atPath($subPath) - ->validate($value, $constraints, $groups) + ->validate($value, $constraint, $groups) ; } diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 30c7b1ad3b04c..85e872026d60b 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -201,20 +201,12 @@ public function addConstraint(Constraint $constraint) } if ($constraint instanceof Traverse) { - if (true === $constraint->traverse) { + if ($constraint->traverse) { // If traverse is true, traversal should be explicitly enabled $this->traversalStrategy = TraversalStrategy::TRAVERSE; - - if (!$constraint->deep) { - $this->traversalStrategy |= TraversalStrategy::STOP_RECURSION; - } - } elseif (false === $constraint->traverse) { + } else { // If traverse is false, traversal should be explicitly disabled $this->traversalStrategy = TraversalStrategy::NONE; - } else { - // Else, traverse depending on the contextual information that - // is available during validation - $this->traversalStrategy = TraversalStrategy::IMPLICIT; } // The constraint is not added diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index f77e11736a464..6c4f1869918cc 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -12,8 +12,10 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Exception\BadMethodCallException; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\ValidationVisitorInterface; /** @@ -103,9 +105,20 @@ public function __clone() * @param Constraint $constraint The constraint to add * * @return GenericMetadata This object + * + * @throws ConstraintDefinitionException When trying to add the + * {@link Traverse} constraint */ public function addConstraint(Constraint $constraint) { + if ($constraint instanceof Traverse) { + throw new ConstraintDefinitionException(sprintf( + 'The constraint "%s" can only be put on classes. Please use '. + '"Symfony\Component\Validator\Constraints\Valid" instead.', + get_class($constraint) + )); + } + if ($constraint instanceof Valid) { $this->cascadingStrategy = CascadingStrategy::CASCADE; diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 6aa0ead2ce683..60dc417024137 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -58,21 +58,6 @@ public function addConstraint(Constraint $constraint) )); } - // BC with Symfony < 2.5 - if ($constraint instanceof Valid) { - if (true === $constraint->traverse) { - // Try to traverse cascaded objects, but ignore if they do not - // implement Traversable - $this->traversalStrategy = TraversalStrategy::IMPLICIT; - - if (!$constraint->deep) { - $this->traversalStrategy |= TraversalStrategy::STOP_RECURSION; - } - } elseif (false === $constraint->traverse) { - $this->traversalStrategy = TraversalStrategy::NONE; - } - } - parent::addConstraint($constraint); return $this; diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 2d34116e97fd0..4559a3ad32e01 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -328,18 +328,208 @@ public function testTraverseTraversableByDefault() $this->assertNull($violations[0]->getCode()); } + public function testTraversalEnabledOnClass() + { + $entity = new Entity(); + $traversable = new \ArrayIterator(array('key' => $entity)); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $traversableMetadata = new ClassMetadata('ArrayIterator'); + $traversableMetadata->addConstraint(new Traverse(true)); + + $this->metadataFactory->addMetadata($traversableMetadata); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validate($traversable, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testTraversalDisabledOnClass() + { + $test = $this; + $entity = new Entity(); + $traversable = new \ArrayIterator(array('key' => $entity)); + + $callback = function ($value, ExecutionContextInterface $context) use ($test) { + $test->fail('Should not be called'); + }; + + $traversableMetadata = new ClassMetadata('ArrayIterator'); + $traversableMetadata->addConstraint(new Traverse(false)); + + $this->metadataFactory->addMetadata($traversableMetadata); + $this->metadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + + $violations = $this->validate($traversable, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + /** * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException */ - public function testExpectTraversableIfTraverseOnClass() + public function testExpectTraversableIfTraversalEnabledOnClass() { $entity = new Entity(); - $this->metadata->addConstraint(new Traverse()); + $this->metadata->addConstraint(new Traverse(true)); $this->validator->validate($entity); } + public function testReferenceTraversalDisabledOnClass() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array('key' => new Reference())); + + $callback = function ($value, ExecutionContextInterface $context) use ($test) { + $test->fail('Should not be called'); + }; + + $traversableMetadata = new ClassMetadata('ArrayIterator'); + $traversableMetadata->addConstraint(new Traverse(false)); + + $this->metadataFactory->addMetadata($traversableMetadata); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $this->metadata->addPropertyConstraint('reference', new Valid()); + + $violations = $this->validate($entity, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + + public function testReferenceTraversalEnabledOnReferenceDisabledOnClass() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array('key' => new Reference())); + + $callback = function ($value, ExecutionContextInterface $context) use ($test) { + $test->fail('Should not be called'); + }; + + $traversableMetadata = new ClassMetadata('ArrayIterator'); + $traversableMetadata->addConstraint(new Traverse(false)); + + $this->metadataFactory->addMetadata($traversableMetadata); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'traverse' => true, + ))); + + $violations = $this->validate($entity, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + + public function testReferenceTraversalDisabledOnReferenceEnabledOnClass() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array('key' => new Reference())); + + $callback = function ($value, ExecutionContextInterface $context) use ($test) { + $test->fail('Should not be called'); + }; + + $traversableMetadata = new ClassMetadata('ArrayIterator'); + $traversableMetadata->addConstraint(new Traverse(true)); + + $this->metadataFactory->addMetadata($traversableMetadata); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'traverse' => false, + ))); + + $violations = $this->validate($entity, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + + public function testReferenceTraversalRecursionEnabledOnReferenceTraversalEnabledOnClass() + { + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => new Reference())), + )); + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message'); + }; + + $traversableMetadata = new ClassMetadata('ArrayIterator'); + $traversableMetadata->addConstraint(new Traverse(true)); + + $this->metadataFactory->addMetadata($traversableMetadata); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'deep' => true, + ))); + + $violations = $this->validate($entity, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(1, $violations); + } + + public function testReferenceTraversalRecursionDisabledOnReferenceTraversalEnabledOnClass() + { + $test = $this; + $entity = new Entity(); + $entity->reference = new \ArrayIterator(array( + 2 => new \ArrayIterator(array('key' => new Reference())), + )); + + $callback = function ($value, ExecutionContextInterface $context) use ($test) { + $test->fail('Should not be called'); + }; + + $traversableMetadata = new ClassMetadata('ArrayIterator'); + $traversableMetadata->addConstraint(new Traverse(true)); + + $this->metadataFactory->addMetadata($traversableMetadata); + $this->referenceMetadata->addConstraint(new Callback(array( + 'callback' => $callback, + 'groups' => 'Group', + ))); + $this->metadata->addPropertyConstraint('reference', new Valid(array( + 'deep' => false, + ))); + + $violations = $this->validate($entity, new Valid(), 'Group'); + + /** @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + public function testAddCustomizedViolation() { $entity = new Entity(); From 79387a7d5e80ab3e0bbce6f35e275c778a9f9c85 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 18:00:47 +0100 Subject: [PATCH 56/86] [Validator] Improved inline documentation of the metadata classes --- .../Mapping/BlackholeMetadataFactory.php | 13 ++- .../Validator/Mapping/ClassMetadata.php | 65 ++++++++++---- .../Mapping/ClassMetadataFactory.php | 58 ++++++++++++- .../Mapping/ClassMetadataInterface.php | 52 ++++++++++- .../Validator/Mapping/ElementMetadata.php | 8 ++ .../Validator/Mapping/GenericMetadata.php | 20 ++++- .../Validator/Mapping/GetterMetadata.php | 17 ++++ .../Validator/Mapping/MemberMetadata.php | 87 +++++++++++++++---- .../Validator/Mapping/MetadataInterface.php | 47 +++++----- .../Validator/Mapping/PropertyMetadata.php | 13 +++ .../Mapping/PropertyMetadataInterface.php | 16 ++-- .../Validator/MetadataFactoryInterface.php | 12 +-- .../Component/Validator/MetadataInterface.php | 12 +-- 13 files changed, 331 insertions(+), 89 deletions(-) diff --git a/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php index 90dd282e08712..6a0f81173a976 100644 --- a/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php @@ -11,25 +11,30 @@ namespace Symfony\Component\Validator\Mapping; +use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\MetadataFactoryInterface; /** - * Simple implementation of MetadataFactoryInterface that can be used when using ValidatorInterface::validateValue(). + * Metadata factory that does not store metadata. + * + * This implementation is useful if you want to validate values against + * constraints only and you don't need to add constraints to classes and + * properties. * * @author Fabien Potencier */ class BlackholeMetadataFactory implements MetadataFactoryInterface { /** - * @inheritdoc + * {@inheritdoc} */ public function getMetadataFor($value) { - throw new \LogicException('BlackholeClassMetadataFactory only works with ValidatorInterface::validateValue().'); + throw new NoSuchMetadataException('This class does not support metadata.'); } /** - * @inheritdoc + * {@inheritdoc} */ public function hasMetadataFor($value) { diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 85e872026d60b..e074f442922c5 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -22,7 +22,9 @@ use Symfony\Component\Validator\Exception\GroupDefinitionException; /** - * Represents all the configured constraints on a given class. + * Default implementation of {@link ClassMetadataInterface}. + * + * This class supports serialization and cloning. * * @author Bernhard Schussek * @author Fabien Potencier @@ -31,36 +33,64 @@ class ClassMetadata extends ElementMetadata implements LegacyMetadataInterface, { /** * @var string + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getClassName()} instead. */ public $name; /** * @var string + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getDefaultGroup()} instead. */ public $defaultGroup; /** * @var MemberMetadata[] + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getPropertyMetadata()} instead. */ public $members = array(); /** * @var PropertyMetadata[] + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getPropertyMetadata()} instead. */ public $properties = array(); /** * @var GetterMetadata[] + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getPropertyMetadata()} instead. */ public $getters = array(); /** * @var array + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getGroupSequence()} instead. */ public $groupSequence = array(); /** * @var Boolean + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link isGroupSequenceProvider()} instead. */ public $groupSequenceProvider = false; @@ -70,6 +100,10 @@ class ClassMetadata extends ElementMetadata implements LegacyMetadataInterface, * By default, only instances of {@link \Traversable} are traversed. * * @var integer + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getTraversalStrategy()} instead. */ public $traversalStrategy = TraversalStrategy::IMPLICIT; @@ -94,6 +128,11 @@ public function __construct($class) } } + /** + * {@inheritdoc} + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + */ public function accept(ValidationVisitorInterface $visitor, $value, $group, $propertyPath, $propagatedGroup = null) { if (null === $propagatedGroup && Constraint::DEFAULT_GROUP === $group @@ -129,9 +168,7 @@ public function accept(ValidationVisitorInterface $visitor, $value, $group, $pro } /** - * Returns the properties to be serialized - * - * @return array + * {@inheritdoc} */ public function __sleep() { @@ -152,9 +189,7 @@ public function __sleep() } /** - * Returns the fully qualified name of the class - * - * @return string The fully qualified class name + * {@inheritdoc} */ public function getClassName() { @@ -356,9 +391,7 @@ public function getPropertyMetadata($property) } /** - * Returns all properties for which constraints are defined. - * - * @return array An array of property names + * {@inheritdoc} */ public function getConstrainedProperties() { @@ -398,9 +431,7 @@ public function setGroupSequence($groupSequence) } /** - * Returns whether this class has an overridden default group sequence. - * - * @return Boolean + * {@inheritdoc} */ public function hasGroupSequence() { @@ -408,9 +439,7 @@ public function hasGroupSequence() } /** - * Returns the default group sequence for this class. - * - * @return GroupSequence The group sequence or null + * {@inheritdoc} */ public function getGroupSequence() { @@ -452,9 +481,7 @@ public function setGroupSequenceProvider($active) } /** - * Returns whether the class is a group sequence provider. - * - * @return Boolean + * {@inheritdoc} */ public function isGroupSequenceProvider() { diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php index 77eb8b528f745..812ad1a8aa72d 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php @@ -17,7 +17,22 @@ use Symfony\Component\Validator\Mapping\Cache\CacheInterface; /** - * A factory for creating metadata for PHP classes. + * Creates new {@link ClassMetadataInterface} instances. + * + * Whenever {@link getMetadataFor()} is called for the first time with a given + * class name or object of that class, a new metadata instance is created and + * returned. On subsequent requests for the same class, the same metadata + * instance will be returned. + * + * You can optionally pass a {@link LoaderInterface} instance to the constructor. + * Whenever a new metadata instance, it will be passed to the loader, which can + * configure the metadata based on configuration loaded from the filesystem or + * a database. If you want to use multiple loaders, wrap them in a + * {@link Loader\LoaderChain}. + * + * You can also optionally pass a {@link CacheInterface} instance to the + * constructor. This cache will be used for persisting the generated metadata + * between multiple PHP requests. * * @author Bernhard Schussek */ @@ -25,18 +40,32 @@ class ClassMetadataFactory implements MetadataFactoryInterface { /** * The loader for loading the class metadata + * * @var LoaderInterface */ protected $loader; /** * The cache for caching class metadata + * * @var CacheInterface */ protected $cache; + /** + * The loaded metadata, indexed by class name + * + * @var ClassMetadata[] + */ protected $loadedClasses = array(); + /** + * Creates a new metadata factory. + * + * @param LoaderInterface|null $loader The loader for configuring new metadata + * @param CacheInterface|null $cache The cache for persisting metadata + * between multiple PHP requests + */ public function __construct(LoaderInterface $loader = null, CacheInterface $cache = null) { $this->loader = $loader; @@ -44,7 +73,25 @@ public function __construct(LoaderInterface $loader = null, CacheInterface $cach } /** - * {@inheritdoc} + * Returns the metadata for the given class name or object. + * + * If the method was called with the same class name (or an object of that + * class) before, the same metadata instance is returned. + * + * If the factory was configured with a cache, this method will first look + * for an existing metadata instance in the cache. If an existing instance + * is found, it will be returned without further ado. + * + * Otherwise, a new metadata instance is created. If the factory was + * configured with a loader, the metadata is passed to the + * {@link LoaderInterface::loadClassMetadata()} method for further + * configuration. At last, the new object is returned. + * + * @param string|object $value A class name or an object + * + * @return MetadataInterface The metadata for the value + * + * @throws NoSuchMetadataException If no metadata exists for the given value */ public function getMetadataFor($value) { @@ -93,7 +140,12 @@ public function getMetadataFor($value) } /** - * {@inheritdoc} + * Returns whether the factory is able to return metadata for the given + * class name or object. + * + * @param string|object $value A class name or an object + * + * @return Boolean Whether metadata can be returned for that class */ public function hasMetadataFor($value) { diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php index efcfbbaa3da0c..f457cabffb02c 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php @@ -15,16 +15,66 @@ use Symfony\Component\Validator\PropertyMetadataContainerInterface as LegacyPropertyMetadataContainerInterface;; /** - * @since %%NextVersion%% + * Stores all metadata needed for validating objects of specific class. + * + * Most importantly, the metadata stores the constraints against which an object + * and its properties should be validated. + * + * Additionally, the metadata stores whether the "Default" group is overridden + * by a group sequence for that class and whether instances of that class + * should be traversed or not. + * + * @since 2.5 * @author Bernhard Schussek + * + * @see MetadataInterface + * @see \Symfony\Component\Validator\Constraints\GroupSequence + * @see \Symfony\Component\Validator\GroupSequenceProviderInterface + * @see TraversalStrategy */ interface ClassMetadataInterface extends MetadataInterface, LegacyPropertyMetadataContainerInterface, ClassBasedInterface { + /** + * Returns the names of all constrained properties. + * + * @return string[] A list of property names + */ public function getConstrainedProperties(); + /** + * Returns whether the "Default" group is overridden by a group sequence. + * + * If it is, you can access the group sequence with {@link getGroupSequence()}. + * + * @return Boolean Returns true if the "Default" group is overridden + * + * @see \Symfony\Component\Validator\Constraints\GroupSequence + */ public function hasGroupSequence(); + /** + * Returns the group sequence that overrides the "Default" group for this + * class. + * + * @return \Symfony\Component\Validator\Constraints\GroupSequence|null The group sequence or null + * + * @see \Symfony\Component\Validator\Constraints\GroupSequence + */ public function getGroupSequence(); + /** + * Returns whether the "Default" group is overridden by a dynamic group + * sequence obtained by the validated objects. + * + * If this method returns true, the class must implement + * {@link \Symfony\Component\Validator\GroupSequenceProviderInterface}. + * This interface will be used to obtain the group sequence when an object + * of this class is validated. + * + * @return Boolean Returns true if the "Default" group is overridden by + * a dynamic group sequence + * + * @see \Symfony\Component\Validator\GroupSequenceProviderInterface + */ public function isGroupSequenceProvider(); } diff --git a/src/Symfony/Component/Validator/Mapping/ElementMetadata.php b/src/Symfony/Component/Validator/Mapping/ElementMetadata.php index 84a826aa10f7d..1b971c91803c1 100644 --- a/src/Symfony/Component/Validator/Mapping/ElementMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ElementMetadata.php @@ -11,6 +11,14 @@ namespace Symfony\Component\Validator\Mapping; +/** + * Contains the metadata of a structural element. + * + * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Extend {@link GenericMetadata} instead. + */ abstract class ElementMetadata extends GenericMetadata { } diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index 6c4f1869918cc..01b3d5a403f04 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -21,6 +21,8 @@ /** * A generic container of {@link Constraint} objects. * + * This class supports serialization and cloning. + * * @since 2.5 * @author Bernhard Schussek */ @@ -28,11 +30,19 @@ class GenericMetadata implements MetadataInterface { /** * @var Constraint[] + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getConstraints()} and {@link findConstraints()} instead. */ public $constraints = array(); /** * @var array + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link findConstraints()} instead. */ public $constraintsByGroup = array(); @@ -44,6 +54,10 @@ class GenericMetadata implements MetadataInterface * @var integer * * @see CascadingStrategy + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getCascadingStrategy()} instead. */ public $cascadingStrategy = CascadingStrategy::NONE; @@ -55,13 +69,17 @@ class GenericMetadata implements MetadataInterface * @var integer * * @see TraversalStrategy + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getTraversalStrategy()} instead. */ public $traversalStrategy = TraversalStrategy::NONE; /** * Returns the names of the properties that should be serialized. * - * @return array + * @return string[] */ public function __sleep() { diff --git a/src/Symfony/Component/Validator/Mapping/GetterMetadata.php b/src/Symfony/Component/Validator/Mapping/GetterMetadata.php index 4bd609d035fae..122e246bdf652 100644 --- a/src/Symfony/Component/Validator/Mapping/GetterMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GetterMetadata.php @@ -13,6 +13,23 @@ use Symfony\Component\Validator\Exception\ValidatorException; +/** + * Stores all metadata needed for validating a class property via its getter + * method. + * + * A property getter is any method that is equal to the property's name, + * prefixed with either "get" or "is". That method will be used to access the + * property's value. + * + * The getter will be invoked by reflection, so the access of private and + * protected getters is supported. + * + * This class supports serialization and cloning. + * + * @author Bernhard Schussek + * + * @see PropertyMetadataInterface + */ class GetterMetadata extends MemberMetadata { /** diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 60dc417024137..fe57afa341dcc 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -16,11 +16,50 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +/** + * Stores all metadata needed for validating a class property. + * + * The method of accessing the property's value must be specified by subclasses + * by implementing the {@link newReflectionMember()} method. + * + * This class supports serialization and cloning. + * + * @author Bernhard Schussek + * + * @see PropertyMetadataInterface + */ abstract class MemberMetadata extends ElementMetadata implements PropertyMetadataInterface { + /** + * @var string + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getClassName()} instead. + */ public $class; + + /** + * @var string + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getName()} instead. + */ public $name; + + /** + * @var string + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getPropertyName()} instead. + */ public $property; + + /** + * @var \ReflectionMethod[]|\ReflectionProperty[] + */ private $reflMember = array(); /** @@ -37,6 +76,11 @@ public function __construct($class, $name, $property) $this->property = $property; } + /** + * {@inheritdoc} + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + */ public function accept(ValidationVisitorInterface $visitor, $value, $group, $propertyPath, $propagatedGroup = null) { $visitor->visit($this, $value, $group, $propertyPath); @@ -64,9 +108,7 @@ public function addConstraint(Constraint $constraint) } /** - * Returns the names of the properties that should be serialized - * - * @return array + * {@inheritdoc} */ public function __sleep() { @@ -78,7 +120,7 @@ public function __sleep() } /** - * Returns the name of the member + * Returns the name of the member. * * @return string */ @@ -88,9 +130,7 @@ public function getName() } /** - * Returns the class this member is defined on - * - * @return string + * {@inheritdoc} */ public function getClassName() { @@ -98,9 +138,7 @@ public function getClassName() } /** - * Returns the name of the property this member belongs to - * - * @return string The property name + * {@inheritdoc} */ public function getPropertyName() { @@ -108,7 +146,7 @@ public function getPropertyName() } /** - * Returns whether this member is public + * Returns whether this member is public. * * @param object|string $objectOrClassName The object or the class name * @@ -132,7 +170,7 @@ public function isProtected($objectOrClassName) } /** - * Returns whether this member is private + * Returns whether this member is private. * * @param object|string $objectOrClassName The object or the class name * @@ -144,9 +182,12 @@ public function isPrivate($objectOrClassName) } /** - * Returns whether objects stored in this member should be validated + * Returns whether objects stored in this member should be validated. * * @return Boolean + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link getCascadingStrategy()} instead. */ public function isCascaded() { @@ -155,9 +196,12 @@ public function isCascaded() /** * Returns whether arrays or traversable objects stored in this member - * should be traversed and validated in each entry + * should be traversed and validated in each entry. * * @return Boolean + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link getTraversalStrategy()} instead. */ public function isCollectionCascaded() { @@ -166,9 +210,12 @@ public function isCollectionCascaded() /** * Returns whether arrays or traversable objects stored in this member - * should be traversed recursively for inner arrays/traversable objects + * should be traversed recursively for inner arrays/traversable objects. * * @return Boolean + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link getTraversalStrategy()} instead. */ public function isCollectionCascadedDeeply() { @@ -176,11 +223,11 @@ public function isCollectionCascadedDeeply() } /** - * Returns the Reflection instance of the member + * Returns the reflection instance for accessing the member's value. * * @param object|string $objectOrClassName The object or the class name * - * @return object + * @return \ReflectionMethod|\ReflectionProperty The reflection instance */ public function getReflectionMember($objectOrClassName) { @@ -193,11 +240,13 @@ public function getReflectionMember($objectOrClassName) } /** - * Creates a new Reflection instance for the member + * Creates a new reflection instance for accessing the member's value. + * + * Must be implemented by subclasses. * * @param object|string $objectOrClassName The object or the class name * - * @return mixed Reflection class + * @return \ReflectionMethod|\ReflectionProperty The reflection instance */ abstract protected function newReflectionMember($objectOrClassName); } diff --git a/src/Symfony/Component/Validator/Mapping/MetadataInterface.php b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php index c533e4c2bd344..e947c8dfe35a5 100644 --- a/src/Symfony/Component/Validator/Mapping/MetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/MetadataInterface.php @@ -16,37 +16,36 @@ /** * A container for validation metadata. * - * The container contains constraints that may belong to different validation - * groups. Constraints for a specific group can be fetched by calling - * {@link findConstraints}. + * Most importantly, the metadata stores the constraints against which an object + * and its properties should be validated. * - * Implement this interface to add validation metadata to your own metadata - * layer. Each metadata may have named properties. Each property can be - * represented by one or more {@link PropertyMetadataInterface} instances that - * are returned by {@link getPropertyMetadata}. Since - * PropertyMetadataInterface inherits from MetadataInterface, - * each property may be divided into further properties. - * - * The {@link accept} method of each metadata implements the Visitor pattern. - * The method should forward the call to the visitor's - * {@link ValidationVisitorInterface::visit} method and additionally call - * accept() on all structurally related metadata instances. - * - * For example, to store constraints for PHP classes and their properties, - * create a class ClassMetadata (implementing MetadataInterface) - * and a class PropertyMetadata (implementing PropertyMetadataInterface). - * ClassMetadata::getPropertyMetadata($property) returns all - * PropertyMetadata instances for a property of that class. Its - * accept()-method simply forwards to ValidationVisitorInterface::visit() - * and calls accept() on all contained PropertyMetadata - * instances, which themselves call ValidationVisitorInterface::visit() - * again. + * Additionally, the metadata stores whether objects should be validated + * against their class' metadata and whether traversable objects should be + * traversed or not. * + * @since 2.5 * @author Bernhard Schussek + * + * @see CascadingStrategy + * @see TraversalStrategy */ interface MetadataInterface extends LegacyMetadataInterface { + /** + * Returns the strategy for cascading objects. + * + * @return integer The cascading strategy + * + * @see CascadingStrategy + */ public function getCascadingStrategy(); + /** + * Returns the strategy for traversing traversable objects. + * + * @return integer The traversal strategy + * + * @see TraversalStrategy + */ public function getTraversalStrategy(); } diff --git a/src/Symfony/Component/Validator/Mapping/PropertyMetadata.php b/src/Symfony/Component/Validator/Mapping/PropertyMetadata.php index 468f196f04a8e..f875cd68cea35 100644 --- a/src/Symfony/Component/Validator/Mapping/PropertyMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/PropertyMetadata.php @@ -13,6 +13,19 @@ use Symfony\Component\Validator\Exception\ValidatorException; +/** + * Stores all metadata needed for validating a class property. + * + * The value of the property is obtained by directly accessing the property. + * The property will be accessed by reflection, so the access of private and + * protected properties is supported. + * + * This class supports serialization and cloning. + * + * @author Bernhard Schussek + * + * @see PropertyMetadataInterface + */ class PropertyMetadata extends MemberMetadata { /** diff --git a/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php index 138b1c9fb59b8..79e2c799de602 100644 --- a/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/PropertyMetadataInterface.php @@ -15,17 +15,21 @@ use Symfony\Component\Validator\PropertyMetadataInterface as LegacyPropertyMetadataInterface; /** - * A container for validation metadata of a property. + * Stores all metadata needed for validating the value of a class property. * - * What exactly you define as "property" is up to you. The validator expects - * implementations of {@link MetadataInterface} that contain constraints and - * optionally a list of named properties that also have constraints (and may - * have further sub properties). Such properties are mapped by implementations - * of this interface. + * Most importantly, the metadata stores the constraints against which the + * property's value should be validated. * + * Additionally, the metadata stores whether objects stored in the property + * should be validated against their class' metadata and whether traversable + * objects should be traversed or not. + * + * @since 2.5 * @author Bernhard Schussek * * @see MetadataInterface + * @see CascadingStrategy + * @see TraversalStrategy */ interface PropertyMetadataInterface extends MetadataInterface, LegacyPropertyMetadataInterface, ClassBasedInterface { diff --git a/src/Symfony/Component/Validator/MetadataFactoryInterface.php b/src/Symfony/Component/Validator/MetadataFactoryInterface.php index 6dbab06ab74e2..40074556c00b4 100644 --- a/src/Symfony/Component/Validator/MetadataFactoryInterface.php +++ b/src/Symfony/Component/Validator/MetadataFactoryInterface.php @@ -21,20 +21,20 @@ interface MetadataFactoryInterface /** * Returns the metadata for the given value. * - * @param mixed $value Some value. + * @param mixed $value Some value * - * @return MetadataInterface The metadata for the value. + * @return MetadataInterface The metadata for the value * - * @throws Exception\NoSuchMetadataException If no metadata exists for the value. + * @throws Exception\NoSuchMetadataException If no metadata exists for the given value */ public function getMetadataFor($value); /** - * Returns whether metadata exists for the given value. + * Returns whether the class is able to return metadata for the given value. * - * @param mixed $value Some value. + * @param mixed $value Some value * - * @return Boolean Whether metadata exists for the value. + * @return Boolean Whether metadata can be returned for that value */ public function hasMetadataFor($value); } diff --git a/src/Symfony/Component/Validator/MetadataInterface.php b/src/Symfony/Component/Validator/MetadataInterface.php index b2cb20e847b33..60abfeb42a9f0 100644 --- a/src/Symfony/Component/Validator/MetadataInterface.php +++ b/src/Symfony/Component/Validator/MetadataInterface.php @@ -53,10 +53,10 @@ interface MetadataInterface * Calls {@link ValidationVisitorInterface::visit} and then forwards the * accept()-call to all property metadata instances. * - * @param ValidationVisitorInterface $visitor The visitor implementing the validation logic. - * @param mixed $value The value to validate. - * @param string|string[] $group The validation group to validate in. - * @param string $propertyPath The current property path in the validation graph. + * @param ValidationVisitorInterface $visitor The visitor implementing the validation logic + * @param mixed $value The value to validate + * @param string|string[] $group The validation group to validate in + * @param string $propertyPath The current property path in the validation graph * * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. */ @@ -65,9 +65,9 @@ public function accept(ValidationVisitorInterface $visitor, $value, $group, $pro /** * Returns all constraints for a given validation group. * - * @param string $group The validation group. + * @param string $group The validation group * - * @return Constraint[] A list of constraint instances. + * @return Constraint[] A list of constraint instances */ public function findConstraints($group); } From 987313d315f1083e3e5a7fd6f25b91e93cbcccaa Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 18:18:23 +0100 Subject: [PATCH 57/86] [Validator] Improved inline documentation of the violation builder --- .../Validator/ConstraintViolation.php | 59 ++++++++------ .../Validator/Context/ExecutionContext.php | 3 + .../Context/ExecutionContextFactory.php | 3 + .../Tests/Validator/Abstract2Dot5ApiTest.php | 2 +- .../Violation/ConstraintViolationBuilder.php | 77 +++++++++++++++--- .../ConstraintViolationBuilderInterface.php | 79 ++++++++++++++++++- 6 files changed, 188 insertions(+), 35 deletions(-) diff --git a/src/Symfony/Component/Validator/ConstraintViolation.php b/src/Symfony/Component/Validator/ConstraintViolation.php index 36a42aaec4310..41f57650a13b8 100644 --- a/src/Symfony/Component/Validator/ConstraintViolation.php +++ b/src/Symfony/Component/Validator/ConstraintViolation.php @@ -31,12 +31,12 @@ class ConstraintViolation implements ConstraintViolationInterface /** * @var array */ - private $messageParameters; + private $parameters; /** * @var integer|null */ - private $messagePluralization; + private $plural; /** * @var mixed @@ -61,27 +61,26 @@ class ConstraintViolation implements ConstraintViolationInterface /** * Creates a new constraint violation. * - * @param string $message The violation message. - * @param string $messageTemplate The raw violation message. - * @param array $messageParameters The parameters to substitute - * in the raw message. - * @param mixed $root The value originally passed - * to the validator. - * @param string $propertyPath The property path from the - * root value to the invalid - * value. - * @param mixed $invalidValue The invalid value causing the - * violation. - * @param integer|null $messagePluralization The pluralization parameter. - * @param mixed $code The error code of the - * violation, if any. - */ - public function __construct($message, $messageTemplate, array $messageParameters, $root, $propertyPath, $invalidValue, $messagePluralization = null, $code = null) + * @param string $message The violation message + * @param string $messageTemplate The raw violation message + * @param array $parameters The parameters to substitute in the + * raw violation message + * @param mixed $root The value originally passed to the + * validator + * @param string $propertyPath The property path from the root + * value to the invalid value + * @param mixed $invalidValue The invalid value that caused this + * violation + * @param integer|null $plural The number for determining the plural + * form when translation the message + * @param mixed $code The error code of the violation + */ + public function __construct($message, $messageTemplate, array $parameters, $root, $propertyPath, $invalidValue, $plural = null, $code = null) { $this->message = $message; $this->messageTemplate = $messageTemplate; - $this->messageParameters = $messageParameters; - $this->messagePluralization = $messagePluralization; + $this->parameters = $parameters; + $this->plural = $plural; $this->root = $root; $this->propertyPath = $propertyPath; $this->invalidValue = $invalidValue; @@ -130,7 +129,15 @@ public function getMessageTemplate() */ public function getMessageParameters() { - return $this->messageParameters; + return $this->parameters; + } + + /** + * Alias of {@link getMessageParameters()}. + */ + public function getParameters() + { + return $this->parameters; } /** @@ -138,7 +145,15 @@ public function getMessageParameters() */ public function getMessagePluralization() { - return $this->messagePluralization; + return $this->plural; + } + + /** + * Alias of {@link getMessagePluralization()}. + */ + public function getPlural() + { + return $this->plural; } /** diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index fdfddf9c4ba9e..e4db8a75b33f6 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -30,6 +30,9 @@ * @author Bernhard Schussek * * @see ExecutionContextInterface + * + * @internal You should not instantiate or use this class. Code against + * {@link ExecutionContextInterface} instead. */ class ExecutionContext implements ExecutionContextInterface { diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php index 3305e1a943452..4553c8b12633b 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php @@ -20,6 +20,9 @@ * * @since 2.5 * @author Bernhard Schussek + * + * @internal You should not instantiate or use this class. Code against + * {@link ExecutionContextFactoryInterface} instead. */ class ExecutionContextFactory implements ExecutionContextFactoryInterface { diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 4559a3ad32e01..48d89e05969bc 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -538,7 +538,7 @@ public function testAddCustomizedViolation() $context->buildViolation('Message %param%') ->setParameter('%param%', 'value') ->setInvalidValue('Invalid value') - ->setPluralization(2) + ->setPlural(2) ->setCode('Code') ->addViolation(); }; diff --git a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php index 5fb8488e80fcf..d5905a0a5cbcb 100644 --- a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php +++ b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php @@ -17,29 +17,64 @@ use Symfony\Component\Validator\Util\PropertyPath; /** - * @since %%NextVersion%% + * Default implementation of {@link ConstraintViolationBuilderInterface}. + * + * @since 2.5 * @author Bernhard Schussek + * + * @internal You should not instantiate or use this class. Code against + * {@link ConstraintViolationBuilderInterface} instead. */ class ConstraintViolationBuilder implements ConstraintViolationBuilderInterface { + /** + * @var ConstraintViolationList + */ private $violations; + /** + * @var string + */ private $message; + /** + * @var array + */ private $parameters; + /** + * @var mixed + */ private $root; + /** + * @var mixed + */ private $invalidValue; + /** + * @var string + */ private $propertyPath; + /** + * @var TranslatorInterface + */ private $translator; + /** + * @var string|null + */ private $translationDomain; - private $pluralization; + /** + * @var integer|null + */ + private $plural; + /** + * @var mixed + */ private $code; public function __construct(ConstraintViolationList $violations, $message, array $parameters, $root, $propertyPath, $invalidValue, TranslatorInterface $translator, $translationDomain = null) @@ -54,13 +89,19 @@ public function __construct(ConstraintViolationList $violations, $message, array $this->translationDomain = $translationDomain; } - public function atPath($subPath) + /** + * {@inheritdoc} + */ + public function atPath($path) { - $this->propertyPath = PropertyPath::append($this->propertyPath, $subPath); + $this->propertyPath = PropertyPath::append($this->propertyPath, $path); return $this; } + /** + * {@inheritdoc} + */ public function setParameter($key, $value) { $this->parameters[$key] = $value; @@ -68,6 +109,9 @@ public function setParameter($key, $value) return $this; } + /** + * {@inheritdoc} + */ public function setParameters(array $parameters) { $this->parameters = $parameters; @@ -75,6 +119,9 @@ public function setParameters(array $parameters) return $this; } + /** + * {@inheritdoc} + */ public function setTranslationDomain($translationDomain) { $this->translationDomain = $translationDomain; @@ -82,6 +129,9 @@ public function setTranslationDomain($translationDomain) return $this; } + /** + * {@inheritdoc} + */ public function setInvalidValue($invalidValue) { $this->invalidValue = $invalidValue; @@ -89,13 +139,19 @@ public function setInvalidValue($invalidValue) return $this; } - public function setPluralization($pluralization) + /** + * {@inheritdoc} + */ + public function setPlural($number) { - $this->pluralization = $pluralization; + $this->plural = $number; return $this; } + /** + * {@inheritdoc} + */ public function setCode($code) { $this->code = $code; @@ -103,9 +159,12 @@ public function setCode($code) return $this; } + /** + * {@inheritdoc} + */ public function addViolation() { - if (null === $this->pluralization) { + if (null === $this->plural) { $translatedMessage = $this->translator->trans( $this->message, $this->parameters, @@ -115,7 +174,7 @@ public function addViolation() try { $translatedMessage = $this->translator->transChoice( $this->message, - $this->pluralization, + $this->plural, $this->parameters, $this->translationDomain# ); @@ -135,7 +194,7 @@ public function addViolation() $this->root, $this->propertyPath, $this->invalidValue, - $this->pluralization, + $this->plural, $this->code )); } diff --git a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php index 9d62c3ccb5dce..c522860aa75e2 100644 --- a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php +++ b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php @@ -12,24 +12,97 @@ namespace Symfony\Component\Validator\Violation; /** - * @since %%NextVersion%% + * Builds {@link \Symfony\Component\Validator\ConstraintViolationInterface} + * objects. + * + * Use the various methods on this interface to configure the built violation. + * Finally, call {@link addViolation()} to add the violation to the current + * execution context. + * + * @since 2.5 * @author Bernhard Schussek */ interface ConstraintViolationBuilderInterface { - public function atPath($subPath); + /** + * Stores the property path at which the violation should be generated. + * + * The passed path will be appended to the current property path of the + * execution context. + * + * @param string $path The property path + * + * @return ConstraintViolationBuilderInterface This builder + */ + public function atPath($path); + /** + * Sets a parameter to be inserted into the violation message. + * + * @param string $key The name of the parameter + * @param string $value The value to be inserted in the parameter's place + * + * @return ConstraintViolationBuilderInterface This builder + */ public function setParameter($key, $value); + /** + * Sets all parameters to be inserted into the violation message. + * + * @param array $parameters An array with the parameter names as keys and + * the values to be inserted in their place as + * values + * + * @return ConstraintViolationBuilderInterface This builder + */ public function setParameters(array $parameters); + /** + * Sets the translation domain which should be used for translating the + * violation message. + * + * @param string $translationDomain The translation domain + * + * @return ConstraintViolationBuilderInterface This builder + * + * @see \Symfony\Component\Translation\TranslatorInterface + */ public function setTranslationDomain($translationDomain); + /** + * Sets the invalid value that caused this violation. + * + * @param mixed $invalidValue The invalid value + * + * @return ConstraintViolationBuilderInterface This builder + */ public function setInvalidValue($invalidValue); - public function setPluralization($pluralization); + /** + * Sets the number which determines how the plural form of the violation + * message is chosen when it is translated. + * + * @param integer $number The number for determining the plural form + * + * @return ConstraintViolationBuilderInterface This builder + * + * @see \Symfony\Component\Translation\TranslatorInterface::transChoice() + */ + public function setPlural($number); + /** + * Sets the violation code. + * + * @param mixed $code The violation code + * + * @return ConstraintViolationBuilderInterface This builder + * + * @internal This method is internal and should not be used by user code + */ public function setCode($code); + /** + * Adds the violation to the current execution context. + */ public function addViolation(); } From 93fdff764acf3ffdcf1c96e9b927d0dc72daf61d Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 18:48:33 +0100 Subject: [PATCH 58/86] [Validator] The supported API versions can now be passed to the ValidatorBuilder --- .../Context/LegacyExecutionContext.php | 8 +- .../Mapping/BlackholeMetadataFactory.php | 3 +- .../Validator/Tests/ValidatorBuilderTest.php | 34 ++++++++ .../Component/Validator/Validation.php | 10 +++ .../Component/Validator/ValidatorBuilder.php | 75 ++++++++++++++++- .../Validator/ValidatorBuilderInterface.php | 81 ++++++++++++------- 6 files changed, 173 insertions(+), 38 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index 1a147e441cf00..ea238f9bcd20a 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -63,13 +63,13 @@ public function __construct(ValidatorInterface $validator, $root, GroupManagerIn /** * {@inheritdoc} */ - public function addViolation($message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) + public function addViolation($message, array $parameters = array(), $invalidValue = null, $plural = null, $code = null) { if (func_num_args() > 2) { $this ->buildViolation($message, $parameters) ->setInvalidValue($invalidValue) - ->setPluralization($pluralization) + ->setPlural($plural) ->setCode($code) ->addViolation() ; @@ -83,14 +83,14 @@ public function addViolation($message, array $parameters = array(), $invalidValu /** * {@inheritdoc} */ - public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $plural = null, $code = null) { if (func_num_args() > 2) { $this ->buildViolation($message, $parameters) ->atPath($subPath) ->setInvalidValue($invalidValue) - ->setPluralization($pluralization) + ->setPlural($plural) ->setCode($code) ->addViolation() ; diff --git a/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php index 6a0f81173a976..28eaa5f02def4 100644 --- a/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Validator\Mapping; -use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\MetadataFactoryInterface; /** @@ -30,7 +29,7 @@ class BlackholeMetadataFactory implements MetadataFactoryInterface */ public function getMetadataFor($value) { - throw new NoSuchMetadataException('This class does not support metadata.'); + throw new \LogicException('This class does not support metadata.'); } /** diff --git a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php index 900243f31ad65..fe45c56c07819 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Tests; +use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\ValidatorBuilder; use Symfony\Component\Validator\ValidatorBuilderInterface; @@ -108,4 +109,37 @@ public function testSetTranslationDomain() { $this->assertSame($this->builder, $this->builder->setTranslationDomain('TRANS_DOMAIN')); } + + public function testDefaultApiVersion() + { + if (version_compare(PHP_VERSION, '5.3.9', '<')) { + // Old implementation on PHP < 5.3.9 + $this->assertInstanceOf('Symfony\Component\Validator\Validator', $this->builder->getValidator()); + } else { + // Legacy compatible implementation on PHP >= 5.3.9 + $this->assertInstanceOf('Symfony\Component\Validator\Validator\LegacyValidator', $this->builder->getValidator()); + } + } + + public function testSetApiVersion24() + { + $this->assertSame($this->builder, $this->builder->setApiVersion(Validation::API_VERSION_2_4)); + $this->assertInstanceOf('Symfony\Component\Validator\Validator', $this->builder->getValidator()); + } + + 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()); + } + + public function testSetApiVersion24And25() + { + if (version_compare(PHP_VERSION, '5.3.9', '<')) { + $this->markTestSkipped('Not supported prior to PHP 5.3.9'); + } + + $this->assertSame($this->builder, $this->builder->setApiVersion(Validation::API_VERSION_2_4 | Validation::API_VERSION_2_5)); + $this->assertInstanceOf('Symfony\Component\Validator\Validator\LegacyValidator', $this->builder->getValidator()); + } } diff --git a/src/Symfony/Component/Validator/Validation.php b/src/Symfony/Component/Validator/Validation.php index de77e838fb6ff..450a8350050e1 100644 --- a/src/Symfony/Component/Validator/Validation.php +++ b/src/Symfony/Component/Validator/Validation.php @@ -18,6 +18,16 @@ */ final class Validation { + /** + * The Validator API provided by Symfony 2.4 and older. + */ + const API_VERSION_2_4 = 1; + + /** + * The Validator API provided by Symfony 2.5 and newer. + */ + const API_VERSION_2_5 = 2; + /** * Creates a new validator. * diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index e24a7071662e2..b6763ede74e03 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -13,6 +13,9 @@ use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Validator\Context\ExecutionContextFactory; +use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; +use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Mapping\ClassMetadataFactory; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\Loader\LoaderChain; @@ -28,6 +31,14 @@ use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Cache\ArrayCache; +use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; +use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; +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\LegacyValidator; /** * The default implementation of {@link ValidatorBuilderInterface}. @@ -91,6 +102,11 @@ class ValidatorBuilder implements ValidatorBuilderInterface */ private $propertyAccessor; + /** + * @var integer + */ + private $apiVersion; + /** * {@inheritdoc} */ @@ -303,6 +319,32 @@ public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor) return $this; } + /** + * {@inheritdoc} + */ + public function setApiVersion($apiVersion) + { + if (!($apiVersion & (Validation::API_VERSION_2_4 | Validation::API_VERSION_2_5))) { + throw new InvalidArgumentException(sprintf( + 'The requested API version is invalid: "%s"', + $apiVersion + )); + } + + if (version_compare(PHP_VERSION, '5.3.9', '<') && $apiVersion === (Validation::API_VERSION_2_4 | Validation::API_VERSION_2_5)) { + throw new InvalidArgumentException(sprintf( + 'The Validator API that is compatible with both Symfony 2.4 '. + 'and Symfony 2.5 can only be used on PHP 5.3.9 and higher. '. + 'Your current PHP version is %s.', + PHP_VERSION + )); + } + + $this->apiVersion = $apiVersion; + + return $this; + } + /** * {@inheritdoc} */ @@ -347,7 +389,38 @@ public function getValidator() $propertyAccessor = $this->propertyAccessor ?: PropertyAccess::createPropertyAccessor(); $validatorFactory = $this->validatorFactory ?: new ConstraintValidatorFactory($propertyAccessor); $translator = $this->translator ?: new DefaultTranslator(); + $apiVersion = $this->apiVersion; + + if (null === $apiVersion) { + $apiVersion = version_compare(PHP_VERSION, '5.3.9', '<') + ? Validation::API_VERSION_2_4 + : (Validation::API_VERSION_2_4 | Validation::API_VERSION_2_5); + } + + if (Validation::API_VERSION_2_4 === $apiVersion) { + 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); + } else { + $contextFactory = new LegacyExecutionContextFactory($nodeValidator, $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); + } - return new Validator($metadataFactory, $validatorFactory, $translator, $this->translationDomain, $this->initializers); + return new LegacyValidator($contextFactory, $nodeTraverser, $metadataFactory); } } diff --git a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php index 92aaca756a3bc..d486faed127cc 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php +++ b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php @@ -26,124 +26,124 @@ interface ValidatorBuilderInterface /** * Adds an object initializer to the validator. * - * @param ObjectInitializerInterface $initializer The initializer. + * @param ObjectInitializerInterface $initializer The initializer * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function addObjectInitializer(ObjectInitializerInterface $initializer); /** * Adds a list of object initializers to the validator. * - * @param array $initializers The initializer. + * @param array $initializers The initializer * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function addObjectInitializers(array $initializers); /** * Adds an XML constraint mapping file to the validator. * - * @param string $path The path to the mapping file. + * @param string $path The path to the mapping file * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function addXmlMapping($path); /** * Adds a list of XML constraint mapping files to the validator. * - * @param array $paths The paths to the mapping files. + * @param array $paths The paths to the mapping files * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function addXmlMappings(array $paths); /** * Adds a YAML constraint mapping file to the validator. * - * @param string $path The path to the mapping file. + * @param string $path The path to the mapping file * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function addYamlMapping($path); /** * Adds a list of YAML constraint mappings file to the validator. * - * @param array $paths The paths to the mapping files. + * @param array $paths The paths to the mapping files * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function addYamlMappings(array $paths); /** * Enables constraint mapping using the given static method. * - * @param string $methodName The name of the method. + * @param string $methodName The name of the method * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function addMethodMapping($methodName); /** * Enables constraint mapping using the given static methods. * - * @param array $methodNames The names of the methods. + * @param array $methodNames The names of the methods * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function addMethodMappings(array $methodNames); /** * Enables annotation based constraint mapping. * - * @param Reader $annotationReader The annotation reader to be used. + * @param Reader $annotationReader The annotation reader to be used * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function enableAnnotationMapping(Reader $annotationReader = null); /** * Disables annotation based constraint mapping. * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function disableAnnotationMapping(); /** * Sets the class metadata factory used by the validator. * - * @param MetadataFactoryInterface $metadataFactory The metadata factory. + * @param MetadataFactoryInterface $metadataFactory The metadata factory * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function setMetadataFactory(MetadataFactoryInterface $metadataFactory); /** * Sets the cache for caching class metadata. * - * @param CacheInterface $cache The cache instance. + * @param CacheInterface $cache The cache instance * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function setMetadataCache(CacheInterface $cache); /** * Sets the constraint validator factory used by the validator. * - * @param ConstraintValidatorFactoryInterface $validatorFactory The validator factory. + * @param ConstraintValidatorFactoryInterface $validatorFactory The validator factory * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function setConstraintValidatorFactory(ConstraintValidatorFactoryInterface $validatorFactory); /** * Sets the translator used for translating violation messages. * - * @param TranslatorInterface $translator The translator instance. + * @param TranslatorInterface $translator The translator instance * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function setTranslator(TranslatorInterface $translator); @@ -154,21 +154,40 @@ public function setTranslator(TranslatorInterface $translator); * Pass the domain that is used for violation messages by default to this * method. * - * @param string $translationDomain The translation domain of the violation messages. + * @param string $translationDomain The translation domain of the violation messages * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function setTranslationDomain($translationDomain); /** * Sets the property accessor for resolving property paths. * - * @param PropertyAccessorInterface $propertyAccessor The property accessor. + * @param PropertyAccessorInterface $propertyAccessor The property accessor * - * @return ValidatorBuilderInterface The builder object. + * @return ValidatorBuilderInterface The builder object */ public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor); + /** + * Sets the API versions that the returned validator should support. + * + * Use a bitwise "or" to pass multiple versions: + * + * $builder->setApiVersion(Validation::API_VERSION_2_4 | Validation::API_VERSION_2_5); + * + * The builder will try to return an implementation that supports all + * requested versions. + * + * @param integer $apiVersion The supported API version(s) + * + * @return ValidatorBuilderInterface The builder object + * + * @see Validation::API_VERSION_2_4 + * @see Validation::API_VERSION_2_5 + */ + public function setApiVersion($apiVersion); + /** * Builds and returns a new validator object. * From 886e05e7ed0755a42bbf16f6d0f0ab1cf41958fe Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 23:48:06 +0100 Subject: [PATCH 59/86] [Validator] Removed unused use statement --- src/Symfony/Component/Validator/Mapping/MemberMetadata.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index fe57afa341dcc..9423cfd6c550c 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Validator\Mapping; -use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; From f61d31e5faa44d44f6e417c92e7b24ef10272d39 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Fri, 21 Feb 2014 23:49:25 +0100 Subject: [PATCH 60/86] [Validator] Fixed grammar --- .../Component/Validator/Mapping/ClassMetadataFactory.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php index 812ad1a8aa72d..39bc32a201e86 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php @@ -25,9 +25,9 @@ * instance will be returned. * * You can optionally pass a {@link LoaderInterface} instance to the constructor. - * Whenever a new metadata instance, it will be passed to the loader, which can - * configure the metadata based on configuration loaded from the filesystem or - * a database. If you want to use multiple loaders, wrap them in a + * Whenever a new metadata instance is created, it is passed to the loader, + * which can configure the metadata based on configuration loaded from the + * filesystem or a database. If you want to use multiple loaders, wrap them in a * {@link Loader\LoaderChain}. * * You can also optionally pass a {@link CacheInterface} instance to the From 23534ca6ab4a107a5e801e710fb3c07b1a8874d9 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Sat, 22 Feb 2014 11:43:44 +0100 Subject: [PATCH 61/86] [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 e4db8a75b33f6..959f44ea79cd4 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\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 mixed + */ + 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 Node + * @var string|null */ - private $node; + 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 setValue($value) + { + $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 setCurrentNode(Node $node) + public function setGroup($group) { - $this->node = $node; + $this->group = $group; } /** @@ -209,7 +244,7 @@ public function getRoot() */ public function getValue() { - return $this->node ? $this->node->value : null; + return $this->value; } /** @@ -217,7 +252,7 @@ public function getValue() */ public function getMetadata() { - return $this->node ? $this->node->metadata : null; + return $this->metadata; } /** @@ -225,7 +260,7 @@ public function getMetadata() */ public function getGroup() { - return $this->groupManager->getCurrentGroup(); + return $this->group; } /** @@ -233,9 +268,7 @@ public function getGroup() */ 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 @@ public function getClassName() */ 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 @@ public function getPropertyName() */ 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 4553c8b12633b..5e660f47b0c92 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php @@ -26,11 +26,6 @@ */ 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 @@ public function createContext(ValidatorInterface $validator, $root) 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 b955a34f27655..241dc03107865 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 @@ public function buildViolation($message, array $parameters = array()); 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 ea238f9bcd20a..abac3e9048fd8 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 @@ public function __construct(ValidatorInterface $validator, $root, GroupManagerIn 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 7f19bb204c02b..88d3c0ff5d6f8 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php @@ -26,11 +26,6 @@ */ 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 @@ public function createContext(ValidatorInterface $validator, $root) 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 a231b907d55f3..0000000000000 --- 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 ecf0b2694c243..03243960b1d05 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 cc6f5780785ff..9d18c7ef48503 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -14,7 +14,6 @@ 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 @@ * @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 @@ public function visit(Node $node, ExecutionContextInterface $context) 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 @@ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, 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 @@ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterf $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 0f6e51f2ffc9e..0000000000000 --- 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 c9a8fb509d7fa..3ff580b1103dd 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -34,17 +34,8 @@ protected function setUp() 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 8ba44f697d2b6..493fa7a616439 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -34,17 +34,8 @@ protected function setUp() 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 0000000000000..57134410dcb08 --- /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 fb6f6317c868a..61e78456b3e00 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\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 fe45c56c07819..5cd1198654697 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php @@ -130,7 +130,7 @@ public function testSetApiVersion24() 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 e80157db8123c..31dd4b9c384f5 100644 --- a/src/Symfony/Component/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator.php @@ -22,7 +22,7 @@ * @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 eae4746e886f4..8f93b67a893e5 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -28,7 +28,7 @@ * @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 0000000000000..df7901f60f4c2 --- /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 0000000000000..a8f9307d71e23 --- /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 b250278941fd8..2f23ce71f3315 100644 --- a/src/Symfony/Component/Validator/Validator/ContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php @@ -29,7 +29,7 @@ * @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 a73a72ebb5884..4352b3180f81a 100644 --- a/src/Symfony/Component/Validator/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator/TraversingValidator.php @@ -22,7 +22,7 @@ * @since 2.5 * @author Bernhard Schussek */ -class Validator implements ValidatorInterface +class TraversingValidator implements ValidatorInterface { /** * @var ExecutionContextFactoryInterface @@ -61,7 +61,7 @@ public function __construct(ExecutionContextFactoryInterface $contextFactory, No */ public function startContext($root = null) { - return new ContextualValidator( + return new TraversingContextualValidator( $this->contextFactory->createContext($this, $root), $this->nodeTraverser, $this->metadataFactory @@ -73,7 +73,7 @@ public function startContext($root = null) */ 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 b6763ede74e03..351c5d654e73d 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\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 @@ public function getValidator() 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 @@ public function getValidator() 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); } } From 38e26fbcaf05ff547db7d1ee0c84a8afaa59b7d6 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Sat, 22 Feb 2014 12:12:31 +0100 Subject: [PATCH 62/86] [Validator] Decoupled RecursiveContextualValidator from Node --- .../RecursiveContextualValidator.php | 325 ++++++++++-------- 1 file changed, 174 insertions(+), 151 deletions(-) diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index df7901f60f4c2..e4278691af543 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -16,18 +16,19 @@ use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; 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\MetadataInterface; 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; @@ -52,8 +53,6 @@ class RecursiveContextualValidator implements ContextualValidatorInterface private $validatorFactory; - private $currentGroup; - /** * Creates a validator for the given context. * @@ -96,12 +95,16 @@ public function validate($value, $constraints = null, $groups = null) $metadata->addConstraints($constraints); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $this->traverseGenericNode(new GenericNode( + $this->traverseGenericNode( $value, + null, $metadata, $this->defaultPropertyPath, - $groups - ), $this->context); + $groups, + null, + TraversalStrategy::IMPLICIT, + $this->context + ); return $this; } @@ -128,13 +131,16 @@ public function validateProperty($object, $propertyName, $groups = null) foreach ($propertyMetadatas as $propertyMetadata) { $propertyValue = $propertyMetadata->getPropertyValue($object); - $this->traverseGenericNode(new PropertyNode( - $object, + $this->traverseGenericNode( $propertyValue, + $object, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), - $groups - ), $this->context); + $groups, + null, + TraversalStrategy::IMPLICIT, + $this->context + ); } return $this; @@ -160,14 +166,16 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; foreach ($propertyMetadatas as $propertyMetadata) { - $this->traverseGenericNode(new PropertyNode( - $object, + $this->traverseGenericNode( $value, + $object, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups, - $groups - ), $this->context); + null, + TraversalStrategy::IMPLICIT, + $this->context + ); } return $this; @@ -218,18 +226,16 @@ protected function normalizeGroups($groups) * @see CollectionNode * @see TraversalStrategy */ - private function traverseClassNode(ClassNode $node, ExecutionContextInterface $context) + private function traverseClassNode($value, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { - if (false === $this->validateNode($node, $context)) { - return; - } + $groups = $this->validateNode($value, $value, $metadata, $propertyPath, $groups, $traversalStrategy, $context); - if (0 === count($node->groups)) { + if (0 === count($groups)) { return; } - foreach ($node->metadata->getConstrainedProperties() as $propertyName) { - foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { + foreach ($metadata->getConstrainedProperties() as $propertyName) { + foreach ($metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { if (!$propertyMetadata instanceof PropertyMetadataInterface) { throw new UnsupportedMetadataException(sprintf( 'The property metadata instances should implement '. @@ -239,26 +245,26 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c )); } - $this->traverseGenericNode(new PropertyNode( - $node->value, - $propertyMetadata->getPropertyValue($node->value), + $this->traverseGenericNode( + $propertyMetadata->getPropertyValue($value), + $value, $propertyMetadata, - $node->propertyPath - ? $node->propertyPath.'.'.$propertyName + $propertyPath + ? $propertyPath.'.'.$propertyName : $propertyName, - $node->groups, - $node->cascadedGroups - ), $context); + $groups, + $cascadedGroups, + TraversalStrategy::IMPLICIT, + $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 = $metadata->getTraversalStrategy() | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); } @@ -268,18 +274,28 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c } // If IMPLICIT, stop unless we deal with a Traversable - if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) { + if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$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); + // If TRAVERSE, fail if we have no Traversable + if (!$value instanceof \Traversable) { + // Must throw a ConstraintDefinitionException for backwards + // compatibility reasons with Symfony < 2.5 + throw new ConstraintDefinitionException(sprintf( + 'Traversal was enabled for "%s", but this class '. + 'does not implement "\Traversable".', + get_class($value) + )); + } + + $this->cascadeCollection( + $value, + $propertyPath, + $groups, + $traversalStrategy, + $context + ); } /** @@ -304,28 +320,26 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c * @see ClassNode * @see CollectionNode */ - private function traverseCollectionNode(CollectionNode $node, ExecutionContextInterface $context) + private function cascadeCollection($collection, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { - $traversalStrategy = $node->traversalStrategy; - if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) { $traversalStrategy = TraversalStrategy::NONE; } else { $traversalStrategy = TraversalStrategy::IMPLICIT; } - foreach ($node->value as $key => $value) { + foreach ($collection 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( + $this->cascadeCollection( $value, - $node->propertyPath.'['.$key.']', - $node->groups, - null, - $traversalStrategy - ), $context); + $propertyPath.'['.$key.']', + $groups, + $traversalStrategy, + $context + ); continue; } @@ -335,8 +349,8 @@ private function traverseCollectionNode(CollectionNode $node, ExecutionContextIn if (is_object($value)) { $this->cascadeObject( $value, - $node->propertyPath.'['.$key.']', - $node->groups, + $propertyPath.'['.$key.']', + $groups, $traversalStrategy, $context ); @@ -361,48 +375,45 @@ private function traverseCollectionNode(CollectionNode $node, ExecutionContextIn * @param Node $node The node * @param ExecutionContextInterface $context The current execution context */ - private function traverseGenericNode(Node $node, ExecutionContextInterface $context) + private function traverseGenericNode($value, $object, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { - if (false === $this->validateNode($node, $context)) { + $groups = $this->validateNode($value, $object, $metadata, $propertyPath, $groups, $traversalStrategy, $context); + + if (0 === count($groups)) { return; } - if (null === $node->value) { + if (null === $value) { return; } // The "cascadedGroups" property is set by the NodeValidationVisitor when // traversing group sequences - $cascadedGroups = null !== $node->cascadedGroups - ? $node->cascadedGroups - : $node->groups; + $cascadedGroups = count($cascadedGroups) > 0 + ? $cascadedGroups + : $groups; - if (0 === count($cascadedGroups)) { - return; - } - - $cascadingStrategy = $node->metadata->getCascadingStrategy(); - $traversalStrategy = $node->traversalStrategy; + $cascadingStrategy = $metadata->getCascadingStrategy(); // 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 = $metadata->getTraversalStrategy() | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); } - if (is_array($node->value)) { + if (is_array($value)) { // Arrays are always traversed, independent of the specified // traversal strategy // (BC with Symfony < 2.5) - $this->traverseCollectionNode(new CollectionNode( - $node->value, - $node->propertyPath, + $this->cascadeCollection( + $value, + $propertyPath, $cascadedGroups, - null, - $traversalStrategy - ), $context); + $traversalStrategy, + $context + ); return; } @@ -412,8 +423,8 @@ private function traverseGenericNode(Node $node, ExecutionContextInterface $cont // a NoSuchMetadataException to be thrown in that case // (BC with Symfony < 2.5) $this->cascadeObject( - $node->value, - $node->propertyPath, + $value, + $propertyPath, $cascadedGroups, $traversalStrategy, $context @@ -467,14 +478,15 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal )); } - $this->traverseClassNode(new ClassNode( + $this->traverseClassNode( $object, $classMetadata, $propertyPath, $groups, null, - $traversalStrategy - ), $context); + $traversalStrategy, + $context + ); } catch (NoSuchMetadataException $e) { // Rethrow if not Traversable if (!$object instanceof \Traversable) { @@ -486,13 +498,13 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal throw $e; } - $this->traverseCollectionNode(new CollectionNode( + $this->cascadeCollection( $object, $propertyPath, $groups, - null, - $traversalStrategy - ), $context); + $traversalStrategy, + $context + ); } } @@ -506,55 +518,19 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal * @param Node $node The current node * @param ExecutionContextInterface $context The execution context * - * @return Boolean Whether to traverse the successor nodes + * @return array The groups in which the successor nodes should be validated */ - public function validateNode(Node $node, ExecutionContextInterface $context) + public function validateNode($value, $object, MetadataInterface $metadata = null, $propertyPath, array $groups, $traversalStrategy, 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); + $context->setValue($value); + $context->setMetadata($metadata); + $context->setPropertyPath($propertyPath); - 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 ($metadata instanceof ClassMetadataInterface) { + $groups = $this->replaceDefaultGroup($value, $metadata, $groups); } - if ($node instanceof ClassNode) { - $objectHash = spl_object_hash($node->value); - } elseif ($node instanceof PropertyNode) { - $objectHash = spl_object_hash($node->object); - } else { - $objectHash = null; - } + $objectHash = is_object($object) ? spl_object_hash($object) : null; // if group (=[,G3,G4]) contains group sequence (=) // then call traverse() with each entry of the group sequence and abort @@ -562,20 +538,20 @@ public function validateNode(Node $node, ExecutionContextInterface $context) // finally call traverse() with remaining entries ([G3,G4]) or // simply continue traversal (if possible) - foreach ($node->groups as $key => $group) { + foreach ($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) { + if ($metadata instanceof ClassMetadataInterface) { // 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]); + unset($groups[$key]); continue; } @@ -585,19 +561,19 @@ public function validateNode(Node $node, ExecutionContextInterface $context) // Validate normal group if (!$group instanceof GroupSequence) { - $this->validateNodeForGroup($node, $group, $context, $objectHash); + $this->validateNodeForGroup($value, $objectHash, $metadata, $group, $context); continue; } // Traverse group sequence until a violation is generated - $this->stepThroughGroupSequence($node, $group, $context); + $this->stepThroughGroupSequence($value, $object, $metadata, $propertyPath, $traversalStrategy, $group, $context); // Skip the group sequence when validating successor nodes - unset($node->groups[$key]); + unset($groups[$key]); } - return true; + return $groups; } /** @@ -610,24 +586,39 @@ public function validateNode(Node $node, ExecutionContextInterface $context) * @param GroupSequence $groupSequence The group sequence * @param ExecutionContextInterface $context The execution context */ - private function stepThroughGroupSequence(Node $node, GroupSequence $groupSequence, ExecutionContextInterface $context) + private function stepThroughGroupSequence($value, $object, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); foreach ($groupSequence->groups as $groupInSequence) { - $node = clone $node; - $node->groups = array($groupInSequence); + $groups = array($groupInSequence); + $cascadedGroups = null; if (null !== $groupSequence->cascadedGroup) { - $node->cascadedGroups = array($groupSequence->cascadedGroup); + $cascadedGroups = array($groupSequence->cascadedGroup); } - if ($node instanceof ClassNode) { - $this->traverseClassNode($node, $context); - } elseif ($node instanceof CollectionNode) { - $this->traverseCollectionNode($node, $context); + if ($metadata instanceof ClassMetadataInterface) { + $this->traverseClassNode( + $value, + $metadata, + $propertyPath, + $groups, + $cascadedGroups, + $traversalStrategy, + $context + ); } else { - $this->traverseGenericNode($node, $context); + $this->traverseGenericNode( + $value, + $object, + $metadata, + $propertyPath, + $groups, + $cascadedGroups, + $traversalStrategy, + $context + ); } // Abort sequence validation if a violation was generated @@ -648,25 +639,25 @@ private function stepThroughGroupSequence(Node $node, GroupSequence $groupSequen * * @throws \Exception */ - private function validateNodeForGroup(Node $node, $group, ExecutionContextInterface $context, $objectHash) + private function validateNodeForGroup($value, $objectHash, MetadataInterface $metadata = null, $group, ExecutionContextInterface $context) { try { $context->setGroup($group); - foreach ($node->metadata->findConstraints($group) as $constraint) { + foreach ($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 ($metadata instanceof ClassMetadataInterface) { if ($context->isClassConstraintValidated($objectHash, $constraintHash)) { continue; } $context->markClassConstraintAsValidated($objectHash, $constraintHash); - } elseif ($node instanceof PropertyNode) { - $propertyName = $node->metadata->getPropertyName(); + } elseif ($metadata instanceof PropertyMetadataInterface) { + $propertyName = $metadata->getPropertyName(); if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { continue; @@ -678,7 +669,7 @@ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterf $validator = $this->validatorFactory->getInstance($constraint); $validator->initialize($context); - $validator->validate($node->value, $constraint); + $validator->validate($value, $constraint); } $context->setGroup(null); @@ -691,10 +682,42 @@ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterf } /** - * {@inheritdoc} + * @param $value + * @param ClassMetadataInterface $metadata + * @param array $groups + * + * @return array */ - public function getCurrentGroup() + private function replaceDefaultGroup($value, ClassMetadataInterface $metadata, array $groups) { - return $this->currentGroup; + $groupSequence = null; + + if ($metadata->hasGroupSequence()) { + // The group sequence is statically defined for the class + $groupSequence = $metadata->getGroupSequence(); + } elseif ($metadata->isGroupSequenceProvider()) { + // The group sequence is dynamically obtained from the validated + // object + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $groupSequence = $value->getGroupSequence(); + + if (!$groupSequence instanceof GroupSequence) { + $groupSequence = new GroupSequence($groupSequence); + } + } + + if (null !== $groupSequence) { + $key = array_search(Constraint::DEFAULT_GROUP, $groups); + + if (false !== $key) { + // Replace the "Default" group by the group sequence + $groups[$key] = $groupSequence; + + // Cascade the "Default" group when validating the sequence + $groupSequence->cascadedGroup = Constraint::DEFAULT_GROUP; + } + } + + return $groups; } } From 274d4e619572cc88743cab9be37fe659de6d2ea2 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 11 Mar 2014 11:00:37 +0100 Subject: [PATCH 63/86] [Validator] Changed ValidatorBuilder to always use LegacyExecutionContext This is necessary because, until Symfony 3.0, constraint validators will continue to rely on the old context methods in order to be backwards compatible. --- .../Context/LegacyExecutionContext.php | 23 ++++++++----------- .../Context/LegacyExecutionContextFactory.php | 19 +++++++++++---- .../Validator/LegacyValidator2Dot5ApiTest.php | 2 +- .../LegacyValidatorLegacyApiTest.php | 2 +- .../Validator/Validator/LegacyValidator.php | 9 ++++++++ .../Component/Validator/ValidatorBuilder.php | 6 +---- 6 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index abac3e9048fd8..c7fb24ab2d377 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -16,6 +16,7 @@ use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; @@ -30,33 +31,29 @@ */ class LegacyExecutionContext extends ExecutionContext { + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + /** * Creates a new context. * - * This constructor ensures that the given validator implements the - * deprecated {@link \Symfony\Component\Validator\ValidatorInterface}. If - * it does not, an {@link InvalidArgumentException} is thrown. - * * @see ExecutionContext::__construct() * * @internal Called by {@link LegacyExecutionContextFactory}. Should not be used * in user code. */ - public function __construct(ValidatorInterface $validator, $root, TranslatorInterface $translator, $translationDomain = null) + public function __construct(ValidatorInterface $validator, $root, MetadataFactoryInterface $metadataFactory, TranslatorInterface $translator, $translationDomain = null) { - if (!$validator instanceof LegacyValidatorInterface) { - throw new InvalidArgumentException( - 'The validator passed to LegacyExecutionContext must implement '. - '"Symfony\Component\Validator\ValidatorInterface".' - ); - } - parent::__construct( $validator, $root, $translator, $translationDomain ); + + $this->metadataFactory = $metadataFactory; } /** @@ -158,6 +155,6 @@ public function validateValue($value, $constraints, $subPath = '', $groups = nul */ public function getMetadataFactory() { - return $this->getValidator()->getMetadataFactory(); + return $this->metadataFactory; } } diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php index 88d3c0ff5d6f8..b44121af7a4c1 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php @@ -13,6 +13,7 @@ use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Group\GroupManagerInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; /** @@ -26,6 +27,11 @@ */ class LegacyExecutionContextFactory implements ExecutionContextFactoryInterface { + /** + * @var MetadataFactoryInterface + */ + private $metadataFactory; + /** * @var TranslatorInterface */ @@ -39,13 +45,15 @@ class LegacyExecutionContextFactory implements ExecutionContextFactoryInterface /** * Creates a new context factory. * - * @param TranslatorInterface $translator The translator - * @param string|null $translationDomain The translation domain to - * use for translating - * violation messages + * @param MetadataFactoryInterface $metadataFactory The metadata factory + * @param TranslatorInterface $translator The translator + * @param string|null $translationDomain The translation domain + * to use for translating + * violation messages */ - public function __construct(TranslatorInterface $translator, $translationDomain = null) + public function __construct(MetadataFactoryInterface $metadataFactory, TranslatorInterface $translator, $translationDomain = null) { + $this->metadataFactory = $metadataFactory; $this->translator = $translator; $this->translationDomain = $translationDomain; } @@ -58,6 +66,7 @@ public function createContext(ValidatorInterface $validator, $root) return new LegacyExecutionContext( $validator, $root, + $this->metadataFactory, $this->translator, $this->translationDomain ); diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index 3ff580b1103dd..eef4e84e7461b 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -34,7 +34,7 @@ protected function setUp() protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $contextFactory = new LegacyExecutionContextFactory(new DefaultTranslator()); + $contextFactory = new LegacyExecutionContextFactory($metadataFactory, new DefaultTranslator()); 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 493fa7a616439..494d9fc029e8e 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -34,7 +34,7 @@ protected function setUp() protected function createValidator(MetadataFactoryInterface $metadataFactory) { - $contextFactory = new LegacyExecutionContextFactory(new DefaultTranslator()); + $contextFactory = new LegacyExecutionContextFactory($metadataFactory, new DefaultTranslator()); return new LegacyValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory()); } diff --git a/src/Symfony/Component/Validator/Validator/LegacyValidator.php b/src/Symfony/Component/Validator/Validator/LegacyValidator.php index 8f93b67a893e5..12bea285c432c 100644 --- a/src/Symfony/Component/Validator/Validator/LegacyValidator.php +++ b/src/Symfony/Component/Validator/Validator/LegacyValidator.php @@ -19,6 +19,15 @@ /** * A validator that supports both the API of Symfony < 2.5 and Symfony 2.5+. * + * This class is incompatible with PHP versions < 5.3.9, because it implements + * two different interfaces specifying the same method validate(): + * + * - {@link \Symfony\Component\Validator\ValidatorInterface} + * - {@link \Symfony\Component\Validator\Validator\ValidatorInterface} + * + * In PHP versions prior to 5.3.9, either use {@link RecursiveValidator} or the + * deprecated class {@link \Symfony\Component\Validator\Validator} instead. + * * @since 2.5 * @author Bernhard Schussek * diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index 351c5d654e73d..b8216f77fe215 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -415,11 +415,7 @@ public function getValidator() return new ValidatorV24($metadataFactory, $validatorFactory, $translator, $this->translationDomain, $this->initializers); } - if (Validation::API_VERSION_2_5 === $apiVersion) { - $contextFactory = new ExecutionContextFactory($translator, $this->translationDomain); - } else { - $contextFactory = new LegacyExecutionContextFactory($translator, $this->translationDomain); - } + $contextFactory = new LegacyExecutionContextFactory($metadataFactory, $translator, $this->translationDomain); if (Validation::API_VERSION_2_5 === $apiVersion) { $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); From eeed509dfc43097a6243004db78e5855bdf418e7 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 11 Mar 2014 11:07:57 +0100 Subject: [PATCH 64/86] [Validator] Improved phpdoc of RecursiveValidator --- .../Validator/RecursiveContextualValidator.php | 15 ++++++++++----- .../Validator/Validator/RecursiveValidator.php | 17 +++++++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index e4278691af543..acd79165c06c2 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -34,7 +34,7 @@ use Symfony\Component\Validator\Util\PropertyPath; /** - * Default implementation of {@link ContextualValidatorInterface}. + * Recursive implementation of {@link ContextualValidatorInterface}. * * @since 2.5 * @author Bernhard Schussek @@ -51,15 +51,20 @@ class RecursiveContextualValidator implements ContextualValidatorInterface */ private $metadataFactory; + /** + * @var ConstraintValidatorFactoryInterface + */ private $validatorFactory; /** * 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 + * @param ExecutionContextInterface $context The execution context + * @param MetadataFactoryInterface $metadataFactory The factory for + * fetching the metadata + * of validated objects + * @param ConstraintValidatorFactoryInterface $validatorFactory The factory for creating + * constraint validators */ public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory) { diff --git a/src/Symfony/Component/Validator/Validator/RecursiveValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveValidator.php index a8f9307d71e23..d0a66f3d8a740 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveValidator.php @@ -17,7 +17,7 @@ use Symfony\Component\Validator\MetadataFactoryInterface; /** - * Default implementation of {@link ValidatorInterface}. + * Recursive implementation of {@link ValidatorInterface}. * * @since 2.5 * @author Bernhard Schussek @@ -34,16 +34,21 @@ class RecursiveValidator implements ValidatorInterface */ protected $metadataFactory; + /** + * @var ConstraintValidatorFactoryInterface + */ 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 + * @param ExecutionContextFactoryInterface $contextFactory The factory for + * creating new contexts + * @param MetadataFactoryInterface $metadataFactory The factory for + * fetching the metadata + * of validated objects + * @param ConstraintValidatorFactoryInterface $validatorFactory The factory for creating + * constraint validators */ public function __construct(ExecutionContextFactoryInterface $contextFactory, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory) { From 5c479d803c462a6455e9f3848c8229eff90801bc Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 11 Mar 2014 11:36:07 +0100 Subject: [PATCH 65/86] [Validator] Simplified validateNodeForGroup --- .../RecursiveContextualValidator.php | 49 ++++++++----------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index acd79165c06c2..ff6c6e740d52f 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -646,43 +646,34 @@ private function stepThroughGroupSequence($value, $object, MetadataInterface $me */ private function validateNodeForGroup($value, $objectHash, MetadataInterface $metadata = null, $group, ExecutionContextInterface $context) { - try { - $context->setGroup($group); - - foreach ($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); + $context->setGroup($group); - if ($metadata instanceof ClassMetadataInterface) { - if ($context->isClassConstraintValidated($objectHash, $constraintHash)) { - continue; - } + foreach ($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); - $context->markClassConstraintAsValidated($objectHash, $constraintHash); - } elseif ($metadata instanceof PropertyMetadataInterface) { - $propertyName = $metadata->getPropertyName(); + if ($metadata instanceof ClassMetadataInterface) { + if ($context->isClassConstraintValidated($objectHash, $constraintHash)) { + continue; + } - if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { - continue; - } + $context->markClassConstraintAsValidated($objectHash, $constraintHash); + } elseif ($metadata instanceof PropertyMetadataInterface) { + $propertyName = $metadata->getPropertyName(); - $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); + if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { + continue; } - } - $validator = $this->validatorFactory->getInstance($constraint); - $validator->initialize($context); - $validator->validate($value, $constraint); + $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); + } } - $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; + $validator = $this->validatorFactory->getInstance($constraint); + $validator->initialize($context); + $validator->validate($value, $constraint); } } From eed29d8ad34903f854d5323431ee59e7124cd62e Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 11 Mar 2014 14:16:20 +0100 Subject: [PATCH 66/86] [Validator] Improved performance of *ContextualValidator::validate() --- .../Exception/ValidatorException.php | 2 +- .../Tests/Fixtures/FakeClassMetadata.php | 3 - .../Tests/Fixtures/FakeMetadataFactory.php | 2 +- .../Tests/Validator/Abstract2Dot5ApiTest.php | 8 ++ .../Tests/Validator/AbstractValidatorTest.php | 4 + .../RecursiveContextualValidator.php | 73 ++++++++++++++----- .../TraversingContextualValidator.php | 68 +++++++++++++---- 7 files changed, 121 insertions(+), 39 deletions(-) diff --git a/src/Symfony/Component/Validator/Exception/ValidatorException.php b/src/Symfony/Component/Validator/Exception/ValidatorException.php index 6ee2416d84825..28bd4704e8fdb 100644 --- a/src/Symfony/Component/Validator/Exception/ValidatorException.php +++ b/src/Symfony/Component/Validator/Exception/ValidatorException.php @@ -11,6 +11,6 @@ namespace Symfony\Component\Validator\Exception; -class ValidatorException extends \RuntimeException +class ValidatorException extends RuntimeException { } diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php b/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php index c6b79f66f336d..5ae0e68a777e8 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/FakeClassMetadata.php @@ -11,10 +11,7 @@ namespace Symfony\Component\Validator\Tests\Fixtures; -use Symfony\Component\Validator\ClassBasedInterface; use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\MetadataInterface; -use Symfony\Component\Validator\PropertyMetadataContainerInterface; class FakeClassMetadata extends ClassMetadata { diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php index 09b0ca63bea53..852eb484b0ff1 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php @@ -49,8 +49,8 @@ public function hasMetadataFor($class) $hash = null; if (is_object($class)) { + $hash = spl_object_hash($class); $class = get_class($class); - $hash = spl_object_hash($hash); } if (!is_string($class)) { diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 48d89e05969bc..8698df384585b 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -650,4 +650,12 @@ public function testNoDuplicateValidationIfPropertyConstraintInMultipleGroups() /** @var ConstraintViolationInterface[] $violations */ $this->assertCount(1, $violations); } + + /** + * @expectedException \Symfony\Component\Validator\Exception\RuntimeException + */ + public function testValidateFailsIfNoConstraintsAndNoObjectOrArray() + { + $this->validate('Foobar'); + } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index 11e4d31eb6366..b1629ea50a470 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -837,6 +837,8 @@ public function testValidateProperty() } /** + * Cannot be UnsupportedMetadataException for BC with Symfony < 2.5. + * * @expectedException \Symfony\Component\Validator\Exception\ValidatorException */ public function testValidatePropertyFailsIfPropertiesNotSupported() @@ -903,6 +905,8 @@ public function testValidatePropertyValue() } /** + * Cannot be UnsupportedMetadataException for BC with Symfony < 2.5. + * * @expectedException \Symfony\Component\Validator\Exception\ValidatorException */ public function testValidatePropertyValueFailsIfPropertiesNotSupported() diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index ff6c6e740d52f..69b29a1c1ca73 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -13,11 +13,11 @@ 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\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\NoSuchMetadataException; +use Symfony\Component\Validator\Exception\RuntimeException; use Symfony\Component\Validator\Exception\UnsupportedMetadataException; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\CascadingStrategy; @@ -90,28 +90,59 @@ public function atPath($path) */ public function validate($value, $constraints = null, $groups = null) { - if (null === $constraints) { - $constraints = array(new Valid()); - } elseif (!is_array($constraints)) { - $constraints = array($constraints); + $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + + if (null !== $constraints) { + if (!is_array($constraints)) { + $constraints = array($constraints); + } + + $metadata = new GenericMetadata(); + $metadata->addConstraints($constraints); + + $this->traverseGenericNode( + $value, + null, + $metadata, + $this->defaultPropertyPath, + $groups, + null, + TraversalStrategy::IMPLICIT, + $this->context + ); + + return $this; } - $metadata = new GenericMetadata(); - $metadata->addConstraints($constraints); - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + if (is_object($value)) { + $this->cascadeObject( + $value, + $this->defaultPropertyPath, + $groups, + TraversalStrategy::IMPLICIT, + $this->context + ); - $this->traverseGenericNode( - $value, - null, - $metadata, - $this->defaultPropertyPath, - $groups, - null, - TraversalStrategy::IMPLICIT, - $this->context - ); + return $this; + } - return $this; + if (is_array($value)) { + $this->cascadeCollection( + $value, + $this->defaultPropertyPath, + $groups, + TraversalStrategy::IMPLICIT, + $this->context + ); + + return $this; + } + + throw new RuntimeException(sprintf( + 'Cannot validate values of type "%s" automatically. Please '. + 'provide a constraint.', + gettype($value) + )); } /** @@ -122,6 +153,8 @@ public function validateProperty($object, $propertyName, $groups = null) $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { + // Cannot be UnsupportedMetadataException because of BC with + // Symfony < 2.5 throw new ValidatorException(sprintf( 'The metadata factory should return instances of '. '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. @@ -159,6 +192,8 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { + // Cannot be UnsupportedMetadataException because of BC with + // Symfony < 2.5 throw new ValidatorException(sprintf( 'The metadata factory should return instances of '. '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. diff --git a/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php index 2f23ce71f3315..288fd8a66c603 100644 --- a/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php @@ -12,12 +12,15 @@ namespace Symfony\Component\Validator\Validator; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\RuntimeException; +use Symfony\Component\Validator\Exception\UnsupportedMetadataException; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\GenericMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Node\ClassNode; +use Symfony\Component\Validator\Node\CollectionNode; use Symfony\Component\Validator\Node\GenericNode; use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; @@ -79,22 +82,53 @@ public function atPath($path) */ 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; - $node = new GenericNode( - $value, - $metadata, - $this->defaultPropertyPath, - $groups - ); + if (null !== $constraints) { + if (!is_array($constraints)) { + $constraints = array($constraints); + } + + $metadata = new GenericMetadata(); + $metadata->addConstraints($constraints); + + $node = new GenericNode( + $value, + $metadata, + $this->defaultPropertyPath, + $groups + ); + } elseif (is_array($value) || $value instanceof \Traversable && !$this->metadataFactory->hasMetadataFor($value)) { + $node = new CollectionNode( + $value, + $this->defaultPropertyPath, + $groups + ); + } elseif (is_object($value)) { + $metadata = $this->metadataFactory->getMetadataFor($value); + + if (!$metadata instanceof ClassMetadataInterface) { + throw new UnsupportedMetadataException(sprintf( + 'The metadata factory should return instances of '. + '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. + 'got: "%s".', + is_object($metadata) ? get_class($metadata) : gettype($metadata) + )); + } + + $node = new ClassNode( + $value, + $metadata, + $this->defaultPropertyPath, + $groups + ); + } else { + throw new RuntimeException(sprintf( + 'Cannot validate values of type "%s" automatically. Please '. + 'provide a constraint.', + gettype($value) + )); + } $this->nodeTraverser->traverse(array($node), $this->context); @@ -109,6 +143,8 @@ public function validateProperty($object, $propertyName, $groups = null) $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { + // Cannot be UnsupportedMetadataException because of BC with + // Symfony < 2.5 throw new ValidatorException(sprintf( 'The metadata factory should return instances of '. '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. @@ -146,6 +182,8 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { + // Cannot be UnsupportedMetadataException because of BC with + // Symfony < 2.5 throw new ValidatorException(sprintf( 'The metadata factory should return instances of '. '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. From 50bb84d06bc8b437000a1d21db83b6cc4b951fd2 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 11 Mar 2014 14:45:21 +0100 Subject: [PATCH 67/86] [Validator] Optimized RecursiveContextualValidator --- .../NodeVisitor/NodeValidationVisitor.php | 16 ++++++------- .../RecursiveContextualValidator.php | 23 +++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index 9d18c7ef48503..65e9bcc78868f 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -103,18 +103,18 @@ public function visit(Node $node, ExecutionContextInterface $context) $context->markObjectAsValidatedForGroup($objectHash, $groupHash); } - // Validate normal group - if (!$group instanceof GroupSequence) { - $this->validateNodeForGroup($node, $group, $context, $objectHash); + if ($group instanceof GroupSequence) { + // Traverse group sequence until a violation is generated + $this->traverseGroupSequence($node, $group, $context); + + // Skip the group sequence when validating successor nodes + unset($node->groups[$key]); continue; } - // Traverse group sequence until a violation is generated - $this->traverseGroupSequence($node, $group, $context); - - // Skip the group sequence when validating successor nodes - unset($node->groups[$key]); + // Validate normal group + $this->validateNodeForGroup($node, $group, $context, $objectHash); } return true; diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index 69b29a1c1ca73..e3872b4232c3f 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -268,6 +268,9 @@ protected function normalizeGroups($groups) */ private function traverseClassNode($value, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { + // Replace "Default" group by group sequence, if appropriate + $groups = $this->replaceDefaultGroup($value, $metadata, $groups); + $groups = $this->validateNode($value, $value, $metadata, $propertyPath, $groups, $traversalStrategy, $context); if (0 === count($groups)) { @@ -566,10 +569,6 @@ public function validateNode($value, $object, MetadataInterface $metadata = null $context->setMetadata($metadata); $context->setPropertyPath($propertyPath); - if ($metadata instanceof ClassMetadataInterface) { - $groups = $this->replaceDefaultGroup($value, $metadata, $groups); - } - $objectHash = is_object($object) ? spl_object_hash($object) : null; // if group (=[,G3,G4]) contains group sequence (=) @@ -599,18 +598,18 @@ public function validateNode($value, $object, MetadataInterface $metadata = null $context->markObjectAsValidatedForGroup($objectHash, $groupHash); } - // Validate normal group - if (!$group instanceof GroupSequence) { - $this->validateNodeForGroup($value, $objectHash, $metadata, $group, $context); + if ($group instanceof GroupSequence) { + // Traverse group sequence until a violation is generated + $this->stepThroughGroupSequence($value, $object, $metadata, $propertyPath, $traversalStrategy, $group, $context); + + // Skip the group sequence when validating successor nodes + unset($groups[$key]); continue; } - // Traverse group sequence until a violation is generated - $this->stepThroughGroupSequence($value, $object, $metadata, $propertyPath, $traversalStrategy, $group, $context); - - // Skip the group sequence when validating successor nodes - unset($groups[$key]); + // Validate normal group + $this->validateNodeForGroup($value, $objectHash, $metadata, $group, $context); } return $groups; From be508e01dd179a57d45001f0cbfbcdcf75aa1f49 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 11 Mar 2014 14:56:15 +0100 Subject: [PATCH 68/86] [Validator] Merged DefaultGroupReplacingVisitor and ContextUpdateVisitor into NodeValidationVisitor --- .../NodeVisitor/ContextUpdateVisitor.php | 37 ---------- .../DefaultGroupReplacingVisitor.php | 73 ------------------- .../NodeVisitor/NodeValidationVisitor.php | 47 ++++++++++++ .../TraversingValidator2Dot5ApiTest.php | 8 +- 4 files changed, 48 insertions(+), 117 deletions(-) delete mode 100644 src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php delete mode 100644 src/Symfony/Component/Validator/NodeVisitor/DefaultGroupReplacingVisitor.php diff --git a/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php deleted file mode 100644 index 03243960b1d05..0000000000000 --- a/src/Symfony/Component/Validator/NodeVisitor/ContextUpdateVisitor.php +++ /dev/null @@ -1,37 +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\NodeVisitor; - -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Node\Node; - -/** - * Informs the execution context about the currently validated node. - * - * @since 2.5 - * @author Bernhard Schussek - */ -class ContextUpdateVisitor extends AbstractVisitor -{ - /** - * Updates the execution context. - * - * @param Node $node The current node - * @param ExecutionContextInterface $context The execution context - */ - public function visit(Node $node, ExecutionContextInterface $context) - { - $context->setValue($node->value); - $context->setMetadata($node->metadata); - $context->setPropertyPath($node->propertyPath); - } -} diff --git a/src/Symfony/Component/Validator/NodeVisitor/DefaultGroupReplacingVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/DefaultGroupReplacingVisitor.php deleted file mode 100644 index 6d152edf9c286..0000000000000 --- a/src/Symfony/Component/Validator/NodeVisitor/DefaultGroupReplacingVisitor.php +++ /dev/null @@ -1,73 +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\NodeVisitor; - -use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\GroupSequence; -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\Node; - -/** - * Checks class nodes whether their "Default" group is replaced by a group - * sequence and adjusts the validation groups accordingly. - * - * If the "Default" group is replaced for a class node, and if the validated - * groups of the node contain the group "Default", that group is replaced by - * the group sequence specified in the class' metadata. - * - * @since 2.5 - * @author Bernhard Schussek - */ -class DefaultGroupReplacingVisitor extends AbstractVisitor -{ - /** - * Replaces the "Default" group in the node's groups by the class' group - * sequence. - * - * @param Node $node The current node - * @param ExecutionContextInterface $context The execution context - */ - public function visit(Node $node, ExecutionContextInterface $context) - { - if (!$node instanceof ClassNode) { - return; - } - - 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); - } - } else { - // The "Default" group is not overridden. Quit. - return; - } - - $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; - } - } -} diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index 65e9bcc78868f..3a5a0f2bf8a33 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\NodeVisitor; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -68,7 +69,13 @@ public function visit(Node $node, ExecutionContextInterface $context) return true; } + $context->setValue($node->value); + $context->setMetadata($node->metadata); + $context->setPropertyPath($node->propertyPath); + if ($node instanceof ClassNode) { + $this->replaceDefaultGroup($node); + $objectHash = spl_object_hash($node->value); } elseif ($node instanceof PropertyNode) { $objectHash = spl_object_hash($node->object); @@ -203,4 +210,44 @@ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterf throw $e; } } + + /** + * Checks class nodes whether their "Default" group is replaced by a group + * sequence and adjusts the validation groups accordingly. + * + * If the "Default" group is replaced for a class node, and if the validated + * groups of the node contain the group "Default", that group is replaced by + * the group sequence specified in the class' metadata. + * + * @param ClassNode $node The node + */ + private function replaceDefaultGroup(ClassNode $node) + { + 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); + } + } else { + // The "Default" group is not overridden. Quit. + return; + } + + $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; + } + } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php index 61e78456b3e00..7f96944b77ce3 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php @@ -29,13 +29,7 @@ protected function createValidator(MetadataFactoryInterface $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); - $nodeTraverser->addVisitor($nodeValidator); + $nodeTraverser->addVisitor(new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory())); return $validator; } From 1622eb3a98a9f74a1e6b3eabfcb35176547461a6 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 11 Mar 2014 15:16:49 +0100 Subject: [PATCH 69/86] [Validator] Fixed reference to removed class in ValidatorBuilder --- src/Symfony/Component/Validator/ValidatorBuilder.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index b8216f77fe215..4f536c15551c4 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -14,7 +14,6 @@ use Doctrine\Common\Annotations\AnnotationRegistry; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\Validator\Context\ExecutionContextFactory; use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\Exception\InvalidArgumentException; use Symfony\Component\Validator\Mapping\ClassMetadataFactory; @@ -33,8 +32,6 @@ use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Cache\ArrayCache; use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; -use Symfony\Component\Validator\NodeVisitor\ContextUpdateVisitor; -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; @@ -422,8 +419,6 @@ public function getValidator() 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); From 94ef21e495155836e41dee515550121e160710fb Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Mar 2014 16:44:19 +0100 Subject: [PATCH 70/86] [Validator] Optimized use statements --- .../Component/Validator/Constraint.php | 2 +- .../Constraints/CallbackValidator.php | 2 +- .../Constraints/ExpressionValidator.php | 2 +- .../Validator/Constraints/FileValidator.php | 4 +-- .../Component/Validator/Constraints/Ip.php | 2 +- .../Validator/Context/ExecutionContext.php | 2 -- .../Context/ExecutionContextFactory.php | 1 - .../Context/ExecutionContextInterface.php | 1 - .../Context/LegacyExecutionContext.php | 4 --- .../Context/LegacyExecutionContextFactory.php | 1 - .../Component/Validator/DefaultTranslator.php | 2 +- .../Validator/Mapping/Cache/DoctrineCache.php | 2 +- .../Validator/Mapping/ClassMetadata.php | 8 +++--- .../Mapping/ClassMetadataFactory.php | 4 +-- .../Mapping/ClassMetadataInterface.php | 4 ++- .../Mapping/Loader/AbstractLoader.php | 2 +- .../Mapping/Loader/AnnotationLoader.php | 6 ++--- .../Mapping/Loader/XmlFileLoader.php | 2 +- .../Validator/Mapping/MemberMetadata.php | 2 +- .../Tests/Constraints/AllValidatorTest.php | 5 ++-- .../Constraints/CallbackValidatorTest.php | 2 +- .../Tests/Constraints/CollectionTest.php | 4 +-- .../Constraints/CollectionValidatorTest.php | 8 +++--- .../Tests/Constraints/FileValidatorTest.php | 2 +- .../Validator/Tests/ExecutionContextTest.php | 4 +-- .../Tests/Fixtures/FakeMetadataFactory.php | 1 - .../Tests/Mapping/Cache/DoctrineCacheTest.php | 2 +- .../Mapping/ClassMetadataFactoryTest.php | 5 ++-- .../Tests/Mapping/ClassMetadataTest.php | 2 -- .../Tests/Mapping/ElementMetadataTest.php | 2 +- .../Mapping/Loader/AnnotationLoaderTest.php | 2 +- .../Tests/Mapping/Loader/FilesLoaderTest.php | 2 +- .../Mapping/Loader/XmlFileLoaderTest.php | 2 +- .../Mapping/Loader/YamlFileLoaderTest.php | 2 +- .../Tests/Mapping/MemberMetadataTest.php | 6 ++--- .../Tests/Validator/AbstractValidatorTest.php | 6 ++--- .../Validator/LegacyValidator2Dot5ApiTest.php | 6 +---- .../LegacyValidatorLegacyApiTest.php | 6 +---- .../RecursiveValidator2Dot5ApiTest.php | 7 +----- .../TraversingValidator2Dot5ApiTest.php | 6 ++--- .../Validator/Tests/ValidatorTest.php | 4 +-- .../Component/Validator/ValidationVisitor.php | 2 +- src/Symfony/Component/Validator/Validator.php | 2 +- .../Validator/TraversingValidator.php | 2 +- .../Component/Validator/ValidatorBuilder.php | 25 ++++++++++--------- .../Validator/ValidatorBuilderInterface.php | 4 +-- 46 files changed, 74 insertions(+), 100 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraint.php b/src/Symfony/Component/Validator/Constraint.php index 0f2c226ae9f77..7f7f6b83c4208 100644 --- a/src/Symfony/Component/Validator/Constraint.php +++ b/src/Symfony/Component/Validator/Constraint.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Validator; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\InvalidOptionsException; use Symfony\Component\Validator\Exception\MissingOptionsException; -use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * Contains the properties of a constraint definition. diff --git a/src/Symfony/Component/Validator/Constraints/CallbackValidator.php b/src/Symfony/Component/Validator/Constraints/CallbackValidator.php index 57e28fb82f137..c378e55e769f7 100644 --- a/src/Symfony/Component/Validator/Constraints/CallbackValidator.php +++ b/src/Symfony/Component/Validator/Constraints/CallbackValidator.php @@ -13,8 +13,8 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; -use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; /** * Validator for Callback constraint diff --git a/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php b/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php index 8323e5f6dbad6..0151dfe6bc2db 100644 --- a/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php +++ b/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; -use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\Validator\Exception\RuntimeException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; diff --git a/src/Symfony/Component/Validator/Constraints/FileValidator.php b/src/Symfony/Component/Validator/Constraints/FileValidator.php index 54f0b45d60dae..f789fb108418a 100644 --- a/src/Symfony/Component/Validator/Constraints/FileValidator.php +++ b/src/Symfony/Component/Validator/Constraints/FileValidator.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Validator\Constraints; +use Symfony\Component\HttpFoundation\File\File as FileObject; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; -use Symfony\Component\HttpFoundation\File\File as FileObject; -use Symfony\Component\HttpFoundation\File\UploadedFile; /** * @author Bernhard Schussek diff --git a/src/Symfony/Component/Validator/Constraints/Ip.php b/src/Symfony/Component/Validator/Constraints/Ip.php index 099f2aabd7348..0f124f9c05bff 100644 --- a/src/Symfony/Component/Validator/Constraints/Ip.php +++ b/src/Symfony/Component/Validator/Constraints/Ip.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Validator\Constraints; -use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; /** * Validates that a value is a valid IP address diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 959f44ea79cd4..683ddd8c97329 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -16,10 +16,8 @@ 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; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php index 5e660f47b0c92..52bd1e6907cea 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Validator\Context; use Symfony\Component\Translation\TranslatorInterface; -use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; /** diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index 241dc03107865..b3224d5dd7a98 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -13,7 +13,6 @@ 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; diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php index c7fb24ab2d377..de34b1fc2cae6 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContext.php @@ -12,13 +12,9 @@ namespace Symfony\Component\Validator\Context; use Symfony\Component\Translation\TranslatorInterface; -use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; -use Symfony\Component\Validator\Exception\InvalidArgumentException; -use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; -use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface; /** * An execution context that is compatible with the legacy API (< 2.5). diff --git a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php index b44121af7a4c1..cf5cd07e9ef54 100644 --- a/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/LegacyExecutionContextFactory.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Validator\Context; use Symfony\Component\Translation\TranslatorInterface; -use Symfony\Component\Validator\Group\GroupManagerInterface; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; diff --git a/src/Symfony/Component/Validator/DefaultTranslator.php b/src/Symfony/Component/Validator/DefaultTranslator.php index 20b2e11350839..3340cce6330b9 100644 --- a/src/Symfony/Component/Validator/DefaultTranslator.php +++ b/src/Symfony/Component/Validator/DefaultTranslator.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Validator; +use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Exception\BadMethodCallException; use Symfony\Component\Validator\Exception\InvalidArgumentException; -use Symfony\Component\Translation\TranslatorInterface; /** * Simple translator implementation that simply replaces the parameters in diff --git a/src/Symfony/Component/Validator/Mapping/Cache/DoctrineCache.php b/src/Symfony/Component/Validator/Mapping/Cache/DoctrineCache.php index 56ead5d0ccc05..6dd5447fedc88 100644 --- a/src/Symfony/Component/Validator/Mapping/Cache/DoctrineCache.php +++ b/src/Symfony/Component/Validator/Mapping/Cache/DoctrineCache.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Validator\Mapping\Cache; -use Symfony\Component\Validator\Mapping\ClassMetadata; use Doctrine\Common\Cache\Cache; +use Symfony\Component\Validator\Mapping\ClassMetadata; /** * Adapts a Doctrine cache to a CacheInterface. diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index e074f442922c5..28bf5cda678bf 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -11,15 +11,15 @@ namespace Symfony\Component\Validator\Mapping; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\Constraints\Valid; -use Symfony\Component\Validator\ValidationVisitorInterface; -use Symfony\Component\Validator\PropertyMetadataContainerInterface; -use Symfony\Component\Validator\MetadataInterface as LegacyMetadataInterface; -use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\GroupDefinitionException; +use Symfony\Component\Validator\MetadataInterface as LegacyMetadataInterface; +use Symfony\Component\Validator\PropertyMetadataContainerInterface; +use Symfony\Component\Validator\ValidationVisitorInterface; /** * Default implementation of {@link ClassMetadataInterface}. diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php index 39bc32a201e86..8c26b7acecaba 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Validator\Mapping; -use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Exception\NoSuchMetadataException; -use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; use Symfony\Component\Validator\Mapping\Cache\CacheInterface; +use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; +use Symfony\Component\Validator\MetadataFactoryInterface; /** * Creates new {@link ClassMetadataInterface} instances. diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php index f457cabffb02c..0e0d2448d7c59 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php @@ -12,7 +12,9 @@ namespace Symfony\Component\Validator\Mapping; use Symfony\Component\Validator\ClassBasedInterface; -use Symfony\Component\Validator\PropertyMetadataContainerInterface as LegacyPropertyMetadataContainerInterface;; +use Symfony\Component\Validator\PropertyMetadataContainerInterface as LegacyPropertyMetadataContainerInterface; + +; /** * Stores all metadata needed for validating objects of specific class. diff --git a/src/Symfony/Component/Validator/Mapping/Loader/AbstractLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/AbstractLoader.php index 9b5093e1bd928..24591d6be5487 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/AbstractLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/AbstractLoader.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Validator\Mapping\Loader; -use Symfony\Component\Validator\Exception\MappingException; use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\MappingException; abstract class AbstractLoader implements LoaderInterface { diff --git a/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php index 3a624072e0242..9cd86a1a66002 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php @@ -12,12 +12,12 @@ namespace Symfony\Component\Validator\Mapping\Loader; use Doctrine\Common\Annotations\Reader; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Callback; -use Symfony\Component\Validator\Exception\MappingException; -use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\GroupSequenceProvider; -use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\MappingException; +use Symfony\Component\Validator\Mapping\ClassMetadata; class AnnotationLoader implements LoaderInterface { diff --git a/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php index 3f157c924195d..0cf33e0edff65 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Validator\Mapping\Loader; +use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\Validator\Exception\MappingException; use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Config\Util\XmlUtils; class XmlFileLoader extends FileLoader { diff --git a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php index 9423cfd6c550c..9b4860cfaf859 100644 --- a/src/Symfony/Component/Validator/Mapping/MemberMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/MemberMetadata.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Validator\Mapping; -use Symfony\Component\Validator\ValidationVisitorInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\ValidationVisitorInterface; /** * Stores all metadata needed for validating a class property. diff --git a/src/Symfony/Component/Validator/Tests/Constraints/AllValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/AllValidatorTest.php index eaa9044e9ef52..3a654a3b457eb 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/AllValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/AllValidatorTest.php @@ -11,11 +11,10 @@ namespace Symfony\Component\Validator\Tests\Constraints; -use Symfony\Component\Validator\ExecutionContext; -use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\AllValidator; +use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\Constraints\Range; class AllValidatorTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php index 98f12cb954b35..06883e3525b15 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php @@ -12,9 +12,9 @@ namespace Symfony\Component\Validator\Tests\Constraints; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\ExecutionContext; use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\CallbackValidator; +use Symfony\Component\Validator\ExecutionContext; class CallbackValidatorTest_Class { diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CollectionTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CollectionTest.php index da868d3cfdd1f..4b485a9b108b7 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CollectionTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CollectionTest.php @@ -12,9 +12,9 @@ namespace Symfony\Component\Validator\Tests\Constraints; use Symfony\Component\Validator\Constraints\Collection; -use Symfony\Component\Validator\Constraints\Required; -use Symfony\Component\Validator\Constraints\Optional; use Symfony\Component\Validator\Constraints\Email; +use Symfony\Component\Validator\Constraints\Optional; +use Symfony\Component\Validator\Constraints\Required; use Symfony\Component\Validator\Constraints\Valid; /** diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CollectionValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CollectionValidatorTest.php index 4a13234b69550..4dc5f7e8bc7a5 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CollectionValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CollectionValidatorTest.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Validator\Tests\Constraints; -use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\NotNull; -use Symfony\Component\Validator\Constraints\Required; -use Symfony\Component\Validator\Constraints\Optional; use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\CollectionValidator; +use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\Constraints\Optional; +use Symfony\Component\Validator\Constraints\Range; +use Symfony\Component\Validator\Constraints\Required; abstract class CollectionValidatorTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php index 0927aedacdd5c..f5178cc029af7 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Validator\Tests\Constraints; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\Validator\Constraints\File; use Symfony\Component\Validator\Constraints\FileValidator; -use Symfony\Component\HttpFoundation\File\UploadedFile; abstract class FileValidatorTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/ExecutionContextTest.php b/src/Symfony/Component/Validator/Tests/ExecutionContextTest.php index dcc9c027bf75b..d58bf25ba6b2f 100644 --- a/src/Symfony/Component/Validator/Tests/ExecutionContextTest.php +++ b/src/Symfony/Component/Validator/Tests/ExecutionContextTest.php @@ -11,13 +11,13 @@ namespace Symfony\Component\Validator\Tests; +use Symfony\Component\Validator\Constraints\Collection; +use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\ExecutionContext; -use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\ValidationVisitor; -use Symfony\Component\Validator\ConstraintValidatorFactory; class ExecutionContextTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php index 852eb484b0ff1..e3f0d9a007800 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/FakeMetadataFactory.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Validator\Tests\Fixtures; use Symfony\Component\Validator\Exception\NoSuchMetadataException; -use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\MetadataInterface; diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Cache/DoctrineCacheTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Cache/DoctrineCacheTest.php index df2d9f4104ce8..f238a899d6176 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Cache/DoctrineCacheTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Cache/DoctrineCacheTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Validator\Tests\Mapping\Cache; -use Symfony\Component\Validator\Mapping\Cache\DoctrineCache; use Doctrine\Common\Cache\ArrayCache; +use Symfony\Component\Validator\Mapping\Cache\DoctrineCache; class DoctrineCacheTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataFactoryTest.php b/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataFactoryTest.php index bee4025d0d279..aee137a1f8c02 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataFactoryTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataFactoryTest.php @@ -11,11 +11,10 @@ namespace Symfony\Component\Validator\Tests\Mapping; -use Symfony\Component\Validator\Tests\Fixtures\Entity; -use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; -use Symfony\Component\Validator\Mapping\ClassMetadataFactory; use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\ClassMetadataFactory; use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; class ClassMetadataFactoryTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php b/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php index 9ead7d134eb60..9579b36b5b560 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/ClassMetadataTest.php @@ -14,8 +14,6 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\Exception\GroupDefinitionException; -use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; use Symfony\Component\Validator\Tests\Fixtures\PropertyConstraint; diff --git a/src/Symfony/Component/Validator/Tests/Mapping/ElementMetadataTest.php b/src/Symfony/Component/Validator/Tests/Mapping/ElementMetadataTest.php index 8cf3e6dec4683..9539b0f22cfed 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/ElementMetadataTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/ElementMetadataTest.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Validator\Tests\Mapping; +use Symfony\Component\Validator\Mapping\ElementMetadata; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; -use Symfony\Component\Validator\Mapping\ElementMetadata; class ElementMetadataTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php index e4ea6cfc6a498..8da207ff9de57 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php @@ -14,10 +14,10 @@ use Doctrine\Common\Annotations\AnnotationReader; use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\True; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/FilesLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/FilesLoaderTest.php index 7723349e94d8e..09e6e449e0309 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/FilesLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/FilesLoaderTest.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Validator\Tests\Mapping\Loader; -use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; class FilesLoaderTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php index e7243edc2c31f..e2b27f0bb6107 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -13,10 +13,10 @@ use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\Regex; use Symfony\Component\Validator\Constraints\True; use Symfony\Component\Validator\Mapping\ClassMetadata; diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php index 1de902a551a40..aeccf0c2836ab 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -13,10 +13,10 @@ use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\Choice; use Symfony\Component\Validator\Constraints\True; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader; diff --git a/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php b/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php index bfb402cdee2be..f91088de0b016 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/MemberMetadataTest.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Tests\Mapping; -use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; -use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; -use Symfony\Component\Validator\Tests\Fixtures\ClassConstraint; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Mapping\MemberMetadata; +use Symfony\Component\Validator\Tests\Fixtures\ClassConstraint; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; class MemberMetadataTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index b1629ea50a470..2808d38137ac6 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -13,14 +13,14 @@ use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ExecutionContextInterface; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; -use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Tests\Fixtures\GroupSequenceProviderEntity; use Symfony\Component\Validator\Tests\Fixtures\Reference; -use Symfony\Component\Validator\Tests\Fixtures\Entity; -use Symfony\Component\Validator\Mapping\ClassMetadata; /** * @since 2.5 diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php index eef4e84e7461b..7faeea6284e9c 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -11,14 +11,10 @@ namespace Symfony\Component\Validator\Tests\Validator; -use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; +use Symfony\Component\Validator\DefaultTranslator; 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\LegacyValidator; class LegacyValidator2Dot5ApiTest extends Abstract2Dot5ApiTest diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index 494d9fc029e8e..581e6768399aa 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -11,14 +11,10 @@ namespace Symfony\Component\Validator\Tests\Validator; -use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; +use Symfony\Component\Validator\DefaultTranslator; 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\LegacyValidator; class LegacyValidatorLegacyApiTest extends AbstractLegacyApiTest diff --git a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php index 57134410dcb08..da43279638852 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php @@ -11,16 +11,11 @@ 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\DefaultTranslator; 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 { diff --git a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php index 7f96944b77ce3..5c76a173a4c53 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php @@ -11,14 +11,12 @@ 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\DefaultTranslator; 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\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\Validator\TraversingValidator; class TraversingValidator2Dot5ApiTest extends Abstract2Dot5ApiTest diff --git a/src/Symfony/Component/Validator/Tests/ValidatorTest.php b/src/Symfony/Component/Validator/Tests/ValidatorTest.php index a983a78a70819..0f588fef8d3db 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorTest.php @@ -12,12 +12,12 @@ namespace Symfony\Component\Validator\Tests; use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\ConstraintValidatorFactory; +use Symfony\Component\Validator\DefaultTranslator; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Tests\Validator\AbstractLegacyApiTest; use Symfony\Component\Validator\Validator as LegacyValidator; -use Symfony\Component\Validator\DefaultTranslator; -use Symfony\Component\Validator\ConstraintValidatorFactory; class ValidatorTest extends AbstractLegacyApiTest { diff --git a/src/Symfony/Component/Validator/ValidationVisitor.php b/src/Symfony/Component/Validator/ValidationVisitor.php index 302d33c9d7f43..fef687491f4b9 100644 --- a/src/Symfony/Component/Validator/ValidationVisitor.php +++ b/src/Symfony/Component/Validator/ValidationVisitor.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Validator; +use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Exception\NoSuchMetadataException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; -use Symfony\Component\Translation\TranslatorInterface; /** * Default implementation of {@link ValidationVisitorInterface} and diff --git a/src/Symfony/Component/Validator/Validator.php b/src/Symfony/Component/Validator/Validator.php index 31dd4b9c384f5..849ef489fe56b 100644 --- a/src/Symfony/Component/Validator/Validator.php +++ b/src/Symfony/Component/Validator/Validator.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Validator; +use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Exception\ValidatorException; -use Symfony\Component\Translation\TranslatorInterface; /** * Default implementation of {@link ValidatorInterface}. diff --git a/src/Symfony/Component/Validator/Validator/TraversingValidator.php b/src/Symfony/Component/Validator/Validator/TraversingValidator.php index 4352b3180f81a..8fe07f630fb1f 100644 --- a/src/Symfony/Component/Validator/Validator/TraversingValidator.php +++ b/src/Symfony/Component/Validator/Validator/TraversingValidator.php @@ -13,8 +13,8 @@ use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; /** * Default implementation of {@link ValidatorInterface}. diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index 4f536c15551c4..2d66fa9f6af5b 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -11,32 +11,32 @@ namespace Symfony\Component\Validator; +use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\AnnotationRegistry; +use Doctrine\Common\Annotations\CachedReader; +use Doctrine\Common\Annotations\Reader; +use Doctrine\Common\Cache\ArrayCache; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Context\LegacyExecutionContextFactory; use Symfony\Component\Validator\Exception\InvalidArgumentException; -use Symfony\Component\Validator\Mapping\ClassMetadataFactory; use Symfony\Component\Validator\Exception\ValidatorException; -use Symfony\Component\Validator\Mapping\Loader\LoaderChain; use Symfony\Component\Validator\Mapping\Cache\CacheInterface; -use Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader; -use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader; +use Symfony\Component\Validator\Mapping\ClassMetadataFactory; use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; -use Symfony\Component\Validator\Mapping\Loader\YamlFilesLoader; +use Symfony\Component\Validator\Mapping\Loader\LoaderChain; +use Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader; use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader; use Symfony\Component\Validator\Mapping\Loader\XmlFilesLoader; -use Symfony\Component\Translation\TranslatorInterface; -use Doctrine\Common\Annotations\Reader; -use Doctrine\Common\Annotations\AnnotationReader; -use Doctrine\Common\Annotations\CachedReader; -use Doctrine\Common\Cache\ArrayCache; +use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader; +use Symfony\Component\Validator\Mapping\Loader\YamlFilesLoader; use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeVisitor\ObjectInitializationVisitor; -use Symfony\Component\Validator\Validator as ValidatorV24; -use Symfony\Component\Validator\Validator\TraversingValidator; use Symfony\Component\Validator\Validator\LegacyValidator; +use Symfony\Component\Validator\Validator\TraversingValidator; +use Symfony\Component\Validator\Validator as ValidatorV24; /** * The default implementation of {@link ValidatorBuilderInterface}. @@ -378,6 +378,7 @@ public function getValidator() if (is_file($file)) { require_once $file; + return true; } } diff --git a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php index d486faed127cc..e35822e2621d8 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php +++ b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Validator; +use Doctrine\Common\Annotations\Reader; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\Validator\Mapping\Cache\CacheInterface; use Symfony\Component\Translation\TranslatorInterface; -use Doctrine\Common\Annotations\Reader; +use Symfony\Component\Validator\Mapping\Cache\CacheInterface; /** * A configurable builder for ValidatorInterface objects. From 73c9cc58064fb7facece7e29662f984260dd4bc6 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Mar 2014 17:45:55 +0100 Subject: [PATCH 71/86] [Validator] Optimized performance by calling spl_object_hash() only once per object --- .../RecursiveContextualValidator.php | 123 ++++++++++-------- 1 file changed, 71 insertions(+), 52 deletions(-) diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index e3872b4232c3f..b1b4b714cd281 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -102,6 +102,8 @@ public function validate($value, $constraints = null, $groups = null) $this->traverseGenericNode( $value, + is_object($value) ? spl_object_hash($value) : null, + null, null, $metadata, $this->defaultPropertyPath, @@ -116,11 +118,12 @@ public function validate($value, $constraints = null, $groups = null) if (is_object($value)) { $this->cascadeObject( - $value, - $this->defaultPropertyPath, - $groups, - TraversalStrategy::IMPLICIT, - $this->context + $value, + spl_object_hash($value), + $this->defaultPropertyPath, + $groups, + TraversalStrategy::IMPLICIT, + $this->context ); return $this; @@ -128,11 +131,11 @@ public function validate($value, $constraints = null, $groups = null) if (is_array($value)) { $this->cascadeCollection( - $value, - $this->defaultPropertyPath, - $groups, - TraversalStrategy::IMPLICIT, - $this->context + $value, + $this->defaultPropertyPath, + $groups, + TraversalStrategy::IMPLICIT, + $this->context ); return $this; @@ -148,9 +151,9 @@ public function validate($value, $constraints = null, $groups = null) /** * {@inheritdoc} */ - public function validateProperty($object, $propertyName, $groups = null) + public function validateProperty($container, $propertyName, $groups = null) { - $classMetadata = $this->metadataFactory->getMetadataFor($object); + $classMetadata = $this->metadataFactory->getMetadataFor($container); if (!$classMetadata instanceof ClassMetadataInterface) { // Cannot be UnsupportedMetadataException because of BC with @@ -165,13 +168,16 @@ public function validateProperty($object, $propertyName, $groups = null) $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $containerHash = spl_object_hash($container); foreach ($propertyMetadatas as $propertyMetadata) { - $propertyValue = $propertyMetadata->getPropertyValue($object); + $propertyValue = $propertyMetadata->getPropertyValue($container); $this->traverseGenericNode( $propertyValue, - $object, + is_object($propertyValue) ? spl_object_hash($propertyValue) : null, + $container, + $containerHash, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups, @@ -187,9 +193,9 @@ public function validateProperty($object, $propertyName, $groups = null) /** * {@inheritdoc} */ - public function validatePropertyValue($object, $propertyName, $value, $groups = null) + public function validatePropertyValue($container, $propertyName, $value, $groups = null) { - $classMetadata = $this->metadataFactory->getMetadataFor($object); + $classMetadata = $this->metadataFactory->getMetadataFor($container); if (!$classMetadata instanceof ClassMetadataInterface) { // Cannot be UnsupportedMetadataException because of BC with @@ -204,11 +210,14 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $containerHash = spl_object_hash($container); foreach ($propertyMetadatas as $propertyMetadata) { $this->traverseGenericNode( $value, - $object, + is_object($value) ? spl_object_hash($value) : null, + $container, + $containerHash, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups, @@ -266,12 +275,12 @@ protected function normalizeGroups($groups) * @see CollectionNode * @see TraversalStrategy */ - private function traverseClassNode($value, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) + private function traverseClassNode($value, $valueHash, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { // Replace "Default" group by group sequence, if appropriate $groups = $this->replaceDefaultGroup($value, $metadata, $groups); - $groups = $this->validateNode($value, $value, $metadata, $propertyPath, $groups, $traversalStrategy, $context); + $groups = $this->validateNode($value, $valueHash, null, null, $metadata, $propertyPath, $groups, $traversalStrategy, $context); if (0 === count($groups)) { return; @@ -288,9 +297,13 @@ private function traverseClassNode($value, ClassMetadataInterface $metadata = nu )); } + $propertyValue = $propertyMetadata->getPropertyValue($value); + $this->traverseGenericNode( - $propertyMetadata->getPropertyValue($value), + $propertyValue, + is_object($propertyValue) ? spl_object_hash($propertyValue) : null, $value, + $valueHash, $propertyMetadata, $propertyPath ? $propertyPath.'.'.$propertyName @@ -392,6 +405,7 @@ private function cascadeCollection($collection, $propertyPath, array $groups, $t if (is_object($value)) { $this->cascadeObject( $value, + spl_object_hash($value), $propertyPath.'['.$key.']', $groups, $traversalStrategy, @@ -418,9 +432,9 @@ private function cascadeCollection($collection, $propertyPath, array $groups, $t * @param Node $node The node * @param ExecutionContextInterface $context The current execution context */ - private function traverseGenericNode($value, $object, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) + private function traverseGenericNode($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { - $groups = $this->validateNode($value, $object, $metadata, $propertyPath, $groups, $traversalStrategy, $context); + $groups = $this->validateNode($value, $valueHash, $container, $containerHash, $metadata, $propertyPath, $groups, $traversalStrategy, $context); if (0 === count($groups)) { return; @@ -467,6 +481,7 @@ private function traverseGenericNode($value, $object, MetadataInterface $metadat // (BC with Symfony < 2.5) $this->cascadeObject( $value, + $valueHash, $propertyPath, $cascadedGroups, $traversalStrategy, @@ -492,7 +507,7 @@ private function traverseGenericNode($value, $object, MetadataInterface $metadat * 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 object $container The object to cascade * @param string $propertyPath The current property path * @param string[] $groups The validated groups * @param integer $traversalStrategy The strategy for traversing the @@ -507,10 +522,10 @@ private function traverseGenericNode($value, $object, MetadataInterface $metadat * metadata factory does not implement * {@link ClassMetadataInterface} */ - private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) + private function cascadeObject($container, $containerHash, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { try { - $classMetadata = $this->metadataFactory->getMetadataFor($object); + $classMetadata = $this->metadataFactory->getMetadataFor($container); if (!$classMetadata instanceof ClassMetadataInterface) { throw new UnsupportedMetadataException(sprintf( @@ -522,7 +537,8 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal } $this->traverseClassNode( - $object, + $container, + $containerHash, $classMetadata, $propertyPath, $groups, @@ -532,7 +548,7 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal ); } catch (NoSuchMetadataException $e) { // Rethrow if not Traversable - if (!$object instanceof \Traversable) { + if (!$container instanceof \Traversable) { throw $e; } @@ -542,7 +558,7 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal } $this->cascadeCollection( - $object, + $container, $propertyPath, $groups, $traversalStrategy, @@ -563,14 +579,12 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal * * @return array The groups in which the successor nodes should be validated */ - public function validateNode($value, $object, MetadataInterface $metadata = null, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) + public function validateNode($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { $context->setValue($value); $context->setMetadata($metadata); $context->setPropertyPath($propertyPath); - $objectHash = is_object($object) ? spl_object_hash($object) : null; - // if group (=[,G3,G4]) contains group sequence (=) // then call traverse() with each entry of the group sequence and abort // if necessary (G1, G2) @@ -587,7 +601,7 @@ public function validateNode($value, $object, MetadataInterface $metadata = null // Use the object hash for group sequences $groupHash = is_object($group) ? spl_object_hash($group) : $group; - if ($context->isObjectValidatedForGroup($objectHash, $groupHash)) { + if ($context->isObjectValidatedForGroup($valueHash, $groupHash)) { // Skip this group when validating the successor nodes // (property and/or collection nodes) unset($groups[$key]); @@ -595,12 +609,12 @@ public function validateNode($value, $object, MetadataInterface $metadata = null continue; } - $context->markObjectAsValidatedForGroup($objectHash, $groupHash); + $context->markObjectAsValidatedForGroup($valueHash, $groupHash); } if ($group instanceof GroupSequence) { // Traverse group sequence until a violation is generated - $this->stepThroughGroupSequence($value, $object, $metadata, $propertyPath, $traversalStrategy, $group, $context); + $this->stepThroughGroupSequence($value, $valueHash, $container, $containerHash, $metadata, $propertyPath, $traversalStrategy, $group, $context); // Skip the group sequence when validating successor nodes unset($groups[$key]); @@ -609,7 +623,7 @@ public function validateNode($value, $object, MetadataInterface $metadata = null } // Validate normal group - $this->validateNodeForGroup($value, $objectHash, $metadata, $group, $context); + $this->validateNodeForGroup($value, $valueHash, $containerHash, $metadata, $group, $context); } return $groups; @@ -625,7 +639,7 @@ public function validateNode($value, $object, MetadataInterface $metadata = null * @param GroupSequence $groupSequence The group sequence * @param ExecutionContextInterface $context The execution context */ - private function stepThroughGroupSequence($value, $object, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, ExecutionContextInterface $context) + private function stepThroughGroupSequence($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); @@ -640,6 +654,7 @@ private function stepThroughGroupSequence($value, $object, MetadataInterface $me if ($metadata instanceof ClassMetadataInterface) { $this->traverseClassNode( $value, + $valueHash, $metadata, $propertyPath, $groups, @@ -650,7 +665,9 @@ private function stepThroughGroupSequence($value, $object, MetadataInterface $me } else { $this->traverseGenericNode( $value, - $object, + $valueHash, + $container, + $containerHash, $metadata, $propertyPath, $groups, @@ -673,36 +690,38 @@ private function stepThroughGroupSequence($value, $object, MetadataInterface $me * @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 + * @param string $containerHash The hash of the node's * object (if any) * * @throws \Exception */ - private function validateNodeForGroup($value, $objectHash, MetadataInterface $metadata = null, $group, ExecutionContextInterface $context) + private function validateNodeForGroup($value, $valueHash, $containerHash, MetadataInterface $metadata = null, $group, ExecutionContextInterface $context) { $context->setGroup($group); + $propertyName = $metadata instanceof PropertyMetadataInterface + ? $metadata->getPropertyName() + : null; + foreach ($metadata->findConstraints($group) as $constraint) { // Prevent duplicate validation of constraints, in the case // that constraints belong to multiple validated groups - if (null !== $objectHash) { + if (null !== $propertyName) { $constraintHash = spl_object_hash($constraint); - if ($metadata instanceof ClassMetadataInterface) { - if ($context->isClassConstraintValidated($objectHash, $constraintHash)) { - continue; - } - - $context->markClassConstraintAsValidated($objectHash, $constraintHash); - } elseif ($metadata instanceof PropertyMetadataInterface) { - $propertyName = $metadata->getPropertyName(); + if ($context->isPropertyConstraintValidated($containerHash, $propertyName, $constraintHash)) { + continue; + } - if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { - continue; - } + $context->markPropertyConstraintAsValidated($containerHash, $propertyName, $constraintHash); + } elseif (null !== $valueHash) { + $constraintHash = spl_object_hash($constraint); - $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); + if ($context->isClassConstraintValidated($valueHash, $constraintHash)) { + continue; } + + $context->markClassConstraintAsValidated($valueHash, $constraintHash); } $validator = $this->validatorFactory->getInstance($constraint); From 2f23d9725b3ccfbd990725c832319c50894ac52c Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Mar 2014 17:55:15 +0100 Subject: [PATCH 72/86] [Validator] Reduced number of method calls on the execution context --- .../Validator/Context/ExecutionContext.php | 16 +----- .../Context/ExecutionContextInterface.php | 26 ++------- .../NodeVisitor/NodeValidationVisitor.php | 53 ++++++++----------- .../RecursiveContextualValidator.php | 4 +- 4 files changed, 27 insertions(+), 72 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 683ddd8c97329..718de5eb2585a 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -139,24 +139,10 @@ public function __construct(ValidatorInterface $validator, $root, TranslatorInte /** * {@inheritdoc} */ - public function setValue($value) + public function setNode($value, MetadataInterface $metadata = null, $propertyPath) { $this->value = $value; - } - - /** - * {@inheritdoc} - */ - public function setMetadata(MetadataInterface $metadata = null) - { $this->metadata = $metadata; - } - - /** - * {@inheritdoc} - */ - public function setPropertyPath($propertyPath) - { $this->propertyPath = (string) $propertyPath; } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index b3224d5dd7a98..beafe75433c5c 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -102,32 +102,14 @@ public function getValidator(); /** * Sets the currently validated value. * - * @param mixed $value The validated value + * @param mixed $value The validated value + * @param MetadataInterface $metadata The validation metadata + * @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 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); + public function setNode($value, MetadataInterface $metadata = null, $propertyPath); /** * Sets the currently validated group. diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index 3a5a0f2bf8a33..d6e818c44a664 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -69,9 +69,7 @@ public function visit(Node $node, ExecutionContextInterface $context) return true; } - $context->setValue($node->value); - $context->setMetadata($node->metadata); - $context->setPropertyPath($node->propertyPath); + $context->setNode($node->value, $node->metadata, $node->propertyPath); if ($node instanceof ClassNode) { $this->replaceDefaultGroup($node); @@ -171,43 +169,34 @@ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, */ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterface $context, $objectHash) { - try { - $context->setGroup($group); + $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); + 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 ($node instanceof ClassNode) { + if ($context->isClassConstraintValidated($objectHash, $constraintHash)) { + continue; + } - if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { - continue; - } + $context->markClassConstraintAsValidated($objectHash, $constraintHash); + } elseif ($node instanceof PropertyNode) { + $propertyName = $node->metadata->getPropertyName(); - $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); + if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { + continue; } - } - $validator = $this->validatorFactory->getInstance($constraint); - $validator->initialize($context); - $validator->validate($node->value, $constraint); + $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); + } } - $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; + $validator = $this->validatorFactory->getInstance($constraint); + $validator->initialize($context); + $validator->validate($node->value, $constraint); } } diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index b1b4b714cd281..563fe74288be5 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -581,9 +581,7 @@ private function cascadeObject($container, $containerHash, $propertyPath, array */ public function validateNode($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { - $context->setValue($value); - $context->setMetadata($metadata); - $context->setPropertyPath($propertyPath); + $context->setNode($value, $metadata, $propertyPath); // if group (=[,G3,G4]) contains group sequence (=) // then call traverse() with each entry of the group sequence and abort From 029a71638e4ab3461e4636f7f1991632d496939a Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Mar 2014 18:28:47 +0100 Subject: [PATCH 73/86] [Validator] Moved logic of replaceDefaultGroup() to validateNode() --- .../NodeVisitor/NodeValidationVisitor.php | 79 +++++------ .../RecursiveContextualValidator.php | 123 ++++++++---------- 2 files changed, 83 insertions(+), 119 deletions(-) diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index d6e818c44a664..d3d7937ad93bd 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -72,8 +72,6 @@ public function visit(Node $node, ExecutionContextInterface $context) $context->setNode($node->value, $node->metadata, $node->propertyPath); if ($node instanceof ClassNode) { - $this->replaceDefaultGroup($node); - $objectHash = spl_object_hash($node->value); } elseif ($node instanceof PropertyNode) { $objectHash = spl_object_hash($node->object); @@ -88,6 +86,8 @@ public function visit(Node $node, ExecutionContextInterface $context) // simply continue traversal (if possible) foreach ($node->groups as $key => $group) { + $cascadedGroup = null; + // Even if we remove the following clause, the constraints on an // object won't be validated again due to the measures taken in // validateNodeForGroup(). @@ -106,11 +106,36 @@ public function visit(Node $node, ExecutionContextInterface $context) } $context->markObjectAsValidatedForGroup($objectHash, $groupHash); + + // Replace the "Default" group by the group sequence defined + // for the class, if applicable + // This is done after checking the cache, so that + // spl_object_hash() isn't called for this sequence and + // "Default" is used instead in the cache. This is useful + // if the getters below return different group sequences in + // every call. + if (Constraint::DEFAULT_GROUP === $group) { + if ($node->metadata->hasGroupSequence()) { + // The group sequence is statically defined for the class + $group = $node->metadata->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + } elseif ($node->metadata->isGroupSequenceProvider()) { + // The group sequence is dynamically obtained from the validated + // object + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $group = $node->value->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + + if (!$group instanceof GroupSequence) { + $group = new GroupSequence($group); + } + } + } } if ($group instanceof GroupSequence) { // Traverse group sequence until a violation is generated - $this->traverseGroupSequence($node, $group, $context); + $this->traverseGroupSequence($node, $group, $cascadedGroup, $context); // Skip the group sequence when validating successor nodes unset($node->groups[$key]); @@ -135,17 +160,15 @@ public function visit(Node $node, ExecutionContextInterface $context) * @param GroupSequence $groupSequence The group sequence * @param ExecutionContextInterface $context The execution context */ - private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, ExecutionContextInterface $context) + private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); + $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null; foreach ($groupSequence->groups as $groupInSequence) { $node = clone $node; $node->groups = array($groupInSequence); - - if (null !== $groupSequence->cascadedGroup) { - $node->cascadedGroups = array($groupSequence->cascadedGroup); - } + $node->cascadedGroups = $cascadedGroups; $this->nodeTraverser->traverse(array($node), $context); @@ -199,44 +222,4 @@ private function validateNodeForGroup(Node $node, $group, ExecutionContextInterf $validator->validate($node->value, $constraint); } } - - /** - * Checks class nodes whether their "Default" group is replaced by a group - * sequence and adjusts the validation groups accordingly. - * - * If the "Default" group is replaced for a class node, and if the validated - * groups of the node contain the group "Default", that group is replaced by - * the group sequence specified in the class' metadata. - * - * @param ClassNode $node The node - */ - private function replaceDefaultGroup(ClassNode $node) - { - 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); - } - } else { - // The "Default" group is not overridden. Quit. - return; - } - - $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; - } - } } diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index 563fe74288be5..bb5a0fff5e0c7 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -277,9 +277,6 @@ protected function normalizeGroups($groups) */ private function traverseClassNode($value, $valueHash, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { - // Replace "Default" group by group sequence, if appropriate - $groups = $this->replaceDefaultGroup($value, $metadata, $groups); - $groups = $this->validateNode($value, $valueHash, null, null, $metadata, $propertyPath, $groups, $traversalStrategy, $context); if (0 === count($groups)) { @@ -444,14 +441,13 @@ private function traverseGenericNode($value, $valueHash, $container, $containerH return; } - // The "cascadedGroups" property is set by the NodeValidationVisitor when - // traversing group sequences - $cascadedGroups = count($cascadedGroups) > 0 - ? $cascadedGroups - : $groups; - $cascadingStrategy = $metadata->getCascadingStrategy(); + // Quit unless we have an array or a cascaded object + if (!is_array($value) && !($cascadingStrategy & CascadingStrategy::CASCADE)) { + return; + } + // If no specific traversal strategy was requested when this method // was called, use the traversal strategy of the node's metadata if ($traversalStrategy & TraversalStrategy::IMPLICIT) { @@ -460,6 +456,12 @@ private function traverseGenericNode($value, $valueHash, $container, $containerH | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); } + // The "cascadedGroups" property is set by the NodeValidationVisitor when + // traversing group sequences + $cascadedGroups = count($cascadedGroups) > 0 + ? $cascadedGroups + : $groups; + if (is_array($value)) { // Arrays are always traversed, independent of the specified // traversal strategy @@ -475,21 +477,17 @@ private function traverseGenericNode($value, $valueHash, $container, $containerH 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( - $value, - $valueHash, - $propertyPath, - $cascadedGroups, - $traversalStrategy, - $context - ); - - return; - } + // If the value is a scalar, pass it anyway, because we want + // a NoSuchMetadataException to be thrown in that case + // (BC with Symfony < 2.5) + $this->cascadeObject( + $value, + $valueHash, + $propertyPath, + $cascadedGroups, + $traversalStrategy, + $context + ); // Currently, the traversal strategy can only be TRAVERSE for a // generic node if the cascading strategy is CASCADE. Thus, traversable @@ -590,6 +588,8 @@ public function validateNode($value, $valueHash, $container, $containerHash, Met // simply continue traversal (if possible) foreach ($groups as $key => $group) { + $cascadedGroup = null; + // Even if we remove the following clause, the constraints on an // object won't be validated again due to the measures taken in // validateNodeForGroup(). @@ -608,11 +608,36 @@ public function validateNode($value, $valueHash, $container, $containerHash, Met } $context->markObjectAsValidatedForGroup($valueHash, $groupHash); + + // Replace the "Default" group by the group sequence defined + // for the class, if applicable + // This is done after checking the cache, so that + // spl_object_hash() isn't called for this sequence and + // "Default" is used instead in the cache. This is useful + // if the getters below return different group sequences in + // every call. + if (Constraint::DEFAULT_GROUP === $group) { + if ($metadata->hasGroupSequence()) { + // The group sequence is statically defined for the class + $group = $metadata->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + } elseif ($metadata->isGroupSequenceProvider()) { + // The group sequence is dynamically obtained from the validated + // object + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $group = $value->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + + if (!$group instanceof GroupSequence) { + $group = new GroupSequence($group); + } + } + } } if ($group instanceof GroupSequence) { // Traverse group sequence until a violation is generated - $this->stepThroughGroupSequence($value, $valueHash, $container, $containerHash, $metadata, $propertyPath, $traversalStrategy, $group, $context); + $this->stepThroughGroupSequence($value, $valueHash, $container, $containerHash, $metadata, $propertyPath, $traversalStrategy, $group, $cascadedGroup, $context); // Skip the group sequence when validating successor nodes unset($groups[$key]); @@ -637,17 +662,13 @@ public function validateNode($value, $valueHash, $container, $containerHash, Met * @param GroupSequence $groupSequence The group sequence * @param ExecutionContextInterface $context The execution context */ - private function stepThroughGroupSequence($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, ExecutionContextInterface $context) + private function stepThroughGroupSequence($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); + $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null; foreach ($groupSequence->groups as $groupInSequence) { $groups = array($groupInSequence); - $cascadedGroups = null; - - if (null !== $groupSequence->cascadedGroup) { - $cascadedGroups = array($groupSequence->cascadedGroup); - } if ($metadata instanceof ClassMetadataInterface) { $this->traverseClassNode( @@ -727,44 +748,4 @@ private function validateNodeForGroup($value, $valueHash, $containerHash, Metada $validator->validate($value, $constraint); } } - - /** - * @param $value - * @param ClassMetadataInterface $metadata - * @param array $groups - * - * @return array - */ - private function replaceDefaultGroup($value, ClassMetadataInterface $metadata, array $groups) - { - $groupSequence = null; - - if ($metadata->hasGroupSequence()) { - // The group sequence is statically defined for the class - $groupSequence = $metadata->getGroupSequence(); - } elseif ($metadata->isGroupSequenceProvider()) { - // The group sequence is dynamically obtained from the validated - // object - /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $groupSequence = $value->getGroupSequence(); - - if (!$groupSequence instanceof GroupSequence) { - $groupSequence = new GroupSequence($groupSequence); - } - } - - if (null !== $groupSequence) { - $key = array_search(Constraint::DEFAULT_GROUP, $groups); - - if (false !== $key) { - // Replace the "Default" group by the group sequence - $groups[$key] = $groupSequence; - - // Cascade the "Default" group when validating the sequence - $groupSequence->cascadedGroup = Constraint::DEFAULT_GROUP; - } - } - - return $groups; - } } From 3183aed7cdd669ee6ad6fc4715e820dad1ae46c6 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 17 Mar 2014 20:33:59 +0100 Subject: [PATCH 74/86] [Validator] Improved performance of cache key generation --- .../Validator/Context/ExecutionContext.php | 50 +-- .../Context/ExecutionContextInterface.php | 43 +-- .../Component/Validator/Node/ClassNode.php | 8 +- .../Validator/Node/CollectionNode.php | 1 + src/Symfony/Component/Validator/Node/Node.php | 5 +- .../Component/Validator/Node/PropertyNode.php | 14 +- .../NonRecursiveNodeTraverser.php | 3 +- .../NodeVisitor/NodeValidationVisitor.php | 36 +- .../Validator/Tests/Node/ClassNodeTest.php | 2 +- .../NonRecursiveNodeTraverserTest.php | 2 +- .../RecursiveContextualValidator.php | 332 ++++++++---------- .../TraversingContextualValidator.php | 8 +- 12 files changed, 198 insertions(+), 306 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 718de5eb2585a..75b7e2c3bf6bc 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -104,7 +104,7 @@ class ExecutionContext implements ExecutionContextInterface * * @var array */ - private $validatedClassConstraints = array(); + private $validatedConstraints = array(); /** * Stores which property constraint has been validated for which property. @@ -319,64 +319,36 @@ public function getMetadataFactory() /** * {@inheritdoc} */ - public function markObjectAsValidatedForGroup($objectHash, $groupHash) + public function markGroupAsValidated($cacheKey, $groupHash) { - if (!isset($this->validatedObjects[$objectHash])) { - $this->validatedObjects[$objectHash] = array(); + if (!isset($this->validatedObjects[$cacheKey])) { + $this->validatedObjects[$cacheKey] = array(); } - $this->validatedObjects[$objectHash][$groupHash] = true; + $this->validatedObjects[$cacheKey][$groupHash] = true; } /** * {@inheritdoc} */ - public function isObjectValidatedForGroup($objectHash, $groupHash) + public function isGroupValidated($cacheKey, $groupHash) { - return isset($this->validatedObjects[$objectHash][$groupHash]); + return isset($this->validatedObjects[$cacheKey][$groupHash]); } /** * {@inheritdoc} */ - public function markClassConstraintAsValidated($objectHash, $constraintHash) + public function markConstraintAsValidated($cacheKey, $constraintHash) { - if (!isset($this->validatedClassConstraints[$objectHash])) { - $this->validatedClassConstraints[$objectHash] = array(); - } - - $this->validatedClassConstraints[$objectHash][$constraintHash] = true; - } - - /** - * {@inheritdoc} - */ - public function isClassConstraintValidated($objectHash, $constraintHash) - { - return isset($this->validatedClassConstraints[$objectHash][$constraintHash]); - } - - /** - * {@inheritdoc} - */ - public function markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash) - { - if (!isset($this->validatedPropertyConstraints[$objectHash])) { - $this->validatedPropertyConstraints[$objectHash] = array(); - } - - if (!isset($this->validatedPropertyConstraints[$objectHash][$propertyName])) { - $this->validatedPropertyConstraints[$objectHash][$propertyName] = array(); - } - - $this->validatedPropertyConstraints[$objectHash][$propertyName][$constraintHash] = true; + $this->validatedConstraints[$cacheKey.':'.$constraintHash] = true; } /** * {@inheritdoc} */ - public function isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash) + public function isConstraintValidated($cacheKey, $constraintHash) { - return isset($this->validatedPropertyConstraints[$objectHash][$propertyName][$constraintHash]); + return isset($this->validatedConstraints[$cacheKey.':'.$constraintHash]); } } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index beafe75433c5c..2e778fbec815f 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -124,19 +124,19 @@ public function setGroup($group); /** * Marks an object as validated in a specific validation group. * - * @param string $objectHash The hash of the object + * @param string $cacheKey The hash of the object * @param string $groupHash The group's name or hash, if it is group * sequence * * @internal Used by the validator engine. Should not be called by user * code. */ - public function markObjectAsValidatedForGroup($objectHash, $groupHash); + public function markGroupAsValidated($cacheKey, $groupHash); /** * Returns whether an object was validated in a specific validation group. * - * @param string $objectHash The hash of the object + * @param string $cacheKey The hash of the object * @param string $groupHash The group's name or hash, if it is group * sequence * @@ -146,23 +146,23 @@ public function markObjectAsValidatedForGroup($objectHash, $groupHash); * @internal Used by the validator engine. Should not be called by user * code. */ - public function isObjectValidatedForGroup($objectHash, $groupHash); + public function isGroupValidated($cacheKey, $groupHash); /** * Marks a constraint as validated for an object. * - * @param string $objectHash The hash of the object + * @param string $cacheKey The hash of the object * @param string $constraintHash The hash of the constraint * * @internal Used by the validator engine. Should not be called by user * code. */ - public function markClassConstraintAsValidated($objectHash, $constraintHash); + public function markConstraintAsValidated($cacheKey, $constraintHash); /** * Returns whether a constraint was validated for an object. * - * @param string $objectHash The hash of the object + * @param string $cacheKey The hash of the object * @param string $constraintHash The hash of the constraint * * @return Boolean Whether the constraint was already validated @@ -170,32 +170,5 @@ public function markClassConstraintAsValidated($objectHash, $constraintHash); * @internal Used by the validator engine. Should not be called by user * code. */ - public function isClassConstraintValidated($objectHash, $constraintHash); - - /** - * Marks a constraint as validated for an object and a property name. - * - * @param string $objectHash The hash of the object - * @param string $propertyName The property name - * @param string $constraintHash The hash of the constraint - * - * @internal Used by the validator engine. Should not be called by user - * code. - */ - public function markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); - - /** - * Returns whether a constraint was validated for an object and a property - * name. - * - * @param string $objectHash The hash of the object - * @param string $propertyName The property name - * @param string $constraintHash The hash of the constraint - * - * @return Boolean Whether the constraint was already validated - * - * @internal Used by the validator engine. Should not be called by user - * code. - */ - public function isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash); + public function isConstraintValidated($cacheKey, $constraintHash); } diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php index 54e22e2d97403..f52a68366be0f 100644 --- a/src/Symfony/Component/Validator/Node/ClassNode.php +++ b/src/Symfony/Component/Validator/Node/ClassNode.php @@ -55,7 +55,7 @@ class ClassNode extends Node * * @see \Symfony\Component\Validator\Mapping\TraversalStrategy */ - public function __construct($object, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) + public function __construct($object, $cacheKey, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { if (!is_object($object)) { throw new UnexpectedTypeException($object, 'object'); @@ -63,12 +63,12 @@ public function __construct($object, ClassMetadataInterface $metadata, $property parent::__construct( $object, + $cacheKey, $metadata, $propertyPath, $groups, - $cascadedGroups + $cascadedGroups, + $traversalStrategy ); - - $this->traversalStrategy = $traversalStrategy; } } diff --git a/src/Symfony/Component/Validator/Node/CollectionNode.php b/src/Symfony/Component/Validator/Node/CollectionNode.php index ddca97a5c7f09..79a49ff6e31f6 100644 --- a/src/Symfony/Component/Validator/Node/CollectionNode.php +++ b/src/Symfony/Component/Validator/Node/CollectionNode.php @@ -56,6 +56,7 @@ public function __construct($collection, $propertyPath, array $groups, $cascaded parent::__construct( $collection, null, + null, $propertyPath, $groups, $cascadedGroups, diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php index 56c8145c45682..93099844beed0 100644 --- a/src/Symfony/Component/Validator/Node/Node.php +++ b/src/Symfony/Component/Validator/Node/Node.php @@ -30,6 +30,8 @@ abstract class Node */ public $value; + public $cacheKey; + /** * The metadata specifying how the value should be validated. * @@ -82,13 +84,14 @@ abstract class Node * * @throws UnexpectedTypeException If $cascadedGroups is invalid */ - public function __construct($value, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) + public function __construct($value, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { if (null !== $cascadedGroups && !is_array($cascadedGroups)) { throw new UnexpectedTypeException($cascadedGroups, 'null or array'); } $this->value = $value; + $this->cacheKey = $cacheKey; $this->metadata = $metadata; $this->propertyPath = $propertyPath; $this->groups = $groups; diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php index 8934bf1d7330c..4ee7ac5918906 100644 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ b/src/Symfony/Component/Validator/Node/PropertyNode.php @@ -41,11 +41,6 @@ */ class PropertyNode extends Node { - /** - * @var object - */ - public $object; - /** * @var PropertyMetadataInterface */ @@ -71,22 +66,17 @@ class PropertyNode extends Node * * @see \Symfony\Component\Validator\Mapping\TraversalStrategy */ - public function __construct($object, $value, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) + public function __construct($value, $cacheKey, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) { - if (!is_object($object)) { - throw new UnexpectedTypeException($object, 'object'); - } - parent::__construct( $value, + $cacheKey, $metadata, $propertyPath, $groups, $cascadedGroups, $traversalStrategy ); - - $this->object = $object; } } diff --git a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php index 5c904f01d47f7..c29bac71e2387 100644 --- a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php +++ b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php @@ -274,8 +274,8 @@ private function traverseClassNode(ClassNode $node, ExecutionContextInterface $c } $nodeStack->push(new PropertyNode( - $node->value, $propertyMetadata->getPropertyValue($node->value), + $node->cacheKey.':'.$propertyName, $propertyMetadata, $node->propertyPath ? $node->propertyPath.'.'.$propertyName @@ -530,6 +530,7 @@ private function cascadeObject($object, $propertyPath, array $groups, $traversal $nodeStack->push(new ClassNode( $object, + spl_object_hash($object), $classMetadata, $propertyPath, $groups, diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php index d3d7937ad93bd..5eee760c8a036 100644 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php @@ -71,14 +71,6 @@ public function visit(Node $node, ExecutionContextInterface $context) $context->setNode($node->value, $node->metadata, $node->propertyPath); - if ($node instanceof ClassNode) { - $objectHash = spl_object_hash($node->value); - } elseif ($node instanceof PropertyNode) { - $objectHash = spl_object_hash($node->object); - } else { - $objectHash = null; - } - // if group (=[,G3,G4]) contains group sequence (=) // then call traverse() with each entry of the group sequence and abort // if necessary (G1, G2) @@ -97,7 +89,7 @@ public function visit(Node $node, ExecutionContextInterface $context) // Use the object hash for group sequences $groupHash = is_object($group) ? spl_object_hash($group) : $group; - if ($context->isObjectValidatedForGroup($objectHash, $groupHash)) { + if ($context->isGroupValidated($node->cacheKey, $groupHash)) { // Skip this group when validating the successor nodes // (property and/or collection nodes) unset($node->groups[$key]); @@ -105,7 +97,7 @@ public function visit(Node $node, ExecutionContextInterface $context) continue; } - $context->markObjectAsValidatedForGroup($objectHash, $groupHash); + $context->markGroupAsValidated($node->cacheKey, $groupHash); // Replace the "Default" group by the group sequence defined // for the class, if applicable @@ -144,7 +136,7 @@ public function visit(Node $node, ExecutionContextInterface $context) } // Validate normal group - $this->validateNodeForGroup($node, $group, $context, $objectHash); + $this->validateInGroup($node, $group, $context); } return true; @@ -190,31 +182,21 @@ private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, * * @throws \Exception */ - private function validateNodeForGroup(Node $node, $group, ExecutionContextInterface $context, $objectHash) + private function validateInGroup(Node $node, $group, ExecutionContextInterface $context) { $context->setGroup($group); foreach ($node->metadata->findConstraints($group) as $constraint) { // Prevent duplicate validation of constraints, in the case // that constraints belong to multiple validated groups - if (null !== $objectHash) { + if (null !== $node->cacheKey) { $constraintHash = spl_object_hash($constraint); - if ($node instanceof ClassNode) { - if ($context->isClassConstraintValidated($objectHash, $constraintHash)) { - continue; - } - - $context->markClassConstraintAsValidated($objectHash, $constraintHash); - } elseif ($node instanceof PropertyNode) { - $propertyName = $node->metadata->getPropertyName(); - - if ($context->isPropertyConstraintValidated($objectHash, $propertyName, $constraintHash)) { - continue; - } - - $context->markPropertyConstraintAsValidated($objectHash, $propertyName, $constraintHash); + if ($context->isConstraintValidated($node->cacheKey, $constraintHash)) { + continue; } + + $context->markConstraintAsValidated($node->cacheKey, $constraintHash); } $validator = $this->validatorFactory->getInstance($constraint); diff --git a/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php b/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php index 1241d1bb5b830..c79f4c838f60c 100644 --- a/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php +++ b/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php @@ -26,6 +26,6 @@ public function testConstructorExpectsObject() { $metadata = $this->getMock('Symfony\Component\Validator\Mapping\ClassMetadataInterface'); - new ClassNode('foobar', $metadata, '', array(), array()); + new ClassNode('foobar', null, $metadata, '', array(), array()); } } diff --git a/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php b/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php index 4dfc7071248e9..09e26bcaf9e11 100644 --- a/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php +++ b/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php @@ -40,7 +40,7 @@ protected function setUp() public function testVisitorsMayPreventTraversal() { - $nodes = array(new GenericNode('value', new GenericMetadata(), '', array('Default'))); + $nodes = array(new GenericNode('value', null, new GenericMetadata(), '', array('Default'))); $context = $this->getMock('Symfony\Component\Validator\Context\ExecutionContextInterface'); $visitor1 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface'); diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index bb5a0fff5e0c7..0e083c05cf8fe 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -100,11 +100,9 @@ public function validate($value, $constraints = null, $groups = null) $metadata = new GenericMetadata(); $metadata->addConstraints($constraints); - $this->traverseGenericNode( + $this->validateGenericNode( $value, is_object($value) ? spl_object_hash($value) : null, - null, - null, $metadata, $this->defaultPropertyPath, $groups, @@ -119,7 +117,6 @@ public function validate($value, $constraints = null, $groups = null) if (is_object($value)) { $this->cascadeObject( $value, - spl_object_hash($value), $this->defaultPropertyPath, $groups, TraversalStrategy::IMPLICIT, @@ -168,16 +165,14 @@ public function validateProperty($container, $propertyName, $groups = null) $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $containerHash = spl_object_hash($container); + $cacheKey = spl_object_hash($container); foreach ($propertyMetadatas as $propertyMetadata) { $propertyValue = $propertyMetadata->getPropertyValue($container); - $this->traverseGenericNode( + $this->validateGenericNode( $propertyValue, - is_object($propertyValue) ? spl_object_hash($propertyValue) : null, - $container, - $containerHash, + $cacheKey.':'.$propertyName, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups, @@ -210,14 +205,12 @@ public function validatePropertyValue($container, $propertyName, $value, $groups $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $containerHash = spl_object_hash($container); + $cacheKey = spl_object_hash($container); foreach ($propertyMetadatas as $propertyMetadata) { - $this->traverseGenericNode( + $this->validateGenericNode( $value, - is_object($value) ? spl_object_hash($value) : null, - $container, - $containerHash, + $cacheKey.':'.$propertyName, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups, @@ -275,9 +268,74 @@ protected function normalizeGroups($groups) * @see CollectionNode * @see TraversalStrategy */ - private function traverseClassNode($value, $valueHash, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) + private function validateClassNode($value, $cacheKey, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { - $groups = $this->validateNode($value, $valueHash, null, null, $metadata, $propertyPath, $groups, $traversalStrategy, $context); + $context->setNode($value, $metadata, $propertyPath); + + // if group (=[,G3,G4]) contains group sequence (=) + // then call traverse() with each entry of the group sequence and abort + // if necessary (G1, G2) + // finally call traverse() with remaining entries ([G3,G4]) or + // simply continue traversal (if possible) + + foreach ($groups as $key => $group) { + $cascadedGroup = null; + + // Even if we remove the following clause, the constraints on an + // object won't be validated again due to the measures taken in + // validateNodeForGroup(). + // The following shortcut, however, prevents validatedNodeForGroup() + // from being called at all and enhances performance a bit. + + // Use the object hash for group sequences + $groupHash = is_object($group) ? spl_object_hash($group) : $group; + + if ($context->isGroupValidated($cacheKey, $groupHash)) { + // Skip this group when validating the successor nodes + // (property and/or collection nodes) + unset($groups[$key]); + + continue; + } + + $context->markGroupAsValidated($cacheKey, $groupHash); + + // Replace the "Default" group by the group sequence defined + // for the class, if applicable + // This is done after checking the cache, so that + // spl_object_hash() isn't called for this sequence and + // "Default" is used instead in the cache. This is useful + // if the getters below return different group sequences in + // every call. + if (Constraint::DEFAULT_GROUP === $group) { + if ($metadata->hasGroupSequence()) { + // The group sequence is statically defined for the class + $group = $metadata->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + } elseif ($metadata->isGroupSequenceProvider()) { + // The group sequence is dynamically obtained from the validated + // object + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ + $group = $value->getGroupSequence(); + $cascadedGroup = Constraint::DEFAULT_GROUP; + + if (!$group instanceof GroupSequence) { + $group = new GroupSequence($group); + } + } + } + + if ($group instanceof GroupSequence) { + $this->stepThroughGroupSequence($value, $cacheKey, $metadata, $propertyPath, $traversalStrategy, $group, $cascadedGroup, $context); + + // Skip the group sequence when validating successor nodes + unset($groups[$key]); + + continue; + } + + $this->validateInGroup($value, $cacheKey, $metadata, $group, $context); + } if (0 === count($groups)) { return; @@ -296,11 +354,9 @@ private function traverseClassNode($value, $valueHash, ClassMetadataInterface $m $propertyValue = $propertyMetadata->getPropertyValue($value); - $this->traverseGenericNode( + $this->validateGenericNode( $propertyValue, - is_object($propertyValue) ? spl_object_hash($propertyValue) : null, - $value, - $valueHash, + $cacheKey.':'.$propertyName, $propertyMetadata, $propertyPath ? $propertyPath.'.'.$propertyName @@ -351,67 +407,6 @@ private function traverseClassNode($value, $valueHash, ClassMetadataInterface $m ); } - /** - * 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 cascadeCollection($collection, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) - { - if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) { - $traversalStrategy = TraversalStrategy::NONE; - } else { - $traversalStrategy = TraversalStrategy::IMPLICIT; - } - - foreach ($collection as $key => $value) { - if (is_array($value)) { - // Arrays are always cascaded, independent of the specified - // traversal strategy - // (BC with Symfony < 2.5) - $this->cascadeCollection( - $value, - $propertyPath.'['.$key.']', - $groups, - $traversalStrategy, - $context - ); - - continue; - } - - // Scalar and null values in the collection are ignored - // (BC with Symfony < 2.5) - if (is_object($value)) { - $this->cascadeObject( - $value, - spl_object_hash($value), - $propertyPath.'['.$key.']', - $groups, - $traversalStrategy, - $context - ); - } - } - } - /** * Traverses a node that is neither a class nor a collection node. * @@ -429,9 +424,22 @@ private function cascadeCollection($collection, $propertyPath, array $groups, $t * @param Node $node The node * @param ExecutionContextInterface $context The current execution context */ - private function traverseGenericNode($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) + private function validateGenericNode($value, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { - $groups = $this->validateNode($value, $valueHash, $container, $containerHash, $metadata, $propertyPath, $groups, $traversalStrategy, $context); + $context->setNode($value, $metadata, $propertyPath); + + foreach ($groups as $key => $group) { + if ($group instanceof GroupSequence) { + $this->stepThroughGroupSequence($value, $cacheKey, $metadata, $propertyPath, $traversalStrategy, $group, null, $context); + + // Skip the group sequence when validating successor nodes + unset($groups[$key]); + + continue; + } + + $this->validateInGroup($value, $cacheKey, $metadata, $group, $context); + } if (0 === count($groups)) { return; @@ -482,7 +490,6 @@ private function traverseGenericNode($value, $valueHash, $container, $containerH // (BC with Symfony < 2.5) $this->cascadeObject( $value, - $valueHash, $propertyPath, $cascadedGroups, $traversalStrategy, @@ -520,7 +527,7 @@ private function traverseGenericNode($value, $valueHash, $container, $containerH * metadata factory does not implement * {@link ClassMetadataInterface} */ - private function cascadeObject($container, $containerHash, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) + private function cascadeObject($container, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { try { $classMetadata = $this->metadataFactory->getMetadataFor($container); @@ -534,9 +541,9 @@ private function cascadeObject($container, $containerHash, $propertyPath, array )); } - $this->traverseClassNode( + $this->validateClassNode( $container, - $containerHash, + spl_object_hash($container), $classMetadata, $propertyPath, $groups, @@ -566,90 +573,63 @@ private function cascadeObject($container, $containerHash, $propertyPath, array } /** - * Validates a node's value against the constraints defined in the node's - * metadata. + * Traverses a collection node. * - * Objects and constraints that were validated before in the same context - * will be skipped. + * 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: * - * @param Node $node The current node - * @param ExecutionContextInterface $context The execution context + * - 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 * - * @return array The groups in which the successor nodes should be validated + * @see ClassNode + * @see CollectionNode */ - public function validateNode($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) + private function cascadeCollection($collection, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) { - $context->setNode($value, $metadata, $propertyPath); - - // if group (=[,G3,G4]) contains group sequence (=) - // then call traverse() with each entry of the group sequence and abort - // if necessary (G1, G2) - // finally call traverse() with remaining entries ([G3,G4]) or - // simply continue traversal (if possible) - - foreach ($groups as $key => $group) { - $cascadedGroup = null; - - // Even if we remove the following clause, the constraints on an - // object won't be validated again due to the measures taken in - // validateNodeForGroup(). - // The following shortcut, however, prevents validatedNodeForGroup() - // from being called at all and enhances performance a bit. - if ($metadata instanceof ClassMetadataInterface) { - // Use the object hash for group sequences - $groupHash = is_object($group) ? spl_object_hash($group) : $group; - - if ($context->isObjectValidatedForGroup($valueHash, $groupHash)) { - // Skip this group when validating the successor nodes - // (property and/or collection nodes) - unset($groups[$key]); - - continue; - } - - $context->markObjectAsValidatedForGroup($valueHash, $groupHash); - - // Replace the "Default" group by the group sequence defined - // for the class, if applicable - // This is done after checking the cache, so that - // spl_object_hash() isn't called for this sequence and - // "Default" is used instead in the cache. This is useful - // if the getters below return different group sequences in - // every call. - if (Constraint::DEFAULT_GROUP === $group) { - if ($metadata->hasGroupSequence()) { - // The group sequence is statically defined for the class - $group = $metadata->getGroupSequence(); - $cascadedGroup = Constraint::DEFAULT_GROUP; - } elseif ($metadata->isGroupSequenceProvider()) { - // The group sequence is dynamically obtained from the validated - // object - /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $group = $value->getGroupSequence(); - $cascadedGroup = Constraint::DEFAULT_GROUP; - - if (!$group instanceof GroupSequence) { - $group = new GroupSequence($group); - } - } - } - } - - if ($group instanceof GroupSequence) { - // Traverse group sequence until a violation is generated - $this->stepThroughGroupSequence($value, $valueHash, $container, $containerHash, $metadata, $propertyPath, $traversalStrategy, $group, $cascadedGroup, $context); + if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) { + $traversalStrategy = TraversalStrategy::NONE; + } else { + $traversalStrategy = TraversalStrategy::IMPLICIT; + } - // Skip the group sequence when validating successor nodes - unset($groups[$key]); + foreach ($collection as $key => $value) { + if (is_array($value)) { + // Arrays are always cascaded, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $this->cascadeCollection( + $value, + $propertyPath.'['.$key.']', + $groups, + $traversalStrategy, + $context + ); continue; } - // Validate normal group - $this->validateNodeForGroup($value, $valueHash, $containerHash, $metadata, $group, $context); + // Scalar and null values in the collection are ignored + // (BC with Symfony < 2.5) + if (is_object($value)) { + $this->cascadeObject( + $value, + $propertyPath.'['.$key.']', + $groups, + $traversalStrategy, + $context + ); + } } - - return $groups; } /** @@ -662,7 +642,7 @@ public function validateNode($value, $valueHash, $container, $containerHash, Met * @param GroupSequence $groupSequence The group sequence * @param ExecutionContextInterface $context The execution context */ - private function stepThroughGroupSequence($value, $valueHash, $container, $containerHash, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) + private function stepThroughGroupSequence($value, $cacheKey, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null; @@ -671,9 +651,9 @@ private function stepThroughGroupSequence($value, $valueHash, $container, $conta $groups = array($groupInSequence); if ($metadata instanceof ClassMetadataInterface) { - $this->traverseClassNode( + $this->validateClassNode( $value, - $valueHash, + $cacheKey, $metadata, $propertyPath, $groups, @@ -682,11 +662,9 @@ private function stepThroughGroupSequence($value, $valueHash, $container, $conta $context ); } else { - $this->traverseGenericNode( + $this->validateGenericNode( $value, - $valueHash, - $container, - $containerHash, + $cacheKey, $metadata, $propertyPath, $groups, @@ -714,33 +692,21 @@ private function stepThroughGroupSequence($value, $valueHash, $container, $conta * * @throws \Exception */ - private function validateNodeForGroup($value, $valueHash, $containerHash, MetadataInterface $metadata = null, $group, ExecutionContextInterface $context) + private function validateInGroup($value, $cacheKey, MetadataInterface $metadata, $group, ExecutionContextInterface $context) { $context->setGroup($group); - $propertyName = $metadata instanceof PropertyMetadataInterface - ? $metadata->getPropertyName() - : null; - foreach ($metadata->findConstraints($group) as $constraint) { // Prevent duplicate validation of constraints, in the case // that constraints belong to multiple validated groups - if (null !== $propertyName) { - $constraintHash = spl_object_hash($constraint); - - if ($context->isPropertyConstraintValidated($containerHash, $propertyName, $constraintHash)) { - continue; - } - - $context->markPropertyConstraintAsValidated($containerHash, $propertyName, $constraintHash); - } elseif (null !== $valueHash) { + if (null !== $cacheKey) { $constraintHash = spl_object_hash($constraint); - if ($context->isClassConstraintValidated($valueHash, $constraintHash)) { + if ($context->isConstraintValidated($cacheKey, $constraintHash)) { continue; } - $context->markClassConstraintAsValidated($valueHash, $constraintHash); + $context->markConstraintAsValidated($cacheKey, $constraintHash); } $validator = $this->validatorFactory->getInstance($constraint); diff --git a/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php index 288fd8a66c603..bd749eeb9757f 100644 --- a/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php @@ -94,6 +94,7 @@ public function validate($value, $constraints = null, $groups = null) $node = new GenericNode( $value, + is_object($value) ? spl_object_hash($value) : null, $metadata, $this->defaultPropertyPath, $groups @@ -118,6 +119,7 @@ public function validate($value, $constraints = null, $groups = null) $node = new ClassNode( $value, + spl_object_hash($value), $metadata, $this->defaultPropertyPath, $groups @@ -155,14 +157,15 @@ public function validateProperty($object, $propertyName, $groups = null) $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $cacheKey = spl_object_hash($object); $nodes = array(); foreach ($propertyMetadatas as $propertyMetadata) { $propertyValue = $propertyMetadata->getPropertyValue($object); $nodes[] = new PropertyNode( - $object, $propertyValue, + $cacheKey.':'.$propertyName, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups @@ -194,12 +197,13 @@ public function validatePropertyValue($object, $propertyName, $value, $groups = $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + $cacheKey = spl_object_hash($object); $nodes = array(); foreach ($propertyMetadatas as $propertyMetadata) { $nodes[] = new PropertyNode( - $object, $value, + $cacheKey.':'.$propertyName, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), $groups, From 90c27bb1e729a3b865f87b79c5f75b97d9c5645c Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Mar 2014 10:02:15 +0100 Subject: [PATCH 75/86] [Validator] Removed traverser implementation The traverser is too slow compared to the current, recursive approach. Testing showed a performance decrease of about 70% without a lot of optimization potential. --- .../Component/Validator/Node/ClassNode.php | 74 --- .../Validator/Node/CollectionNode.php | 66 --- .../Component/Validator/Node/GenericNode.php | 26 - src/Symfony/Component/Validator/Node/Node.php | 101 ---- .../Component/Validator/Node/PropertyNode.php | 82 --- .../NodeTraverser/NodeTraverserInterface.php | 99 ---- .../NonRecursiveNodeTraverser.php | 560 ------------------ .../Validator/NodeVisitor/AbstractVisitor.php | 47 -- .../NodeVisitor/NodeValidationVisitor.php | 207 ------- .../NodeVisitor/NodeVisitorInterface.php | 59 -- .../ObjectInitializationVisitor.php | 83 --- .../Validator/Tests/Node/ClassNodeTest.php | 31 - .../NonRecursiveNodeTraverserTest.php | 91 --- .../TraversingValidator2Dot5ApiTest.php | 34 -- .../Validator/Tests/ValidatorBuilderTest.php | 2 +- .../TraversingContextualValidator.php | 242 -------- .../Validator/TraversingValidator.php | 128 ---- .../Component/Validator/ValidatorBuilder.php | 9 +- 18 files changed, 3 insertions(+), 1938 deletions(-) delete mode 100644 src/Symfony/Component/Validator/Node/ClassNode.php delete mode 100644 src/Symfony/Component/Validator/Node/CollectionNode.php delete mode 100644 src/Symfony/Component/Validator/Node/GenericNode.php delete mode 100644 src/Symfony/Component/Validator/Node/Node.php delete mode 100644 src/Symfony/Component/Validator/Node/PropertyNode.php delete mode 100644 src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php delete mode 100644 src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php delete mode 100644 src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php delete mode 100644 src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php delete mode 100644 src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php delete mode 100644 src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php delete mode 100644 src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php delete mode 100644 src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php delete mode 100644 src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php delete mode 100644 src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php delete mode 100644 src/Symfony/Component/Validator/Validator/TraversingValidator.php diff --git a/src/Symfony/Component/Validator/Node/ClassNode.php b/src/Symfony/Component/Validator/Node/ClassNode.php deleted file mode 100644 index f52a68366be0f..0000000000000 --- a/src/Symfony/Component/Validator/Node/ClassNode.php +++ /dev/null @@ -1,74 +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\Node; - -use Symfony\Component\Validator\Exception\UnexpectedTypeException; -use Symfony\Component\Validator\Mapping\ClassMetadataInterface; -use Symfony\Component\Validator\Mapping\TraversalStrategy; - -/** - * Represents an object and its class metadata in the validation graph. - * - * If the object is a collection which should be traversed, a new - * {@link CollectionNode} instance will be created for that object: - * - * (TagList:ClassNode) - * \ - * (TagList:CollectionNode) - * - * @since 2.5 - * @author Bernhard Schussek - */ -class ClassNode extends Node -{ - /** - * @var ClassMetadataInterface - */ - public $metadata; - - /** - * Creates a new class node. - * - * @param object $object The validated object - * @param ClassMetadataInterface $metadata The class metadata of - * that object - * @param string $propertyPath The property path leading - * to this node - * @param string[] $groups The groups in which this - * node should be validated - * @param string[]|null $cascadedGroups The groups in which - * cascaded objects should - * be validated - * @param integer $traversalStrategy The strategy used for - * traversing the object - * - * @throws UnexpectedTypeException If $object is not an object - * - * @see \Symfony\Component\Validator\Mapping\TraversalStrategy - */ - public function __construct($object, $cacheKey, ClassMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) - { - if (!is_object($object)) { - throw new UnexpectedTypeException($object, 'object'); - } - - parent::__construct( - $object, - $cacheKey, - $metadata, - $propertyPath, - $groups, - $cascadedGroups, - $traversalStrategy - ); - } -} diff --git a/src/Symfony/Component/Validator/Node/CollectionNode.php b/src/Symfony/Component/Validator/Node/CollectionNode.php deleted file mode 100644 index 79a49ff6e31f6..0000000000000 --- a/src/Symfony/Component/Validator/Node/CollectionNode.php +++ /dev/null @@ -1,66 +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\Node; - -use Symfony\Component\Validator\Exception\ConstraintDefinitionException; -use Symfony\Component\Validator\Mapping\TraversalStrategy; - -/** - * Represents a traversable value in the validation graph. - * - * @since 2.5 - * @author Bernhard Schussek - */ -class CollectionNode extends Node -{ - /** - * Creates a new collection node. - * - * @param array|\Traversable $collection The validated collection - * @param string $propertyPath The property path leading - * to this node - * @param string[] $groups The groups in which this - * node should be validated - * @param string[]|null $cascadedGroups The groups in which - * cascaded objects should be - * validated - * @param integer $traversalStrategy The strategy used for - * traversing the collection - * - * @throws ConstraintDefinitionException If $collection is not an array or a - * \Traversable - * - * @see \Symfony\Component\Validator\Mapping\TraversalStrategy - */ - public function __construct($collection, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::TRAVERSE) - { - if (!is_array($collection) && !$collection instanceof \Traversable) { - // Must throw a ConstraintDefinitionException for backwards - // compatibility reasons with Symfony < 2.5 - throw new ConstraintDefinitionException(sprintf( - 'Traversal was enabled for "%s", but this class '. - 'does not implement "\Traversable".', - get_class($collection) - )); - } - - parent::__construct( - $collection, - null, - null, - $propertyPath, - $groups, - $cascadedGroups, - $traversalStrategy - ); - } -} diff --git a/src/Symfony/Component/Validator/Node/GenericNode.php b/src/Symfony/Component/Validator/Node/GenericNode.php deleted file mode 100644 index 9c628e0a465e0..0000000000000 --- a/src/Symfony/Component/Validator/Node/GenericNode.php +++ /dev/null @@ -1,26 +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\Node; - -/** - * Represents a value that has neither class metadata nor property metadata - * attached to it. - * - * Together with {@link \Symfony\Component\Validator\Mapping\GenericMetadata}, - * this node type can be used to validate a value against some constraints. - * - * @since 2.5 - * @author Bernhard Schussek - */ -class GenericNode extends Node -{ -} diff --git a/src/Symfony/Component/Validator/Node/Node.php b/src/Symfony/Component/Validator/Node/Node.php deleted file mode 100644 index 93099844beed0..0000000000000 --- a/src/Symfony/Component/Validator/Node/Node.php +++ /dev/null @@ -1,101 +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\Node; - -use Symfony\Component\Validator\Exception\UnexpectedTypeException; -use Symfony\Component\Validator\Mapping\MetadataInterface; -use Symfony\Component\Validator\Mapping\TraversalStrategy; - -/** - * A node in the validation graph. - * - * @since 2.5 - * @author Bernhard Schussek - */ -abstract class Node -{ - /** - * The validated value. - * - * @var mixed - */ - public $value; - - public $cacheKey; - - /** - * The metadata specifying how the value should be validated. - * - * @var MetadataInterface|null - */ - public $metadata; - - /** - * The property path leading to this node. - * - * @var string - */ - public $propertyPath; - - /** - * The groups in which the value should be validated. - * - * @var string[] - */ - public $groups; - - /** - * The groups in which cascaded values should be validated. - * - * @var string[] - */ - public $cascadedGroups; - - /** - * The strategy used for traversing the validated value. - * - * @var integer - * - * @see \Symfony\Component\Validator\Mapping\TraversalStrategy - */ - public $traversalStrategy; - - /** - * Creates a new property node. - * - * @param mixed $value The property value - * @param MetadataInterface|null $metadata The property's metadata - * @param string $propertyPath The property path leading to - * this node - * @param string[] $groups The groups in which this node - * should be validated - * @param string[]|null $cascadedGroups The groups in which cascaded - * objects should be validated - * @param integer $traversalStrategy - * - * @throws UnexpectedTypeException If $cascadedGroups is invalid - */ - public function __construct($value, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) - { - if (null !== $cascadedGroups && !is_array($cascadedGroups)) { - throw new UnexpectedTypeException($cascadedGroups, 'null or array'); - } - - $this->value = $value; - $this->cacheKey = $cacheKey; - $this->metadata = $metadata; - $this->propertyPath = $propertyPath; - $this->groups = $groups; - $this->cascadedGroups = $cascadedGroups; - $this->traversalStrategy = $traversalStrategy; - } -} diff --git a/src/Symfony/Component/Validator/Node/PropertyNode.php b/src/Symfony/Component/Validator/Node/PropertyNode.php deleted file mode 100644 index 4ee7ac5918906..0000000000000 --- a/src/Symfony/Component/Validator/Node/PropertyNode.php +++ /dev/null @@ -1,82 +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\Node; - -use Symfony\Component\Validator\Exception\UnexpectedTypeException; -use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; -use Symfony\Component\Validator\Mapping\TraversalStrategy; - -/** - * Represents the value of a property and its associated metadata. - * - * If the property contains an object and should be cascaded, a new - * {@link ClassNode} instance will be created for that object: - * - * (Article:ClassNode) - * \ - * (->author:PropertyNode) - * \ - * (Author:ClassNode) - * - * If the property contains a collection which should be traversed, a new - * {@link CollectionNode} instance will be created for that collection: - * - * (Article:ClassNode) - * \ - * (->tags:PropertyNode) - * \ - * (array:CollectionNode) - * - * @since 2.5 - * @author Bernhard Schussek - */ -class PropertyNode extends Node -{ - /** - * @var PropertyMetadataInterface - */ - public $metadata; - - /** - * Creates a new property node. - * - * @param object $object The object the property - * belongs to - * @param mixed $value The property value - * @param PropertyMetadataInterface $metadata The property's metadata - * @param string $propertyPath The property path leading - * to this node - * @param string[] $groups The groups in which this - * node should be validated - * @param string[]|null $cascadedGroups The groups in which - * cascaded objects should - * be validated - * @param integer $traversalStrategy - * - * @throws UnexpectedTypeException If $object is not an object - * - * @see \Symfony\Component\Validator\Mapping\TraversalStrategy - */ - public function __construct($value, $cacheKey, PropertyMetadataInterface $metadata, $propertyPath, array $groups, $cascadedGroups = null, $traversalStrategy = TraversalStrategy::IMPLICIT) - { - parent::__construct( - $value, - $cacheKey, - $metadata, - $propertyPath, - $groups, - $cascadedGroups, - $traversalStrategy - ); - } - -} diff --git a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php b/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php deleted file mode 100644 index 36e559ba6854e..0000000000000 --- a/src/Symfony/Component/Validator/NodeTraverser/NodeTraverserInterface.php +++ /dev/null @@ -1,99 +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\NodeTraverser; - -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; - -/** - * Traverses the nodes of the validation graph. - * - * You can attach visitors to the traverser that are invoked during the - * traversal. Before starting the traversal, the - * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::beforeTraversal()} - * method of each visitor is called. For each node in the graph, the - * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::visit()} - * of each visitor is called. At the end of the traversal, the traverser invokes - * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::afterTraversal()} - * on each visitor. The visitors are called in the same order in which they are - * added to the traverser. - * - * If the {@link traverse()} method is called recursively, the - * {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::beforeTraversal()} - * and {@link \Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface::afterTraversal()} - * methods of the visitors will be invoked for each call. - * - * The validation graph typically contains nodes of the following types: - * - * - {@link \Symfony\Component\Validator\Node\ClassNode}: - * An object with associated class metadata - * - {@link \Symfony\Component\Validator\Node\PropertyNode}: - * A property value with associated property metadata - * - {@link \Symfony\Component\Validator\Node\GenericNode}: - * A generic value with associated constraints - * - {@link \Symfony\Component\Validator\Node\CollectionNode}: - * A traversable collection - * - * Generic nodes are mostly useful when you want to validate a value that has - * neither associated class nor property metadata. Generic nodes usually come - * with {@link \Symfony\Component\Validator\Mapping\GenericMetadata}, that - * contains the constraints that the value should be validated against. - * - * Whenever a class, property or generic node is validated that contains a - * traversable value which should be traversed (according to the - * {@link \Symfony\Component\Validator\Mapping\TraversalStrategy} specified - * in the node or its metadata), a new - * {@link \Symfony\Component\Validator\Node\CollectionNode} will be attached - * to the node graph. - * - * For example: - * - * (TagList:ClassNode) - * \ - * (TagList:CollectionNode) - * - * When writing custom visitors, be aware that collection nodes usually contain - * values that have already been passed to the visitor before through a class - * node, a property node or a generic node. - * - * @since 2.5 - * @author Bernhard Schussek - */ -interface NodeTraverserInterface -{ - /** - * Adds a new visitor to the traverser. - * - * Visitors that have already been added before are ignored. - * - * @param NodeVisitorInterface $visitor The visitor to add - */ - public function addVisitor(NodeVisitorInterface $visitor); - - /** - * Removes a visitor from the traverser. - * - * Non-existing visitors are ignored. - * - * @param NodeVisitorInterface $visitor The visitor to remove - */ - public function removeVisitor(NodeVisitorInterface $visitor); - - /** - * Traverses the given nodes in the given context. - * - * @param Node[] $nodes The nodes to traverse - * @param ExecutionContextInterface $context The validation context - */ - public function traverse($nodes, ExecutionContextInterface $context); -} diff --git a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php b/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php deleted file mode 100644 index c29bac71e2387..0000000000000 --- a/src/Symfony/Component/Validator/NodeTraverser/NonRecursiveNodeTraverser.php +++ /dev/null @@ -1,560 +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\NodeTraverser; - -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Exception\NoSuchMetadataException; -use Symfony\Component\Validator\Exception\UnsupportedMetadataException; -use Symfony\Component\Validator\Mapping\CascadingStrategy; -use Symfony\Component\Validator\Mapping\ClassMetadataInterface; -use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; -use Symfony\Component\Validator\Mapping\TraversalStrategy; -use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\CollectionNode; -use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\Node\PropertyNode; -use Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface; - -/** - * Non-recursive implementation of {@link NodeTraverserInterface}. - * - * This implementation uses a Depth First algorithm to traverse the node - * graph. Instead of loading the complete node graph into memory before the - * traversal, the traverser only expands the successor nodes of a node once - * that node is traversed. For example, when traversing a class node, the - * nodes for all constrained properties of that class are loaded into memory. - * When the traversal of the class node is over, the node is discarded. - * - * Next, one of the class' property nodes is traversed. At that point, the - * successor nodes of that property node (a class node, if the property should - * be cascaded, or a collection node, if the property should be traversed) are - * loaded into memory. As soon as the traversal of the property node is over, - * it is discarded as well. - * - * This leads to an average memory consumption of O(log N * B), where N is the - * number of nodes in the graph and B is the average number of successor nodes - * of a node. - * - * In order to maintain a small execution stack, nodes are not validated - * recursively, but iteratively. Internally, a stack is used to store all the - * nodes that should be processed. Whenever a node is traversed, its successor - * nodes are put on the stack. The traverser keeps fetching and traversing nodes - * from the stack until the stack is empty and all nodes have been traversed. - * - * @since 2.5 - * @author Bernhard Schussek - * - * @see NodeTraverserInterface - * @see CascadingStrategy - * @see TraversalStrategy - */ -class NonRecursiveNodeTraverser implements NodeTraverserInterface -{ - /** - * @var NodeVisitorInterface[] - */ - private $visitors; - - /** - * @var MetadataFactoryInterface - */ - private $metadataFactory; - - /** - * Creates a new traverser. - * - * @param MetadataFactoryInterface $metadataFactory The metadata factory - */ - public function __construct(MetadataFactoryInterface $metadataFactory) - { - $this->visitors = new \SplObjectStorage(); - $this->nodeStack = new \SplStack(); - $this->metadataFactory = $metadataFactory; - } - - /** - * {@inheritdoc} - */ - public function addVisitor(NodeVisitorInterface $visitor) - { - $this->visitors->attach($visitor); - } - - /** - * {@inheritdoc} - */ - public function removeVisitor(NodeVisitorInterface $visitor) - { - $this->visitors->detach($visitor); - } - - /** - * {@inheritdoc} - */ - public function traverse($nodes, ExecutionContextInterface $context) - { - if (!is_array($nodes)) { - $nodes = array($nodes); - } - - $numberOfInitializedVisitors = $this->beforeTraversal($nodes, $context); - - // If any of the visitors requested to abort the traversal, do so, but - // clean up before - if ($numberOfInitializedVisitors < count($this->visitors)) { - $this->afterTraversal($nodes, $context, $numberOfInitializedVisitors); - - return; - } - - // This stack contains all the nodes that should be traversed - // A stack is used rather than a queue in order to traverse the graph - // in a Depth First approach (the last added node is processed first). - // In this way, the order in which the nodes are passed to the visitors - // is similar to a recursive implementation (except that the successor - // nodes of a node are traversed right-to-left instead of left-to-right). - $nodeStack = new \SplStack(); - - foreach ($nodes as $node) { - // Push a node to the stack and immediately process it. This way, - // the successor nodes are traversed before the next node in $nodes - $nodeStack->push($node); - - // Fetch nodes from the stack and traverse them until no more nodes - // are left. Then continue with the next node in $nodes. - while (!$nodeStack->isEmpty()) { - $node = $nodeStack->pop(); - - if ($node instanceof ClassNode) { - $this->traverseClassNode($node, $context, $nodeStack); - } elseif ($node instanceof CollectionNode) { - $this->traverseCollectionNode($node, $context, $nodeStack); - } else { - $this->traverseNode($node, $context, $nodeStack); - } - } - } - - $this->afterTraversal($nodes, $context); - } - - /** - * Executes the {@link NodeVisitorInterface::beforeTraversal()} method of - * each visitor. - * - * @param Node[] $nodes The traversed nodes - * @param ExecutionContextInterface $context The current execution context - * - * @return integer The number of successful calls. This is lower than - * the number of visitors if any of the visitors' - * beforeTraversal() methods returned false - */ - private function beforeTraversal($nodes, ExecutionContextInterface $context) - { - $numberOfCalls = 1; - - foreach ($this->visitors as $visitor) { - if (false === $visitor->beforeTraversal($nodes, $context)) { - break; - } - - ++$numberOfCalls; - } - - return $numberOfCalls; - } - - /** - * Executes the {@link NodeVisitorInterface::beforeTraversal()} method of - * each visitor. - * - * @param Node[] $nodes The traversed nodes - * @param ExecutionContextInterface $context The current execution context - * @param integer|null $limit Limits the number of visitors - * on which beforeTraversal() - * should be called. All visitors - * will be called by default - */ - private function afterTraversal($nodes, ExecutionContextInterface $context, $limit = null) - { - if (null === $limit) { - $limit = count($this->visitors); - } - - $numberOfCalls = 0; - - foreach ($this->visitors as $visitor) { - $visitor->afterTraversal($nodes, $context); - - if (++$numberOfCalls === $limit) { - return; - } - } - } - - /** - * Executes the {@link NodeVisitorInterface::visit()} method of each - * visitor. - * - * @param Node $node The visited node - * @param ExecutionContextInterface $context The current execution context - * - * @return Boolean Whether to traverse the node's successor nodes - */ - private function visit(Node $node, ExecutionContextInterface $context) - { - foreach ($this->visitors as $visitor) { - if (false === $visitor->visit($node, $context)) { - return false; - } - } - - return true; - } - - /** - * Traverses a class node. - * - * At first, each visitor is invoked for this node. Then, unless any - * of the visitors aborts the traversal by returning false, a property - * node is put on the node stack for each constrained property of the class. - * At last, if the class is traversable and should be traversed according - * to the selected traversal strategy, a new collection node is put on the - * stack. - * - * @param ClassNode $node The class node - * @param ExecutionContextInterface $context The current execution context - * @param \SplStack $nodeStack The stack for storing the - * successor nodes - * - * @throws UnsupportedMetadataException If a property metadata does not - * implement {@link PropertyMetadataInterface} - * - * @see ClassNode - * @see PropertyNode - * @see CollectionNode - * @see TraversalStrategy - */ - private function traverseClassNode(ClassNode $node, ExecutionContextInterface $context, \SplStack $nodeStack) - { - // Visitors have two possibilities to influence the traversal: - // - // 1. If a visitor's visit() method returns false, the traversal is - // skipped entirely. - // 2. If a visitor's visit() method removes a group from the node, - // that group will be skipped in the subtree of that node. - - if (false === $this->visit($node, $context)) { - return; - } - - if (0 === count($node->groups)) { - return; - } - - foreach ($node->metadata->getConstrainedProperties() as $propertyName) { - foreach ($node->metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { - if (!$propertyMetadata instanceof PropertyMetadataInterface) { - throw new UnsupportedMetadataException(sprintf( - 'The property metadata instances should implement '. - '"Symfony\Component\Validator\Mapping\PropertyMetadataInterface", '. - 'got: "%s".', - is_object($propertyMetadata) ? get_class($propertyMetadata) : gettype($propertyMetadata) - )); - } - - $nodeStack->push(new PropertyNode( - $propertyMetadata->getPropertyValue($node->value), - $node->cacheKey.':'.$propertyName, - $propertyMetadata, - $node->propertyPath - ? $node->propertyPath.'.'.$propertyName - : $propertyName, - $node->groups, - $node->cascadedGroups - )); - } - } - - $traversalStrategy = $node->traversalStrategy; - - // If no specific traversal strategy was requested when this method - // was called, use the traversal strategy of the class' metadata - if ($traversalStrategy & TraversalStrategy::IMPLICIT) { - // Keep the STOP_RECURSION flag, if it was set - $traversalStrategy = $node->metadata->getTraversalStrategy() - | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); - } - - // Traverse only if IMPLICIT or TRAVERSE - if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { - return; - } - - // If IMPLICIT, stop unless we deal with a Traversable - if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$node->value instanceof \Traversable) { - return; - } - - // If TRAVERSE, the constructor will fail if we have no Traversable - $nodeStack->push(new CollectionNode( - $node->value, - $node->propertyPath, - $node->groups, - $node->cascadedGroups, - $traversalStrategy - )); - } - - /** - * Traverses a collection node. - * - * At first, each visitor is invoked for this node. Then, unless any - * of the visitors aborts the traversal by returning false, the successor - * nodes of the collection node are put on the stack: - * - * - for each object in the collection with associated class metadata, a - * new class node is put on the stack; - * - if an object has no associated class metadata, but is traversable, and - * unless the {@link TraversalStrategy::STOP_RECURSION} flag is set for - * collection node, a new collection node is put on the stack for that - * object; - * - for each array in the collection, a new collection node is put on the - * stack. - * - * @param CollectionNode $node The collection node - * @param ExecutionContextInterface $context The current execution context - * @param \SplStack $nodeStack The stack for storing the - * successor nodes - * - * @see ClassNode - * @see CollectionNode - */ - private function traverseCollectionNode(CollectionNode $node, ExecutionContextInterface $context, \SplStack $nodeStack) - { - // Visitors have two possibilities to influence the traversal: - // - // 1. If a visitor's visit() method returns false, the traversal is - // skipped entirely. - // 2. If a visitor's visit() method removes a group from the node, - // that group will be skipped in the subtree of that node. - - if (false === $this->visit($node, $context)) { - return; - } - - if (0 === count($node->groups)) { - return; - } - - $traversalStrategy = $node->traversalStrategy; - - if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) { - $traversalStrategy = TraversalStrategy::NONE; - } else { - $traversalStrategy = TraversalStrategy::IMPLICIT; - } - - foreach ($node->value as $key => $value) { - if (is_array($value)) { - // Arrays are always cascaded, independent of the specified - // traversal strategy - // (BC with Symfony < 2.5) - $nodeStack->push(new CollectionNode( - $value, - $node->propertyPath.'['.$key.']', - $node->groups, - null, - $traversalStrategy - )); - - continue; - } - - // Scalar and null values in the collection are ignored - // (BC with Symfony < 2.5) - if (is_object($value)) { - $this->cascadeObject( - $value, - $node->propertyPath.'['.$key.']', - $node->groups, - $traversalStrategy, - $nodeStack - ); - } - } - } - - /** - * Traverses a node that is neither a class nor a collection node. - * - * At first, each visitor is invoked for this node. Then, unless any - * of the visitors aborts the traversal by returning false, the successor - * nodes of the collection node are put on the stack: - * - * - if the node contains an object with associated class metadata, a new - * class node is put on the stack; - * - if the node contains a traversable object without associated class - * metadata and traversal is enabled according to the selected traversal - * strategy, a collection node is put on the stack; - * - if the node contains an array, a collection node is put on the stack. - * - * @param Node $node The node - * @param ExecutionContextInterface $context The current execution context - * @param \SplStack $nodeStack The stack for storing the - * successor nodes - */ - private function traverseNode(Node $node, ExecutionContextInterface $context, \SplStack $nodeStack) - { - // Visitors have two possibilities to influence the traversal: - // - // 1. If a visitor's visit() method returns false, the traversal is - // skipped entirely. - // 2. If a visitor's visit() method removes a group from the node, - // that group will be skipped in the subtree of that node. - - if (false === $this->visit($node, $context)) { - return; - } - - if (null === $node->value) { - return; - } - - // The "cascadedGroups" property is set by the NodeValidationVisitor when - // traversing group sequences - $cascadedGroups = null !== $node->cascadedGroups - ? $node->cascadedGroups - : $node->groups; - - if (0 === count($cascadedGroups)) { - return; - } - - $cascadingStrategy = $node->metadata->getCascadingStrategy(); - $traversalStrategy = $node->traversalStrategy; - - // If no specific traversal strategy was requested when this method - // was called, use the traversal strategy of the node's metadata - if ($traversalStrategy & TraversalStrategy::IMPLICIT) { - // Keep the STOP_RECURSION flag, if it was set - $traversalStrategy = $node->metadata->getTraversalStrategy() - | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); - } - - if (is_array($node->value)) { - // Arrays are always traversed, independent of the specified - // traversal strategy - // (BC with Symfony < 2.5) - $nodeStack->push(new CollectionNode( - $node->value, - $node->propertyPath, - $cascadedGroups, - null, - $traversalStrategy - )); - - return; - } - - if ($cascadingStrategy & CascadingStrategy::CASCADE) { - // If the value is a scalar, pass it anyway, because we want - // a NoSuchMetadataException to be thrown in that case - // (BC with Symfony < 2.5) - $this->cascadeObject( - $node->value, - $node->propertyPath, - $cascadedGroups, - $traversalStrategy, - $nodeStack - ); - - return; - } - - // Currently, the traversal strategy can only be TRAVERSE for a - // generic node if the cascading strategy is CASCADE. Thus, traversable - // objects will always be handled within cascadeObject() and there's - // nothing more to do here. - - // see GenericMetadata::addConstraint() - } - - /** - * Executes the cascading logic for an object. - * - * If class metadata is available for the object, a class node is put on - * the node stack. Otherwise, if the selected traversal strategy allows - * traversal of the object, a new collection node is put on the stack. - * Otherwise, an exception is thrown. - * - * @param object $object The object to cascade - * @param string $propertyPath The current property path - * @param string[] $groups The validated groups - * @param integer $traversalStrategy The strategy for traversing the - * cascaded object - * @param \SplStack $nodeStack The stack for storing the successor - * nodes - * - * @throws NoSuchMetadataException If the object has no associated metadata - * and does not implement {@link \Traversable} - * or if traversal is disabled via the - * $traversalStrategy argument - * @throws UnsupportedMetadataException If the metadata returned by the - * metadata factory does not implement - * {@link ClassMetadataInterface} - */ - private function cascadeObject($object, $propertyPath, array $groups, $traversalStrategy, \SplStack $nodeStack) - { - try { - $classMetadata = $this->metadataFactory->getMetadataFor($object); - - if (!$classMetadata instanceof ClassMetadataInterface) { - throw new UnsupportedMetadataException(sprintf( - 'The metadata factory should return instances of '. - '"Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. - 'got: "%s".', - is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) - )); - } - - $nodeStack->push(new ClassNode( - $object, - spl_object_hash($object), - $classMetadata, - $propertyPath, - $groups, - null, - $traversalStrategy - )); - } catch (NoSuchMetadataException $e) { - // Rethrow if not Traversable - if (!$object instanceof \Traversable) { - throw $e; - } - - // Rethrow unless IMPLICIT or TRAVERSE - if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { - throw $e; - } - - $nodeStack->push(new CollectionNode( - $object, - $propertyPath, - $groups, - null, - $traversalStrategy - )); - } - } -} diff --git a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php deleted file mode 100644 index c516f4a8d233b..0000000000000 --- a/src/Symfony/Component/Validator/NodeVisitor/AbstractVisitor.php +++ /dev/null @@ -1,47 +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\NodeVisitor; - -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Node\Node; - -/** - * Base visitor with empty method stubs. - * - * @since 2.5 - * @author Bernhard Schussek - * - * @see NodeVisitorInterface - */ -abstract class AbstractVisitor implements NodeVisitorInterface -{ - /** - * {@inheritdoc} - */ - public function beforeTraversal($nodes, ExecutionContextInterface $context) - { - } - - /** - * {@inheritdoc} - */ - public function afterTraversal($nodes, ExecutionContextInterface $context) - { - } - - /** - * {@inheritdoc} - */ - public function visit(Node $node, ExecutionContextInterface $context) - { - } -} diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php deleted file mode 100644 index 5eee760c8a036..0000000000000 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeValidationVisitor.php +++ /dev/null @@ -1,207 +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\NodeVisitor; - -use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Constraints\GroupSequence; -use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\CollectionNode; -use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\Node\PropertyNode; -use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; - -/** - * Validates a node's value against the constraints defined in it's metadata. - * - * @since 2.5 - * @author Bernhard Schussek - */ -class NodeValidationVisitor extends AbstractVisitor -{ - /** - * @var ConstraintValidatorFactoryInterface - */ - private $validatorFactory; - - /** - * @var NodeTraverserInterface - */ - private $nodeTraverser; - - /** - * Creates a new visitor. - * - * @param NodeTraverserInterface $nodeTraverser The node traverser - * @param ConstraintValidatorFactoryInterface $validatorFactory The validator factory - */ - public function __construct(NodeTraverserInterface $nodeTraverser, ConstraintValidatorFactoryInterface $validatorFactory) - { - $this->validatorFactory = $validatorFactory; - $this->nodeTraverser = $nodeTraverser; - } - - /** - * Validates a node's value against the constraints defined in the node's - * metadata. - * - * Objects and constraints that were validated before in the same context - * will be skipped. - * - * @param Node $node The current node - * @param ExecutionContextInterface $context The execution context - * - * @return Boolean Whether to traverse the successor nodes - */ - public function visit(Node $node, ExecutionContextInterface $context) - { - if ($node instanceof CollectionNode) { - return true; - } - - $context->setNode($node->value, $node->metadata, $node->propertyPath); - - // if group (=[,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) { - $cascadedGroup = null; - - // Even if we remove the following clause, the constraints on an - // object won't be validated again due to the measures taken in - // validateNodeForGroup(). - // The following shortcut, however, prevents validatedNodeForGroup() - // from being called at all and enhances performance a bit. - if ($node instanceof ClassNode) { - // Use the object hash for group sequences - $groupHash = is_object($group) ? spl_object_hash($group) : $group; - - if ($context->isGroupValidated($node->cacheKey, $groupHash)) { - // Skip this group when validating the successor nodes - // (property and/or collection nodes) - unset($node->groups[$key]); - - continue; - } - - $context->markGroupAsValidated($node->cacheKey, $groupHash); - - // Replace the "Default" group by the group sequence defined - // for the class, if applicable - // This is done after checking the cache, so that - // spl_object_hash() isn't called for this sequence and - // "Default" is used instead in the cache. This is useful - // if the getters below return different group sequences in - // every call. - if (Constraint::DEFAULT_GROUP === $group) { - if ($node->metadata->hasGroupSequence()) { - // The group sequence is statically defined for the class - $group = $node->metadata->getGroupSequence(); - $cascadedGroup = Constraint::DEFAULT_GROUP; - } elseif ($node->metadata->isGroupSequenceProvider()) { - // The group sequence is dynamically obtained from the validated - // object - /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $group = $node->value->getGroupSequence(); - $cascadedGroup = Constraint::DEFAULT_GROUP; - - if (!$group instanceof GroupSequence) { - $group = new GroupSequence($group); - } - } - } - } - - if ($group instanceof GroupSequence) { - // Traverse group sequence until a violation is generated - $this->traverseGroupSequence($node, $group, $cascadedGroup, $context); - - // Skip the group sequence when validating successor nodes - unset($node->groups[$key]); - - continue; - } - - // Validate normal group - $this->validateInGroup($node, $group, $context); - } - - return true; - } - - /** - * Validates a node's value in each group of a group sequence. - * - * If any of the groups' constraints generates a violation, subsequent - * groups are not validated anymore. - * - * @param Node $node The validated node - * @param GroupSequence $groupSequence The group sequence - * @param ExecutionContextInterface $context The execution context - */ - private function traverseGroupSequence(Node $node, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) - { - $violationCount = count($context->getViolations()); - $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null; - - foreach ($groupSequence->groups as $groupInSequence) { - $node = clone $node; - $node->groups = array($groupInSequence); - $node->cascadedGroups = $cascadedGroups; - - $this->nodeTraverser->traverse(array($node), $context); - - // Abort sequence validation if a violation was generated - if (count($context->getViolations()) > $violationCount) { - break; - } - } - } - - /** - * Validates a node's value against all constraints in the given group. - * - * @param Node $node The validated node - * @param string $group The group to validate - * @param ExecutionContextInterface $context The execution context - * @param string $objectHash The hash of the node's - * object (if any) - * - * @throws \Exception - */ - private function validateInGroup(Node $node, $group, ExecutionContextInterface $context) - { - $context->setGroup($group); - - foreach ($node->metadata->findConstraints($group) as $constraint) { - // Prevent duplicate validation of constraints, in the case - // that constraints belong to multiple validated groups - if (null !== $node->cacheKey) { - $constraintHash = spl_object_hash($constraint); - - if ($context->isConstraintValidated($node->cacheKey, $constraintHash)) { - continue; - } - - $context->markConstraintAsValidated($node->cacheKey, $constraintHash); - } - - $validator = $this->validatorFactory->getInstance($constraint); - $validator->initialize($context); - $validator->validate($node->value, $constraint); - } - } -} diff --git a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php b/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php deleted file mode 100644 index ec05923f1bd5e..0000000000000 --- a/src/Symfony/Component/Validator/NodeVisitor/NodeVisitorInterface.php +++ /dev/null @@ -1,59 +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\NodeVisitor; - -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Node\Node; - -/** - * A node visitor invoked by the node traverser. - * - * At the beginning of the traversal, the method {@link beforeTraversal()} is - * called. For each traversed node, the method {@link visit()} is called. At - * last, the method {@link afterTraversal()} is called when the traversal is - * complete. - * - * @since 2.5 - * @author Bernhard Schussek - * - * @see \Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface - */ -interface NodeVisitorInterface -{ - /** - * Called at the beginning of a traversal. - * - * @param Node[] $nodes A list of Node instances - * @param ExecutionContextInterface $context The execution context - * - * @return Boolean Whether to continue the traversal - */ - public function beforeTraversal($nodes, ExecutionContextInterface $context); - - /** - * Called at the end of a traversal. - * - * @param Node[] $nodes A list of Node instances - * @param ExecutionContextInterface $context The execution context - */ - public function afterTraversal($nodes, ExecutionContextInterface $context); - - /** - * Called for each node during a traversal. - * - * @param Node $node The current node - * @param ExecutionContextInterface $context The execution context - * - * @return Boolean Whether to traverse the node's successor nodes - */ - public function visit(Node $node, ExecutionContextInterface $context); -} diff --git a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php b/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php deleted file mode 100644 index 18fe19a955d71..0000000000000 --- a/src/Symfony/Component/Validator/NodeVisitor/ObjectInitializationVisitor.php +++ /dev/null @@ -1,83 +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\NodeVisitor; - -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Exception\InvalidArgumentException; -use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\ObjectInitializerInterface; - -/** - * Initializes the objects of all class nodes. - * - * You have to pass at least one instance of {@link ObjectInitializerInterface} - * to the constructor of this visitor. - * - * @since 2.5 - * @author Bernhard Schussek - */ -class ObjectInitializationVisitor extends AbstractVisitor -{ - /** - * @var ObjectInitializerInterface[] - */ - private $initializers; - - /** - * Creates a new visitor. - * - * @param ObjectInitializerInterface[] $initializers The object initializers - * - * @throws InvalidArgumentException - */ - public function __construct(array $initializers) - { - foreach ($initializers as $initializer) { - if (!$initializer instanceof ObjectInitializerInterface) { - throw new InvalidArgumentException(sprintf( - 'Validator initializers must implement '. - '"Symfony\Component\Validator\ObjectInitializerInterface". '. - 'Got: "%s"', - is_object($initializer) ? get_class($initializer) : gettype($initializer) - )); - } - } - - // If no initializer is present, this visitor should not even be created - if (0 === count($initializers)) { - throw new InvalidArgumentException('Please pass at least one initializer.'); - } - - $this->initializers = $initializers; - } - - /** - * Calls the {@link ObjectInitializerInterface::initialize()} method for - * the object of each class node. - * - * @param Node $node The current node - * @param ExecutionContextInterface $context The execution context - * - * @return Boolean Always returns true - */ - public function visit(Node $node, ExecutionContextInterface $context) - { - if ($node instanceof ClassNode) { - foreach ($this->initializers as $initializer) { - $initializer->initialize($node->value); - } - } - - return true; - } -} diff --git a/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php b/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php deleted file mode 100644 index c79f4c838f60c..0000000000000 --- a/src/Symfony/Component/Validator/Tests/Node/ClassNodeTest.php +++ /dev/null @@ -1,31 +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\Node; - -use Symfony\Component\Validator\Node\ClassNode; - -/** - * @since 2.5 - * @author Bernhard Schussek - */ -class ClassNodeTest extends \PHPUnit_Framework_TestCase -{ - /** - * @expectedException \Symfony\Component\Validator\Exception\UnexpectedTypeException - */ - public function testConstructorExpectsObject() - { - $metadata = $this->getMock('Symfony\Component\Validator\Mapping\ClassMetadataInterface'); - - new ClassNode('foobar', null, $metadata, '', array(), array()); - } -} diff --git a/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php b/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php deleted file mode 100644 index 09e26bcaf9e11..0000000000000 --- a/src/Symfony/Component/Validator/Tests/NodeTraverser/NonRecursiveNodeTraverserTest.php +++ /dev/null @@ -1,91 +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\NodeTraverser; - -use Symfony\Component\Validator\Mapping\GenericMetadata; -use Symfony\Component\Validator\Node\GenericNode; -use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; -use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; - -/** - * @since 2.5 - * @author Bernhard Schussek - */ -class NonRecursiveNodeTraverserTest extends \PHPUnit_Framework_TestCase -{ - /** - * @var FakeMetadataFactory - */ - private $metadataFactory; - - /** - * @var NonRecursiveNodeTraverser - */ - private $traverser; - - protected function setUp() - { - $this->metadataFactory = new FakeMetadataFactory(); - $this->traverser = new NonRecursiveNodeTraverser($this->metadataFactory); - } - - public function testVisitorsMayPreventTraversal() - { - $nodes = array(new GenericNode('value', null, new GenericMetadata(), '', array('Default'))); - $context = $this->getMock('Symfony\Component\Validator\Context\ExecutionContextInterface'); - - $visitor1 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface'); - $visitor2 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface'); - $visitor3 = $this->getMock('Symfony\Component\Validator\NodeVisitor\NodeVisitorInterface'); - - $visitor1->expects($this->once()) - ->method('beforeTraversal') - ->with($nodes, $context); - - // abort traversal - $visitor2->expects($this->once()) - ->method('beforeTraversal') - ->with($nodes, $context) - ->will($this->returnValue(false)); - - // never called - $visitor3->expects($this->never()) - ->method('beforeTraversal'); - - $visitor1->expects($this->never()) - ->method('visit'); - $visitor2->expects($this->never()) - ->method('visit'); - $visitor2->expects($this->never()) - ->method('visit'); - - // called in order to clean up - $visitor1->expects($this->once()) - ->method('afterTraversal') - ->with($nodes, $context); - - // abort traversal - $visitor2->expects($this->once()) - ->method('afterTraversal') - ->with($nodes, $context); - - // never called, because beforeTraversal() wasn't called either - $visitor3->expects($this->never()) - ->method('afterTraversal'); - - $this->traverser->addVisitor($visitor1); - $this->traverser->addVisitor($visitor2); - $this->traverser->addVisitor($visitor3); - - $this->traverser->traverse($nodes, $context); - } -} diff --git a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php deleted file mode 100644 index 5c76a173a4c53..0000000000000 --- a/src/Symfony/Component/Validator/Tests/Validator/TraversingValidator2Dot5ApiTest.php +++ /dev/null @@ -1,34 +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\Validator; - -use Symfony\Component\Validator\ConstraintValidatorFactory; -use Symfony\Component\Validator\Context\ExecutionContextFactory; -use Symfony\Component\Validator\DefaultTranslator; -use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; -use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; -use Symfony\Component\Validator\Validator\TraversingValidator; - -class TraversingValidator2Dot5ApiTest extends Abstract2Dot5ApiTest -{ - protected function createValidator(MetadataFactoryInterface $metadataFactory) - { - $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); - $contextFactory = new ExecutionContextFactory(new DefaultTranslator()); - $validator = new TraversingValidator($contextFactory, $nodeTraverser, $metadataFactory); - - $nodeTraverser->addVisitor(new NodeValidationVisitor($nodeTraverser, new ConstraintValidatorFactory())); - - return $validator; - } -} diff --git a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php index 5cd1198654697..9228d4564cf24 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php @@ -130,7 +130,7 @@ public function testSetApiVersion24() public function testSetApiVersion25() { $this->assertSame($this->builder, $this->builder->setApiVersion(Validation::API_VERSION_2_5)); - $this->assertInstanceOf('Symfony\Component\Validator\Validator\TraversingValidator', $this->builder->getValidator()); + $this->assertInstanceOf('Symfony\Component\Validator\Validator\RecursiveValidator', $this->builder->getValidator()); } public function testSetApiVersion24And25() diff --git a/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php b/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php deleted file mode 100644 index bd749eeb9757f..0000000000000 --- a/src/Symfony/Component/Validator/Validator/TraversingContextualValidator.php +++ /dev/null @@ -1,242 +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\Validator; - -use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Exception\RuntimeException; -use Symfony\Component\Validator\Exception\UnsupportedMetadataException; -use Symfony\Component\Validator\Exception\ValidatorException; -use Symfony\Component\Validator\Mapping\ClassMetadataInterface; -use Symfony\Component\Validator\Mapping\GenericMetadata; -use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\CollectionNode; -use Symfony\Component\Validator\Node\GenericNode; -use Symfony\Component\Validator\Node\PropertyNode; -use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; -use Symfony\Component\Validator\Util\PropertyPath; - -/** - * Default implementation of {@link ContextualValidatorInterface}. - * - * @since 2.5 - * @author Bernhard Schussek - */ -class TraversingContextualValidator implements ContextualValidatorInterface -{ - /** - * @var ExecutionContextInterface - */ - private $context; - - /** - * @var NodeTraverserInterface - */ - private $nodeTraverser; - - /** - * @var MetadataFactoryInterface - */ - private $metadataFactory; - - /** - * Creates a validator for the given context. - * - * @param ExecutionContextInterface $context The execution context - * @param NodeTraverserInterface $nodeTraverser The node traverser - * @param MetadataFactoryInterface $metadataFactory The factory for fetching - * the metadata of validated - * objects - */ - public function __construct(ExecutionContextInterface $context, NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory) - { - $this->context = $context; - $this->defaultPropertyPath = $context->getPropertyPath(); - $this->defaultGroups = array($context->getGroup() ?: Constraint::DEFAULT_GROUP); - $this->nodeTraverser = $nodeTraverser; - $this->metadataFactory = $metadataFactory; - } - - /** - * {@inheritdoc} - */ - public function atPath($path) - { - $this->defaultPropertyPath = $this->context->getPropertyPath($path); - - return $this; - } - - /** - * {@inheritdoc} - */ - public function validate($value, $constraints = null, $groups = null) - { - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - - if (null !== $constraints) { - if (!is_array($constraints)) { - $constraints = array($constraints); - } - - $metadata = new GenericMetadata(); - $metadata->addConstraints($constraints); - - $node = new GenericNode( - $value, - is_object($value) ? spl_object_hash($value) : null, - $metadata, - $this->defaultPropertyPath, - $groups - ); - } elseif (is_array($value) || $value instanceof \Traversable && !$this->metadataFactory->hasMetadataFor($value)) { - $node = new CollectionNode( - $value, - $this->defaultPropertyPath, - $groups - ); - } elseif (is_object($value)) { - $metadata = $this->metadataFactory->getMetadataFor($value); - - if (!$metadata instanceof ClassMetadataInterface) { - throw new UnsupportedMetadataException(sprintf( - 'The metadata factory should return instances of '. - '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. - 'got: "%s".', - is_object($metadata) ? get_class($metadata) : gettype($metadata) - )); - } - - $node = new ClassNode( - $value, - spl_object_hash($value), - $metadata, - $this->defaultPropertyPath, - $groups - ); - } else { - throw new RuntimeException(sprintf( - 'Cannot validate values of type "%s" automatically. Please '. - 'provide a constraint.', - gettype($value) - )); - } - - $this->nodeTraverser->traverse(array($node), $this->context); - - return $this; - } - - /** - * {@inheritdoc} - */ - public function validateProperty($object, $propertyName, $groups = null) - { - $classMetadata = $this->metadataFactory->getMetadataFor($object); - - if (!$classMetadata instanceof ClassMetadataInterface) { - // Cannot be UnsupportedMetadataException because of BC with - // Symfony < 2.5 - throw new ValidatorException(sprintf( - 'The metadata factory should return instances of '. - '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. - 'got: "%s".', - is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) - )); - } - - $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $cacheKey = spl_object_hash($object); - $nodes = array(); - - foreach ($propertyMetadatas as $propertyMetadata) { - $propertyValue = $propertyMetadata->getPropertyValue($object); - - $nodes[] = new PropertyNode( - $propertyValue, - $cacheKey.':'.$propertyName, - $propertyMetadata, - PropertyPath::append($this->defaultPropertyPath, $propertyName), - $groups - ); - } - - $this->nodeTraverser->traverse($nodes, $this->context); - - return $this; - } - - /** - * {@inheritdoc} - */ - public function validatePropertyValue($object, $propertyName, $value, $groups = null) - { - $classMetadata = $this->metadataFactory->getMetadataFor($object); - - if (!$classMetadata instanceof ClassMetadataInterface) { - // Cannot be UnsupportedMetadataException because of BC with - // Symfony < 2.5 - throw new ValidatorException(sprintf( - 'The metadata factory should return instances of '. - '"\Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. - 'got: "%s".', - is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) - )); - } - - $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); - $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $cacheKey = spl_object_hash($object); - $nodes = array(); - - foreach ($propertyMetadatas as $propertyMetadata) { - $nodes[] = new PropertyNode( - $value, - $cacheKey.':'.$propertyName, - $propertyMetadata, - PropertyPath::append($this->defaultPropertyPath, $propertyName), - $groups, - $groups - ); - } - - $this->nodeTraverser->traverse($nodes, $this->context); - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getViolations() - { - return $this->context->getViolations(); - } - - /** - * Normalizes the given group or list of groups to an array. - * - * @param mixed $groups The groups to normalize - * - * @return array A group array - */ - protected function normalizeGroups($groups) - { - if (is_array($groups)) { - return $groups; - } - - return array($groups); - } -} diff --git a/src/Symfony/Component/Validator/Validator/TraversingValidator.php b/src/Symfony/Component/Validator/Validator/TraversingValidator.php deleted file mode 100644 index 8fe07f630fb1f..0000000000000 --- a/src/Symfony/Component/Validator/Validator/TraversingValidator.php +++ /dev/null @@ -1,128 +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\Validator; - -use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface; -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\NodeTraverser\NodeTraverserInterface; - -/** - * Default implementation of {@link ValidatorInterface}. - * - * @since 2.5 - * @author Bernhard Schussek - */ -class TraversingValidator implements ValidatorInterface -{ - /** - * @var ExecutionContextFactoryInterface - */ - protected $contextFactory; - - /** - * @var NodeTraverserInterface - */ - protected $nodeTraverser; - - /** - * @var MetadataFactoryInterface - */ - protected $metadataFactory; - - /** - * Creates a new validator. - * - * @param ExecutionContextFactoryInterface $contextFactory The factory for - * creating new contexts - * @param NodeTraverserInterface $nodeTraverser The node traverser - * @param MetadataFactoryInterface $metadataFactory The factory for - * fetching the metadata - * of validated objects - */ - public function __construct(ExecutionContextFactoryInterface $contextFactory, NodeTraverserInterface $nodeTraverser, MetadataFactoryInterface $metadataFactory) - { - $this->contextFactory = $contextFactory; - $this->nodeTraverser = $nodeTraverser; - $this->metadataFactory = $metadataFactory; - } - - /** - * {@inheritdoc} - */ - public function startContext($root = null) - { - return new TraversingContextualValidator( - $this->contextFactory->createContext($this, $root), - $this->nodeTraverser, - $this->metadataFactory - ); - } - - /** - * {@inheritdoc} - */ - public function inContext(ExecutionContextInterface $context) - { - return new TraversingContextualValidator( - $context, - $this->nodeTraverser, - $this->metadataFactory - ); - } - - /** - * {@inheritdoc} - */ - public function getMetadataFor($object) - { - return $this->metadataFactory->getMetadataFor($object); - } - - /** - * {@inheritdoc} - */ - public function hasMetadataFor($object) - { - return $this->metadataFactory->hasMetadataFor($object); - } - - /** - * {@inheritdoc} - */ - public function validate($value, $constraints = null, $groups = null) - { - return $this->startContext($value) - ->validate($value, $constraints, $groups) - ->getViolations(); - } - - /** - * {@inheritdoc} - */ - public function validateProperty($object, $propertyName, $groups = null) - { - return $this->startContext($object) - ->validateProperty($object, $propertyName, $groups) - ->getViolations(); - } - - /** - * {@inheritdoc} - */ - public function validatePropertyValue($object, $propertyName, $value, $groups = null) - { - return $this->startContext($object) - ->validatePropertyValue($object, $propertyName, $value, $groups) - ->getViolations(); - } -} diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index 2d66fa9f6af5b..8250ea6684724 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -35,6 +35,7 @@ use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; use Symfony\Component\Validator\NodeVisitor\ObjectInitializationVisitor; use Symfony\Component\Validator\Validator\LegacyValidator; +use Symfony\Component\Validator\Validator\RecursiveValidator; use Symfony\Component\Validator\Validator\TraversingValidator; use Symfony\Component\Validator\Validator as ValidatorV24; @@ -416,13 +417,7 @@ public function getValidator() $contextFactory = new LegacyExecutionContextFactory($metadataFactory, $translator, $this->translationDomain); if (Validation::API_VERSION_2_5 === $apiVersion) { - $nodeTraverser = new NonRecursiveNodeTraverser($metadataFactory); - if (count($this->initializers) > 0) { - $nodeTraverser->addVisitor(new ObjectInitializationVisitor($this->initializers)); - } - $nodeTraverser->addVisitor(new NodeValidationVisitor($nodeTraverser, $validatorFactory)); - - return new TraversingValidator($contextFactory, $nodeTraverser, $metadataFactory); + return new RecursiveValidator($contextFactory, $metadataFactory, $validatorFactory); } return new LegacyValidator($contextFactory, $metadataFactory, $validatorFactory); From 166d71a7de2bdffda3b2185c30c4841dcaf0f013 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Mar 2014 10:16:30 +0100 Subject: [PATCH 76/86] [Validator] Removed unused property --- .../Component/Validator/Context/ExecutionContext.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 75b7e2c3bf6bc..6ee58d8403a29 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -106,13 +106,6 @@ class ExecutionContext implements ExecutionContextInterface */ private $validatedConstraints = array(); - /** - * Stores which property constraint has been validated for which property. - * - * @var array - */ - private $validatedPropertyConstraints = array(); - /** * Creates a new execution context. * From 7bc952de55a5b936afbffd2f8eda5d34386e6e55 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Mar 2014 11:36:55 +0100 Subject: [PATCH 77/86] [Validator] Improved inline documentation of RecursiveContextualValidator --- .../Validator/Context/ExecutionContext.php | 10 +- .../Context/ExecutionContextInterface.php | 3 +- .../RecursiveContextualValidator.php | 529 ++++++++++-------- 3 files changed, 319 insertions(+), 223 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 6ee58d8403a29..c345e4115fd5b 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -71,6 +71,13 @@ class ExecutionContext implements ExecutionContextInterface */ private $value; + /** + * The currently validated object. + * + * @var object|null + */ + private $object; + /** * The property path leading to the current value. * @@ -132,9 +139,10 @@ public function __construct(ValidatorInterface $validator, $root, TranslatorInte /** * {@inheritdoc} */ - public function setNode($value, MetadataInterface $metadata = null, $propertyPath) + public function setNode($value, $object, MetadataInterface $metadata, $propertyPath) { $this->value = $value; + $this->object = $object; $this->metadata = $metadata; $this->propertyPath = (string) $propertyPath; } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index 2e778fbec815f..b0b8c2b97b201 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -103,13 +103,14 @@ public function getValidator(); * Sets the currently validated value. * * @param mixed $value The validated value + * @param object|null $object The currently validated object * @param MetadataInterface $metadata The validation metadata * @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 setNode($value, MetadataInterface $metadata = null, $propertyPath); + public function setNode($value, $object, MetadataInterface $metadata, $propertyPath); /** * Sets the currently validated group. diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index 0e083c05cf8fe..0da7c7cf51104 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -27,10 +27,6 @@ use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\MetadataFactoryInterface; -use Symfony\Component\Validator\Node\ClassNode; -use Symfony\Component\Validator\Node\CollectionNode; -use Symfony\Component\Validator\Node\Node; -use Symfony\Component\Validator\Node\PropertyNode; use Symfony\Component\Validator\Util\PropertyPath; /** @@ -92,7 +88,11 @@ public function validate($value, $constraints = null, $groups = null) { $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; + // If explicit constraints are passed, validate the value against + // those constraints if (null !== $constraints) { + // You can pass a single constraint or an array of constraints + // Make sure to deal with an array in the rest of the code if (!is_array($constraints)) { $constraints = array($constraints); } @@ -102,6 +102,7 @@ public function validate($value, $constraints = null, $groups = null) $this->validateGenericNode( $value, + null, is_object($value) ? spl_object_hash($value) : null, $metadata, $this->defaultPropertyPath, @@ -114,8 +115,10 @@ public function validate($value, $constraints = null, $groups = null) return $this; } + // If an object is passed without explicit constraints, validate that + // object against the constraints defined for the object's class if (is_object($value)) { - $this->cascadeObject( + $this->validateObject( $value, $this->defaultPropertyPath, $groups, @@ -126,12 +129,14 @@ public function validate($value, $constraints = null, $groups = null) return $this; } + // If an array is passed without explicit constraints, validate each + // object in the array if (is_array($value)) { - $this->cascadeCollection( + $this->validateEachObjectIn( $value, $this->defaultPropertyPath, $groups, - TraversalStrategy::IMPLICIT, + true, $this->context ); @@ -148,9 +153,9 @@ public function validate($value, $constraints = null, $groups = null) /** * {@inheritdoc} */ - public function validateProperty($container, $propertyName, $groups = null) + public function validateProperty($object, $propertyName, $groups = null) { - $classMetadata = $this->metadataFactory->getMetadataFor($container); + $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { // Cannot be UnsupportedMetadataException because of BC with @@ -165,13 +170,14 @@ public function validateProperty($container, $propertyName, $groups = null) $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $cacheKey = spl_object_hash($container); + $cacheKey = spl_object_hash($object); foreach ($propertyMetadatas as $propertyMetadata) { - $propertyValue = $propertyMetadata->getPropertyValue($container); + $propertyValue = $propertyMetadata->getPropertyValue($object); $this->validateGenericNode( $propertyValue, + $object, $cacheKey.':'.$propertyName, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), @@ -188,9 +194,9 @@ public function validateProperty($container, $propertyName, $groups = null) /** * {@inheritdoc} */ - public function validatePropertyValue($container, $propertyName, $value, $groups = null) + public function validatePropertyValue($object, $propertyName, $value, $groups = null) { - $classMetadata = $this->metadataFactory->getMetadataFor($container); + $classMetadata = $this->metadataFactory->getMetadataFor($object); if (!$classMetadata instanceof ClassMetadataInterface) { // Cannot be UnsupportedMetadataException because of BC with @@ -205,11 +211,12 @@ public function validatePropertyValue($container, $propertyName, $value, $groups $propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName); $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups; - $cacheKey = spl_object_hash($container); + $cacheKey = spl_object_hash($object); foreach ($propertyMetadatas as $propertyMetadata) { $this->validateGenericNode( $value, + $object, $cacheKey.':'.$propertyName, $propertyMetadata, PropertyPath::append($this->defaultPropertyPath, $propertyName), @@ -246,53 +253,197 @@ protected function normalizeGroups($groups) return array($groups); } - /** - * Traverses a class node. + * Validates an object against the constraints defined for its class. + * + * If no metadata is available for the class, but the class is an instance + * of {@link \Traversable} and the selected traversal strategy allows + * traversal, the object will be iterated and each nested object will be + * validated instead. + * + * @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 * - * 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. + * @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 validateObject($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->validateClassNode( + $object, + spl_object_hash($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->validateEachObjectIn( + $object, + $propertyPath, + $groups, + $traversalStrategy & TraversalStrategy::STOP_RECURSION, + $context + ); + } + } + + /** + * Validates each object in a collection against the constraints defined + * for their classes. * - * @param ClassNode $node The class node - * @param ExecutionContextInterface $context The current execution context + * If the parameter $recursive is set to true, nested {@link \Traversable} + * objects are iterated as well. Nested arrays are always iterated, + * regardless of the value of $recursive. * - * @throws UnsupportedMetadataException If a property metadata does not - * implement {@link PropertyMetadataInterface} + * @param array|\Traversable $collection The collection + * @param string $propertyPath The current property path + * @param string[] $groups The validated groups + * @param Boolean $stopRecursion Whether to disable + * recursive iteration. For + * backwards compatibility + * with Symfony < 2.5. + * @param ExecutionContextInterface $context The current execution context * * @see ClassNode - * @see PropertyNode * @see CollectionNode - * @see TraversalStrategy */ - private function validateClassNode($value, $cacheKey, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) + private function validateEachObjectIn($collection, $propertyPath, array $groups, $stopRecursion, ExecutionContextInterface $context) { - $context->setNode($value, $metadata, $propertyPath); + if ($stopRecursion) { + $traversalStrategy = TraversalStrategy::NONE; + } else { + $traversalStrategy = TraversalStrategy::IMPLICIT; + } - // 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 ($collection as $key => $value) { + if (is_array($value)) { + // Arrays are always cascaded, independent of the specified + // traversal strategy + // (BC with Symfony < 2.5) + $this->validateEachObjectIn( + $value, + $propertyPath.'['.$key.']', + $groups, + $stopRecursion, + $context + ); - foreach ($groups as $key => $group) { - $cascadedGroup = null; + continue; + } - // 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. + // Scalar and null values in the collection are ignored + // (BC with Symfony < 2.5) + if (is_object($value)) { + $this->validateObject( + $value, + $propertyPath.'['.$key.']', + $groups, + $traversalStrategy, + $context + ); + } + } + } + + /** + * Validates a class node. + * + * A class node is a combination of an object with a {@link ClassMetadataInterface} + * instance. Each class node (conceptionally) has zero or more succeeding + * property nodes: + * + * (Article:class node) + * \ + * ($title:property node) + * + * This method validates the passed objects against all constraints defined + * at class level. It furthermore triggers the validation of each of the + * class' properties against the constraints for that property. + * + * If the selected traversal strategy allows traversal, the object is + * iterated and each nested object is validated against its own constraints. + * The object is not traversed if traversal is disabled in the class + * metadata. + * + * If the passed groups contain the group "Default", the validator will + * check whether the "Default" group has been replaced by a group sequence + * in the class metadata. If this is the case, the group sequence is + * validated instead. + * + * @param object $object The validated object + * @param string $cacheKey The key for caching + * the validated object + * @param ClassMetadataInterface $metadata The class metadata of + * the object + * @param string $propertyPath The property path leading + * to the object + * @param string[] $groups The groups in which the + * object should be validated + * @param string[]|null $cascadedGroups The groups in which + * cascaded objects should + * be validated + * @param integer $traversalStrategy The strategy used for + * traversing the object + * @param ExecutionContextInterface $context The current execution context + * + * @throws UnsupportedMetadataException If a property metadata does not + * implement {@link PropertyMetadataInterface} + * @throws ConstraintDefinitionException If traversal was enabled but the + * object does not implement + * {@link \Traversable} + * + * @see TraversalStrategy + */ + private function validateClassNode($object, $cacheKey, ClassMetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) + { + $context->setNode($object, $object, $metadata, $propertyPath); + + foreach ($groups as $key => $group) { + // If the "Default" group is replaced by a group sequence, remember + // to cascade the "Default" group when traversing the group + // sequence + $defaultOverridden = false; // Use the object hash for group sequences $groupHash = is_object($group) ? spl_object_hash($group) : $group; if ($context->isGroupValidated($cacheKey, $groupHash)) { - // Skip this group when validating the successor nodes - // (property and/or collection nodes) + // Skip this group when validating the properties and when + // traversing the object unset($groups[$key]); continue; @@ -301,7 +452,7 @@ private function validateClassNode($value, $cacheKey, ClassMetadataInterface $me $context->markGroupAsValidated($cacheKey, $groupHash); // Replace the "Default" group by the group sequence defined - // for the class, if applicable + // for the class, if applicable. // This is done after checking the cache, so that // spl_object_hash() isn't called for this sequence and // "Default" is used instead in the cache. This is useful @@ -311,13 +462,13 @@ private function validateClassNode($value, $cacheKey, ClassMetadataInterface $me if ($metadata->hasGroupSequence()) { // The group sequence is statically defined for the class $group = $metadata->getGroupSequence(); - $cascadedGroup = Constraint::DEFAULT_GROUP; + $defaultOverridden = true; } elseif ($metadata->isGroupSequenceProvider()) { // The group sequence is dynamically obtained from the validated // object - /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $value */ - $group = $value->getGroupSequence(); - $cascadedGroup = Constraint::DEFAULT_GROUP; + /** @var \Symfony\Component\Validator\GroupSequenceProviderInterface $object */ + $group = $object->getGroupSequence(); + $defaultOverridden = true; if (!$group instanceof GroupSequence) { $group = new GroupSequence($group); @@ -325,23 +476,43 @@ private function validateClassNode($value, $cacheKey, ClassMetadataInterface $me } } + // If the groups (=[,G3,G4]) contain a group sequence + // (=), then call validateClassNode() with each entry of the + // group sequence and abort if necessary (G1, G2) if ($group instanceof GroupSequence) { - $this->stepThroughGroupSequence($value, $cacheKey, $metadata, $propertyPath, $traversalStrategy, $group, $cascadedGroup, $context); + $this->stepThroughGroupSequence( + $object, + $object, + $cacheKey, + $metadata, + $propertyPath, + $traversalStrategy, + $group, + $defaultOverridden ? Constraint::DEFAULT_GROUP : null, + $context + ); - // Skip the group sequence when validating successor nodes + // Skip the group sequence when validating properties, because + // stepThroughGroupSequence() already validates the properties unset($groups[$key]); continue; } - $this->validateInGroup($value, $cacheKey, $metadata, $group, $context); + $this->validateInGroup($object, $cacheKey, $metadata, $group, $context); } + // If no more groups should be validated for the property nodes, + // we can safely quit if (0 === count($groups)) { return; } + // Validate all properties against their constraints foreach ($metadata->getConstrainedProperties() as $propertyName) { + // If constraints are defined both on the getter of a property as + // well as on the property itself, then getPropertyMetadata() + // returns two metadata objects, not just one foreach ($metadata->getPropertyMetadata($propertyName) as $propertyMetadata) { if (!$propertyMetadata instanceof PropertyMetadataInterface) { throw new UnsupportedMetadataException(sprintf( @@ -352,10 +523,11 @@ private function validateClassNode($value, $cacheKey, ClassMetadataInterface $me )); } - $propertyValue = $propertyMetadata->getPropertyValue($value); + $propertyValue = $propertyMetadata->getPropertyValue($object); $this->validateGenericNode( $propertyValue, + $object, $cacheKey.':'.$propertyName, $propertyMetadata, $propertyPath @@ -383,56 +555,85 @@ private function validateClassNode($value, $cacheKey, ClassMetadataInterface $me } // If IMPLICIT, stop unless we deal with a Traversable - if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$value instanceof \Traversable) { + if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$object instanceof \Traversable) { return; } // If TRAVERSE, fail if we have no Traversable - if (!$value instanceof \Traversable) { + if (!$object instanceof \Traversable) { // Must throw a ConstraintDefinitionException for backwards // compatibility reasons with Symfony < 2.5 throw new ConstraintDefinitionException(sprintf( 'Traversal was enabled for "%s", but this class '. 'does not implement "\Traversable".', - get_class($value) + get_class($object) )); } - $this->cascadeCollection( - $value, + $this->validateEachObjectIn( + $object, $propertyPath, $groups, - $traversalStrategy, + $traversalStrategy & TraversalStrategy::STOP_RECURSION, $context ); } /** - * Traverses a node that is neither a class nor a collection node. + * Validates a node that is not a class 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: + * Currently, two such node types exist: * - * - 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. + * - property nodes, which consist of the value of an object's + * property together with a {@link PropertyMetadataInterface} instance + * - generic nodes, which consist of a value and some arbitrary + * constraints defined in a {@link MetadataInterface} container * - * @param Node $node The node - * @param ExecutionContextInterface $context The current execution context + * In both cases, the value is validated against all constraints defined + * in the passed metadata object. Then, if the value is an instance of + * {@link \Traversable} and the selected traversal strategy permits it, + * the value is traversed and each nested object validated against its own + * constraints. Arrays are always traversed. + * + * @param mixed $value The validated value + * @param object|null $object The current object + * @param string $cacheKey The key for caching + * the validated value + * @param MetadataInterface $metadata The metadata of the + * value + * @param string $propertyPath The property path leading + * to the value + * @param string[] $groups The groups in which the + * value should be validated + * @param string[]|null $cascadedGroups The groups in which + * cascaded objects should + * be validated + * @param integer $traversalStrategy The strategy used for + * traversing the value + * @param ExecutionContextInterface $context The current execution context + * + * @see TraversalStrategy */ - private function validateGenericNode($value, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) + private function validateGenericNode($value, $object, $cacheKey, MetadataInterface $metadata = null, $propertyPath, array $groups, $cascadedGroups, $traversalStrategy, ExecutionContextInterface $context) { - $context->setNode($value, $metadata, $propertyPath); + $context->setNode($value, $object, $metadata, $propertyPath); foreach ($groups as $key => $group) { if ($group instanceof GroupSequence) { - $this->stepThroughGroupSequence($value, $cacheKey, $metadata, $propertyPath, $traversalStrategy, $group, null, $context); + $this->stepThroughGroupSequence( + $value, + $object, + $cacheKey, + $metadata, + $propertyPath, + $traversalStrategy, + $group, + null, + $context + ); - // Skip the group sequence when validating successor nodes + // Skip the group sequence when cascading, as the cascading + // logic is already done in stepThroughGroupSequence() unset($groups[$key]); continue; @@ -464,8 +665,9 @@ private function validateGenericNode($value, $cacheKey, MetadataInterface $metad | ($traversalStrategy & TraversalStrategy::STOP_RECURSION); } - // The "cascadedGroups" property is set by the NodeValidationVisitor when - // traversing group sequences + // The $cascadedGroups property is set, if the "Default" group is + // overridden by a group sequence + // See validateClassNode() $cascadedGroups = count($cascadedGroups) > 0 ? $cascadedGroups : $groups; @@ -474,11 +676,11 @@ private function validateGenericNode($value, $cacheKey, MetadataInterface $metad // Arrays are always traversed, independent of the specified // traversal strategy // (BC with Symfony < 2.5) - $this->cascadeCollection( + $this->validateEachObjectIn( $value, $propertyPath, $cascadedGroups, - $traversalStrategy, + $traversalStrategy & TraversalStrategy::STOP_RECURSION, $context ); @@ -488,7 +690,7 @@ private function validateGenericNode($value, $cacheKey, MetadataInterface $metad // 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( + $this->validateObject( $value, $propertyPath, $cascadedGroups, @@ -498,151 +700,36 @@ private function validateGenericNode($value, $cacheKey, MetadataInterface $metad // 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 + // objects will always be handled within validateObject() and there's // nothing more to do here. // see GenericMetadata::addConstraint() } /** - * Executes the cascading logic for an object. + * Sequentially validates a node's value in each group of a group sequence. * - * If class metadata is available for the object, a class node is put on - * the node stack. Otherwise, if the selected traversal strategy allows - * traversal of the object, a new collection node is put on the stack. - * Otherwise, an exception is thrown. - * - * @param object $container The object to cascade - * @param string $propertyPath The current property path - * @param string[] $groups The validated groups - * @param integer $traversalStrategy The strategy for traversing the - * cascaded object - * @param ExecutionContextInterface $context The current execution context + * If any of the constraints generates a violation, subsequent groups in the + * group sequence are skipped. * - * @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} + * @param mixed $value The validated value + * @param object|null $object The current object + * @param string $cacheKey The key for caching + * the validated value + * @param MetadataInterface $metadata The metadata of the + * value + * @param string $propertyPath The property path leading + * to the value + * @param integer $traversalStrategy The strategy used for + * traversing the value + * @param GroupSequence $groupSequence The group sequence + * @param string[]|null $cascadedGroup The group that should + * be passed to cascaded + * objects instead of + * the group sequence + * @param ExecutionContextInterface $context The execution context */ - private function cascadeObject($container, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) - { - try { - $classMetadata = $this->metadataFactory->getMetadataFor($container); - - if (!$classMetadata instanceof ClassMetadataInterface) { - throw new UnsupportedMetadataException(sprintf( - 'The metadata factory should return instances of '. - '"Symfony\Component\Validator\Mapping\ClassMetadataInterface", '. - 'got: "%s".', - is_object($classMetadata) ? get_class($classMetadata) : gettype($classMetadata) - )); - } - - $this->validateClassNode( - $container, - spl_object_hash($container), - $classMetadata, - $propertyPath, - $groups, - null, - $traversalStrategy, - $context - ); - } catch (NoSuchMetadataException $e) { - // Rethrow if not Traversable - if (!$container instanceof \Traversable) { - throw $e; - } - - // Rethrow unless IMPLICIT or TRAVERSE - if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) { - throw $e; - } - - $this->cascadeCollection( - $container, - $propertyPath, - $groups, - $traversalStrategy, - $context - ); - } - } - - /** - * Traverses a collection node. - * - * 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 cascadeCollection($collection, $propertyPath, array $groups, $traversalStrategy, ExecutionContextInterface $context) - { - if ($traversalStrategy & TraversalStrategy::STOP_RECURSION) { - $traversalStrategy = TraversalStrategy::NONE; - } else { - $traversalStrategy = TraversalStrategy::IMPLICIT; - } - - foreach ($collection as $key => $value) { - if (is_array($value)) { - // Arrays are always cascaded, independent of the specified - // traversal strategy - // (BC with Symfony < 2.5) - $this->cascadeCollection( - $value, - $propertyPath.'['.$key.']', - $groups, - $traversalStrategy, - $context - ); - - continue; - } - - // Scalar and null values in the collection are ignored - // (BC with Symfony < 2.5) - if (is_object($value)) { - $this->cascadeObject( - $value, - $propertyPath.'['.$key.']', - $groups, - $traversalStrategy, - $context - ); - } - } - } - - /** - * 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($value, $cacheKey, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) + private function stepThroughGroupSequence($value, $object, $cacheKey, MetadataInterface $metadata = null, $propertyPath, $traversalStrategy, GroupSequence $groupSequence, $cascadedGroup, ExecutionContextInterface $context) { $violationCount = count($context->getViolations()); $cascadedGroups = $cascadedGroup ? array($cascadedGroup) : null; @@ -664,6 +751,7 @@ private function stepThroughGroupSequence($value, $cacheKey, MetadataInterface $ } else { $this->validateGenericNode( $value, + $object, $cacheKey, $metadata, $propertyPath, @@ -684,13 +772,12 @@ private function stepThroughGroupSequence($value, $cacheKey, MetadataInterface $ /** * Validates a node's value against all constraints in the given group. * - * @param Node $node The validated node + * @param mixed $value The validated value + * @param string $cacheKey The key for caching the + * validated value + * @param MetadataInterface $metadata The metadata of the value * @param string $group The group to validate * @param ExecutionContextInterface $context The execution context - * @param string $containerHash The hash of the node's - * object (if any) - * - * @throws \Exception */ private function validateInGroup($value, $cacheKey, MetadataInterface $metadata, $group, ExecutionContextInterface $context) { From 1b111d0e831111c3499ef7975da9f18f0c8b5206 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Mar 2014 11:42:12 +0100 Subject: [PATCH 78/86] [Validator] Fixed typos pointed out by @cordoval --- src/Symfony/Component/Validator/ConstraintViolation.php | 2 +- .../Component/Validator/Constraints/GroupSequence.php | 8 ++++---- .../Validator/Mapping/ClassMetadataInterface.php | 2 -- .../Component/Validator/Mapping/GenericMetadata.php | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/Validator/ConstraintViolation.php b/src/Symfony/Component/Validator/ConstraintViolation.php index 41f57650a13b8..fa8d70b9543cc 100644 --- a/src/Symfony/Component/Validator/ConstraintViolation.php +++ b/src/Symfony/Component/Validator/ConstraintViolation.php @@ -72,7 +72,7 @@ class ConstraintViolation implements ConstraintViolationInterface * @param mixed $invalidValue The invalid value that caused this * violation * @param integer|null $plural The number for determining the plural - * form when translation the message + * form when translating the message * @param mixed $code The error code of the violation */ public function __construct($message, $messageTemplate, array $parameters, $root, $propertyPath, $invalidValue, $plural = null, $code = null) diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequence.php b/src/Symfony/Component/Validator/Constraints/GroupSequence.php index af3b86c1c6fb7..805aa1b16b56d 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequence.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequence.php @@ -19,14 +19,14 @@ * When validating a group sequence, each group will only be validated if all * of the previous groups in the sequence succeeded. For example: * - * $validator->validate($address, new Valid(), new GroupSequence('Basic', 'Strict')); + * $validator->validate($address, null, new GroupSequence('Basic', 'Strict')); * * In the first step, all constraints that belong to the group "Basic" will be * validated. If none of the constraints fail, the validator will then validate * the constraints in group "Strict". This is useful, for example, if "Strict" * contains expensive checks that require a lot of CPU or slow, external * services. You usually don't want to run expensive checks if any of the cheap - * checks fails. + * checks fail. * * When adding metadata to a class, you can override the "Default" group of * that class with a group sequence: @@ -42,12 +42,12 @@ * Whenever you validate that object in the "Default" group, the group sequence * will be validated: * - * $validator->validate($address, new Valid()); + * $validator->validate($address); * * If you want to execute the constraints of the "Default" group for a class * with an overridden default group, pass the class name as group name instead: * - * $validator->validate($address, new Valid(), "Address") + * $validator->validate($address, null, "Address") * * @Annotation * diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php index 0e0d2448d7c59..332f5fa1c86cb 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php @@ -14,8 +14,6 @@ use Symfony\Component\Validator\ClassBasedInterface; use Symfony\Component\Validator\PropertyMetadataContainerInterface as LegacyPropertyMetadataContainerInterface; -; - /** * Stores all metadata needed for validating objects of specific class. * diff --git a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php index 01b3d5a403f04..33b40fbd369c6 100644 --- a/src/Symfony/Component/Validator/Mapping/GenericMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/GenericMetadata.php @@ -26,7 +26,7 @@ * @since 2.5 * @author Bernhard Schussek */ -class GenericMetadata implements MetadataInterface +class GenericMetadata implements MetadataInterface { /** * @var Constraint[] From 0946dbe7a0640f374b491e6ceadcb344acbdae15 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Mar 2014 12:31:30 +0100 Subject: [PATCH 79/86] [Validator] Adapted CHANGELOG --- src/Symfony/Component/Validator/CHANGELOG.md | 51 +++++- .../Validator/Context/ExecutionContext.php | 4 +- .../Component/Validator/ExecutionContext.php | 16 +- .../Validator/ExecutionContextInterface.php | 26 +-- .../Mapping/BlackholeMetadataFactory.php | 28 +-- .../Mapping/ClassMetadataFactory.php | 150 +--------------- .../Factory/BlackHoleMetadataFactory.php | 40 +++++ .../Mapping/Factory/LazyMetadataFactory.php | 165 ++++++++++++++++++ .../Factory/MetadataFactoryInterface.php | 24 +++ .../Validator/MetadataFactoryInterface.php | 3 + .../Component/Validator/ValidatorBuilder.php | 4 - 11 files changed, 316 insertions(+), 195 deletions(-) create mode 100644 src/Symfony/Component/Validator/Mapping/Factory/BlackHoleMetadataFactory.php create mode 100644 src/Symfony/Component/Validator/Mapping/Factory/LazyMetadataFactory.php create mode 100644 src/Symfony/Component/Validator/Mapping/Factory/MetadataFactoryInterface.php diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 005526215beae..57d6b17bf562d 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -7,8 +7,57 @@ CHANGELOG * deprecated `ApcCache` in favor of `DoctrineCache` * added `DoctrineCache` to adapt any Doctrine cache * `GroupSequence` now implements `ArrayAccess`, `Countable` and `Traversable` - * changed `ClassMetadata::getGroupSequence()` to return a `GroupSequence` instance instead of an array + * [BC BREAK] changed `ClassMetadata::getGroupSequence()` to return a `GroupSequence` instance instead of an array * `Callback` can now be put onto properties (useful when you pass a closure to the constraint) + * deprecated `ClassBasedInterface` + * deprecated `MetadataInterface` + * deprecated `PropertyMetadataInterface` + * deprecated `PropertyMetadataContainerInterface` + * deprecated `Mapping\ElementMetadata` + * added `Mapping\MetadataInterface` + * added `Mapping\ClassMetadataInterface` + * added `Mapping\PropertyMetadataInterface` + * added `Mapping\GenericMetadata` + * added `Mapping\CascadingStrategy` + * added `Mapping\TraversalStrategy` + * deprecated `Mapping\ClassMetadata::accept()` + * deprecated `Mapping\MemberMetadata::accept()` + * removed array type hint of `Mapping\ClassMetadata::setGroupSequence()` + * deprecated `MetadataFactoryInterface` + * deprecated `Mapping\BlackholeMetadataFactory` + * deprecated `Mapping\ClassMetadataFactory` + * added `Mapping\Factory\MetadataFactoryInterface` + * added `Mapping\Factory\BlackHoleMetadataFactory` + * added `Mapping\Factory\LazyMetadataFactory` + * deprecated `ExecutionContextInterface` + * deprecated `ExecutionContext` + * deprecated `GlobalExecutionContextInterface` + * added `Context\ExecutionContextInterface` + * added `Context\ExecutionContext` + * added `Context\ExecutionContextFactoryInterface` + * added `Context\ExecutionContextFactory` + * deprecated `ValidatorInterface` + * deprecated `Validator` + * deprecated `ValidationVisitorInterface` + * deprecated `ValidationVisitor` + * added `Validator\ValidatorInterface` + * added `Validator\RecursiveValidator` + * added `Validator\ContextualValidatorInterface` + * added `Validator\RecursiveContextualValidator` + * added `Violation\ConstraintViolationBuilderInterface` + * added `Violation\ConstraintViolationBuilder` + * added `ConstraintViolation::getParameters()` + * added `ConstraintViolation::getPlural()` + * added `Constraints\Traverse` + * deprecated `$deep` property in `Constraints\Valid` + * added `ValidatorBuilderInterface::setApiVersion()` + * added `Validation::API_VERSION_2_4` + * added `Validation::API_VERSION_2_5` + * added `Exception\OutOfBoundsException` + * added `Exception\UnsupportedMetadataException` + * made `Exception\ValidatorException` extend `Exception\RuntimeException` + * added `Util\PropertyPath` + 2.4.0 ----- diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index c345e4115fd5b..beeef001e42c1 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -158,7 +158,7 @@ public function setGroup($group) /** * {@inheritdoc} */ - public function addViolation($message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) + public function addViolation($message, array $parameters = array(), $invalidValue = null, $plural = null, $code = null) { // The parameters $invalidValue and following are ignored by the new // API, as they are not present in the new interface anymore. @@ -275,7 +275,7 @@ public function getPropertyPath($subPath = '') /** * {@inheritdoc} */ - public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $plural = null, $code = null) { throw new BadMethodCallException( 'addViolationAt() is not supported anymore as of Symfony 2.5. '. diff --git a/src/Symfony/Component/Validator/ExecutionContext.php b/src/Symfony/Component/Validator/ExecutionContext.php index 6435bbf9d0568..5407744bb7093 100644 --- a/src/Symfony/Component/Validator/ExecutionContext.php +++ b/src/Symfony/Component/Validator/ExecutionContext.php @@ -90,13 +90,13 @@ public function __construct(GlobalExecutionContextInterface $globalContext, Tran /** * {@inheritdoc} */ - public function addViolation($message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) + public function addViolation($message, array $params = array(), $invalidValue = null, $plural = null, $code = null) { - if (null === $pluralization) { + if (null === $plural) { $translatedMessage = $this->translator->trans($message, $params, $this->translationDomain); } else { try { - $translatedMessage = $this->translator->transChoice($message, $pluralization, $params, $this->translationDomain); + $translatedMessage = $this->translator->transChoice($message, $plural, $params, $this->translationDomain); } catch (\InvalidArgumentException $e) { $translatedMessage = $this->translator->trans($message, $params, $this->translationDomain); } @@ -110,7 +110,7 @@ public function addViolation($message, array $params = array(), $invalidValue = $this->propertyPath, // check using func_num_args() to allow passing null values func_num_args() >= 3 ? $invalidValue : $this->value, - $pluralization, + $plural, $code )); } @@ -118,19 +118,19 @@ public function addViolation($message, array $params = array(), $invalidValue = /** * {@inheritdoc} */ - public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null) + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $plural = null, $code = null) { $this->globalContext->getViolations()->add(new ConstraintViolation( - null === $pluralization + null === $plural ? $this->translator->trans($message, $parameters, $this->translationDomain) - : $this->translator->transChoice($message, $pluralization, $parameters, $this->translationDomain), + : $this->translator->transChoice($message, $plural, $parameters, $this->translationDomain), $message, $parameters, $this->globalContext->getRoot(), $this->getPropertyPath($subPath), // check using func_num_args() to allow passing null values func_num_args() >= 4 ? $invalidValue : $this->value, - $pluralization, + $plural, $code )); } diff --git a/src/Symfony/Component/Validator/ExecutionContextInterface.php b/src/Symfony/Component/Validator/ExecutionContextInterface.php index b89caa2deaee2..3705bc43102ac 100644 --- a/src/Symfony/Component/Validator/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/ExecutionContextInterface.php @@ -91,11 +91,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 mixed $invalidValue The invalid, validated value. - * @param integer|null $pluralization The number to use to pluralize of the message. - * @param integer|null $code The violation code. + * @param string $message The error message + * @param array $params The parameters substituted in the error message + * @param mixed $invalidValue The invalid, validated value + * @param integer|null $plural The number to use to pluralize of the message + * @param integer|null $code The violation code * * @api * @@ -103,18 +103,18 @@ interface ExecutionContextInterface * deprecated since version 2.5 and will be removed in * Symfony 3.0. */ - public function addViolation($message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null); + public function addViolation($message, array $params = array(), $invalidValue = null, $plural = null, $code = null); /** * Adds a violation at the validation graph node with the given property * path relative to the current property path. * - * @param string $subPath The relative property path for the violation. - * @param string $message The error message. - * @param array $parameters The parameters substituted in the error message. - * @param mixed $invalidValue The invalid, validated value. - * @param integer|null $pluralization The number to use to pluralize of the message. - * @param integer|null $code The violation code. + * @param string $subPath The relative property path for the violation + * @param string $message The error message + * @param array $parameters The parameters substituted in the error message + * @param mixed $invalidValue The invalid, validated value + * @param integer|null $plural The number to use to pluralize of the message + * @param integer|null $code The violation code * * @api * @@ -122,7 +122,7 @@ public function addViolation($message, array $params = array(), $invalidValue = * Use {@link Context\ExecutionContextInterface::buildViolation()} * instead. */ - public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $pluralization = null, $code = null); + public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = null, $plural = null, $code = null); /** * Validates the given value within the scope of the current validation. diff --git a/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php index 28eaa5f02def4..7913e15625499 100644 --- a/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/BlackholeMetadataFactory.php @@ -11,32 +11,14 @@ namespace Symfony\Component\Validator\Mapping; -use Symfony\Component\Validator\MetadataFactoryInterface; - /** - * Metadata factory that does not store metadata. - * - * This implementation is useful if you want to validate values against - * constraints only and you don't need to add constraints to classes and - * properties. + * Alias of {@link Factory\BlackHoleMetadataFactory}. * * @author Fabien Potencier + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Factory\BlackHoleMetadataFactory} instead. */ -class BlackholeMetadataFactory implements MetadataFactoryInterface +class BlackholeMetadataFactory extends \Symfony\Component\Validator\Mapping\Factory\BlackHoleMetadataFactory { - /** - * {@inheritdoc} - */ - public function getMetadataFor($value) - { - throw new \LogicException('This class does not support metadata.'); - } - - /** - * {@inheritdoc} - */ - public function hasMetadataFor($value) - { - return false; - } } diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php index 8c26b7acecaba..956b798ed7635 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataFactory.php @@ -11,154 +11,16 @@ namespace Symfony\Component\Validator\Mapping; -use Symfony\Component\Validator\Exception\NoSuchMetadataException; -use Symfony\Component\Validator\Mapping\Cache\CacheInterface; -use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; -use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\Mapping\Factory\LazyMetadataFactory; /** - * Creates new {@link ClassMetadataInterface} instances. - * - * Whenever {@link getMetadataFor()} is called for the first time with a given - * class name or object of that class, a new metadata instance is created and - * returned. On subsequent requests for the same class, the same metadata - * instance will be returned. - * - * You can optionally pass a {@link LoaderInterface} instance to the constructor. - * Whenever a new metadata instance is created, it is passed to the loader, - * which can configure the metadata based on configuration loaded from the - * filesystem or a database. If you want to use multiple loaders, wrap them in a - * {@link Loader\LoaderChain}. - * - * You can also optionally pass a {@link CacheInterface} instance to the - * constructor. This cache will be used for persisting the generated metadata - * between multiple PHP requests. + * Alias of {@link LazyMetadataFactory}. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link LazyMetadataFactory} instead. */ -class ClassMetadataFactory implements MetadataFactoryInterface +class ClassMetadataFactory extends LazyMetadataFactory { - /** - * The loader for loading the class metadata - * - * @var LoaderInterface - */ - protected $loader; - - /** - * The cache for caching class metadata - * - * @var CacheInterface - */ - protected $cache; - - /** - * The loaded metadata, indexed by class name - * - * @var ClassMetadata[] - */ - protected $loadedClasses = array(); - - /** - * Creates a new metadata factory. - * - * @param LoaderInterface|null $loader The loader for configuring new metadata - * @param CacheInterface|null $cache The cache for persisting metadata - * between multiple PHP requests - */ - public function __construct(LoaderInterface $loader = null, CacheInterface $cache = null) - { - $this->loader = $loader; - $this->cache = $cache; - } - - /** - * Returns the metadata for the given class name or object. - * - * If the method was called with the same class name (or an object of that - * class) before, the same metadata instance is returned. - * - * If the factory was configured with a cache, this method will first look - * for an existing metadata instance in the cache. If an existing instance - * is found, it will be returned without further ado. - * - * Otherwise, a new metadata instance is created. If the factory was - * configured with a loader, the metadata is passed to the - * {@link LoaderInterface::loadClassMetadata()} method for further - * configuration. At last, the new object is returned. - * - * @param string|object $value A class name or an object - * - * @return MetadataInterface The metadata for the value - * - * @throws NoSuchMetadataException If no metadata exists for the given value - */ - public function getMetadataFor($value) - { - if (!is_object($value) && !is_string($value)) { - throw new NoSuchMetadataException(sprintf('Cannot create metadata for non-objects. Got: %s', gettype($value))); - } - - $class = ltrim(is_object($value) ? get_class($value) : $value, '\\'); - - if (isset($this->loadedClasses[$class])) { - return $this->loadedClasses[$class]; - } - - if (null !== $this->cache && false !== ($this->loadedClasses[$class] = $this->cache->read($class))) { - return $this->loadedClasses[$class]; - } - - if (!class_exists($class) && !interface_exists($class)) { - throw new NoSuchMetadataException(sprintf('The class or interface "%s" does not exist.', $class)); - } - - $metadata = new ClassMetadata($class); - - // Include constraints from the parent class - if ($parent = $metadata->getReflectionClass()->getParentClass()) { - $metadata->mergeConstraints($this->getMetadataFor($parent->name)); - } - - // Include constraints from all implemented interfaces - foreach ($metadata->getReflectionClass()->getInterfaces() as $interface) { - if ('Symfony\Component\Validator\GroupSequenceProviderInterface' === $interface->name) { - continue; - } - $metadata->mergeConstraints($this->getMetadataFor($interface->name)); - } - - if (null !== $this->loader) { - $this->loader->loadClassMetadata($metadata); - } - - if (null !== $this->cache) { - $this->cache->write($metadata); - } - - return $this->loadedClasses[$class] = $metadata; - } - - /** - * Returns whether the factory is able to return metadata for the given - * class name or object. - * - * @param string|object $value A class name or an object - * - * @return Boolean Whether metadata can be returned for that class - */ - public function hasMetadataFor($value) - { - if (!is_object($value) && !is_string($value)) { - return false; - } - - $class = ltrim(is_object($value) ? get_class($value) : $value, '\\'); - - if (class_exists($class) || interface_exists($class)) { - return true; - } - - return false; - } } diff --git a/src/Symfony/Component/Validator/Mapping/Factory/BlackHoleMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/Factory/BlackHoleMetadataFactory.php new file mode 100644 index 0000000000000..5b38d0c98a775 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/Factory/BlackHoleMetadataFactory.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping\Factory; + +/** + * Metadata factory that does not store metadata. + * + * This implementation is useful if you want to validate values against + * constraints only and you don't need to add constraints to classes and + * properties. + * + * @author Fabien Potencier + */ +class BlackHoleMetadataFactory implements MetadataFactoryInterface +{ + /** + * {@inheritdoc} + */ + public function getMetadataFor($value) + { + throw new \LogicException('This class does not support metadata.'); + } + + /** + * {@inheritdoc} + */ + public function hasMetadataFor($value) + { + return false; + } +} diff --git a/src/Symfony/Component/Validator/Mapping/Factory/LazyMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/Factory/LazyMetadataFactory.php new file mode 100644 index 0000000000000..ef069b61c1d70 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/Factory/LazyMetadataFactory.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping\Factory; + +use Symfony\Component\Validator\Exception\NoSuchMetadataException; +use Symfony\Component\Validator\Mapping\Cache\CacheInterface; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; +use Symfony\Component\Validator\Mapping\MetadataInterface; + +/** + * Creates new {@link ClassMetadataInterface} instances. + * + * Whenever {@link getMetadataFor()} is called for the first time with a given + * class name or object of that class, a new metadata instance is created and + * returned. On subsequent requests for the same class, the same metadata + * instance will be returned. + * + * You can optionally pass a {@link LoaderInterface} instance to the constructor. + * Whenever a new metadata instance is created, it is passed to the loader, + * which can configure the metadata based on configuration loaded from the + * filesystem or a database. If you want to use multiple loaders, wrap them in a + * {@link Loader\LoaderChain}. + * + * You can also optionally pass a {@link CacheInterface} instance to the + * constructor. This cache will be used for persisting the generated metadata + * between multiple PHP requests. + * + * @author Bernhard Schussek + */ +class LazyMetadataFactory implements MetadataFactoryInterface +{ + /** + * The loader for loading the class metadata + * + * @var LoaderInterface + */ + protected $loader; + + /** + * The cache for caching class metadata + * + * @var CacheInterface + */ + protected $cache; + + /** + * The loaded metadata, indexed by class name + * + * @var ClassMetadata[] + */ + protected $loadedClasses = array(); + + /** + * Creates a new metadata factory. + * + * @param LoaderInterface|null $loader The loader for configuring new metadata + * @param CacheInterface|null $cache The cache for persisting metadata + * between multiple PHP requests + */ + public function __construct(LoaderInterface $loader = null, CacheInterface $cache = null) + { + $this->loader = $loader; + $this->cache = $cache; + } + + /** + * Returns the metadata for the given class name or object. + * + * If the method was called with the same class name (or an object of that + * class) before, the same metadata instance is returned. + * + * If the factory was configured with a cache, this method will first look + * for an existing metadata instance in the cache. If an existing instance + * is found, it will be returned without further ado. + * + * Otherwise, a new metadata instance is created. If the factory was + * configured with a loader, the metadata is passed to the + * {@link LoaderInterface::loadClassMetadata()} method for further + * configuration. At last, the new object is returned. + * + * @param string|object $value A class name or an object + * + * @return MetadataInterface The metadata for the value + * + * @throws NoSuchMetadataException If no metadata exists for the given value + */ + public function getMetadataFor($value) + { + if (!is_object($value) && !is_string($value)) { + throw new NoSuchMetadataException(sprintf('Cannot create metadata for non-objects. Got: %s', gettype($value))); + } + + $class = ltrim(is_object($value) ? get_class($value) : $value, '\\'); + + if (isset($this->loadedClasses[$class])) { + return $this->loadedClasses[$class]; + } + + if (null !== $this->cache && false !== ($this->loadedClasses[$class] = $this->cache->read($class))) { + return $this->loadedClasses[$class]; + } + + if (!class_exists($class) && !interface_exists($class)) { + throw new NoSuchMetadataException(sprintf('The class or interface "%s" does not exist.', $class)); + } + + $metadata = new ClassMetadata($class); + + // Include constraints from the parent class + if ($parent = $metadata->getReflectionClass()->getParentClass()) { + $metadata->mergeConstraints($this->getMetadataFor($parent->name)); + } + + // Include constraints from all implemented interfaces + foreach ($metadata->getReflectionClass()->getInterfaces() as $interface) { + if ('Symfony\Component\Validator\GroupSequenceProviderInterface' === $interface->name) { + continue; + } + $metadata->mergeConstraints($this->getMetadataFor($interface->name)); + } + + if (null !== $this->loader) { + $this->loader->loadClassMetadata($metadata); + } + + if (null !== $this->cache) { + $this->cache->write($metadata); + } + + return $this->loadedClasses[$class] = $metadata; + } + + /** + * Returns whether the factory is able to return metadata for the given + * class name or object. + * + * @param string|object $value A class name or an object + * + * @return Boolean Whether metadata can be returned for that class + */ + public function hasMetadataFor($value) + { + if (!is_object($value) && !is_string($value)) { + return false; + } + + $class = ltrim(is_object($value) ? get_class($value) : $value, '\\'); + + if (class_exists($class) || interface_exists($class)) { + return true; + } + + return false; + } +} diff --git a/src/Symfony/Component/Validator/Mapping/Factory/MetadataFactoryInterface.php b/src/Symfony/Component/Validator/Mapping/Factory/MetadataFactoryInterface.php new file mode 100644 index 0000000000000..ef25174d0ecc2 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/Factory/MetadataFactoryInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping\Factory; + +use Symfony\Component\Validator\MetadataFactoryInterface as LegacyMetadataFactoryInterface; + +/** + * Returns {@link MetadataInterface} instances for values. + * + * @since 2.5 + * @author Bernhard Schussek + */ +interface MetadataFactoryInterface extends LegacyMetadataFactoryInterface +{ +} diff --git a/src/Symfony/Component/Validator/MetadataFactoryInterface.php b/src/Symfony/Component/Validator/MetadataFactoryInterface.php index 40074556c00b4..b025f19ddddd4 100644 --- a/src/Symfony/Component/Validator/MetadataFactoryInterface.php +++ b/src/Symfony/Component/Validator/MetadataFactoryInterface.php @@ -15,6 +15,9 @@ * Returns {@link MetadataInterface} instances for values. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link Mapping\Factory\MetadataFactoryInterface} instead. */ interface MetadataFactoryInterface { diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index 8250ea6684724..1611d9b387df0 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -31,12 +31,8 @@ use Symfony\Component\Validator\Mapping\Loader\XmlFilesLoader; use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader; use Symfony\Component\Validator\Mapping\Loader\YamlFilesLoader; -use Symfony\Component\Validator\NodeTraverser\NonRecursiveNodeTraverser; -use Symfony\Component\Validator\NodeVisitor\NodeValidationVisitor; -use Symfony\Component\Validator\NodeVisitor\ObjectInitializationVisitor; use Symfony\Component\Validator\Validator\LegacyValidator; use Symfony\Component\Validator\Validator\RecursiveValidator; -use Symfony\Component\Validator\Validator\TraversingValidator; use Symfony\Component\Validator\Validator as ValidatorV24; /** From 9b204c93544532ebac27e6b500de22a2599fab7e Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Tue, 18 Mar 2014 17:36:12 +0100 Subject: [PATCH 80/86] [FrameworkBundle] Implemented configuration to select the desired Validator API --- .../Compiler/AddValidatorInitializersPass.php | 6 +- .../DependencyInjection/Configuration.php | 17 ++++ .../FrameworkExtension.php | 47 +++++++-- .../Resources/config/schema/symfony-1.0.xsd | 14 +++ .../Resources/config/validator.xml | 56 ++++------- .../DependencyInjection/ConfigurationTest.php | 2 + .../Fixtures/php/validation_2_4_api.php | 9 ++ .../validation_multiple_static_methods.php | 9 ++ .../php/validation_no_static_method.php | 9 ++ .../Fixtures/xml/validation_2_4_api.xml | 12 +++ .../validation_multiple_static_methods.xml | 15 +++ .../xml/validation_no_static_method.xml | 12 +++ .../Fixtures/yml/validation_2_4_api.yml | 5 + .../validation_multiple_static_methods.yml | 5 + .../yml/validation_no_static_method.yml | 5 + .../FrameworkExtensionTest.php | 95 ++++++++++++++----- .../Validator/Tests/ValidatorBuilderTest.php | 2 +- .../Component/Validator/Validation.php | 6 ++ src/Symfony/Component/Validator/Validator.php | 12 ++- .../Validator/ValidatorInterface.php | 23 +---- .../Component/Validator/ValidatorBuilder.php | 6 +- 21 files changed, 264 insertions(+), 103 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_2_4_api.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_multiple_static_methods.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_no_static_method.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_2_4_api.xml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_multiple_static_methods.xml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_no_static_method.xml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_2_4_api.yml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_multiple_static_methods.yml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_no_static_method.yml diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddValidatorInitializersPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddValidatorInitializersPass.php index bf9f33811199a..6f58fc21bebf1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddValidatorInitializersPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddValidatorInitializersPass.php @@ -19,15 +19,17 @@ class AddValidatorInitializersPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { - if (!$container->hasDefinition('validator')) { + if (!$container->hasDefinition('validator.builder')) { return; } + $validatorBuilder = $container->getDefinition('validator.builder'); + $initializers = array(); foreach ($container->findTaggedServiceIds('validator.initializer') as $id => $attributes) { $initializers[] = new Reference($id); } - $container->getDefinition('validator')->replaceArgument(4, $initializers); + $validatorBuilder->addMethodCall('addObjectInitializers', array($initializers)); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index a317d3cc8efe1..c2639295636c6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -444,8 +444,25 @@ private function addValidationSection(ArrayNodeDefinition $rootNode) ->children() ->scalarNode('cache')->end() ->booleanNode('enable_annotations')->defaultFalse()->end() + ->arrayNode('static_method') + ->defaultValue(array('loadClassMetadata')) + ->prototype('scalar')->end() + ->treatFalseLike(array()) + ->validate() + ->ifTrue(function ($v) { return !is_array($v); }) + ->then(function ($v) { return (array) $v; }) + ->end() + ->end() ->scalarNode('translation_domain')->defaultValue('validators')->end() ->booleanNode('strict_email')->defaultFalse()->end() + ->enumNode('api') + ->values(array('2.4', '2.5', '2.5-bc', 'auto')) + ->defaultValue('auto') + ->beforeNormalization() + ->ifTrue(function ($v) { return is_scalar($v); }) + ->then(function ($v) { return (string) $v; }) + ->end() + ->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 5ee6982bc8d1f..94516e0463be0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -21,6 +21,7 @@ use Symfony\Component\Finder\Finder; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Config\FileLocator; +use Symfony\Component\Validator\Validation; /** * FrameworkExtension. @@ -674,27 +675,57 @@ private function registerValidationConfiguration(array $config, ContainerBuilder $loader->load('validator.xml'); + $validatorBuilder = $container->getDefinition('validator.builder'); + $container->setParameter('validator.translation_domain', $config['translation_domain']); - $container->setParameter('validator.mapping.loader.xml_files_loader.mapping_files', $this->getValidatorXmlMappingFiles($container)); - $container->setParameter('validator.mapping.loader.yaml_files_loader.mapping_files', $this->getValidatorYamlMappingFiles($container)); + + $xmlMappings = $this->getValidatorXmlMappingFiles($container); + $yamlMappings = $this->getValidatorYamlMappingFiles($container); + + if (count($xmlMappings) > 0) { + $validatorBuilder->addMethodCall('addXmlMappings', array($xmlMappings)); + } + + if (count($yamlMappings) > 0) { + $validatorBuilder->addMethodCall('addYamlMappings', array($yamlMappings)); + } $definition = $container->findDefinition('validator.email'); $definition->replaceArgument(0, $config['strict_email']); if (array_key_exists('enable_annotations', $config) && $config['enable_annotations']) { - $loaderChain = $container->getDefinition('validator.mapping.loader.loader_chain'); - $arguments = $loaderChain->getArguments(); - array_unshift($arguments[0], new Reference('validator.mapping.loader.annotation_loader')); - $loaderChain->setArguments($arguments); + $validatorBuilder->addMethodCall('enableAnnotations', array(new Reference('annotation_reader'))); + } + + if (array_key_exists('static_method', $config) && $config['static_method']) { + foreach ($config['static_method'] as $methodName) { + $validatorBuilder->addMethodCall('addMethodMapping', array($methodName)); + } } if (isset($config['cache'])) { - $container->getDefinition('validator.mapping.class_metadata_factory') - ->replaceArgument(1, new Reference('validator.mapping.cache.'.$config['cache'])); $container->setParameter( 'validator.mapping.cache.prefix', 'validator_'.hash('sha256', $container->getParameter('kernel.root_dir')) ); + + $validatorBuilder->addMethodCall('setCache', array(new Reference('validator.mapping.cache.'.$config['cache']))); + } + + if ('auto' !== $config['api']) { + switch ($config['api']) { + case '2.4': + $api = Validation::API_VERSION_2_4; + break; + case '2.5': + $api = Validation::API_VERSION_2_5; + break; + default: + $api = Validation::API_VERSION_2_5_BC; + break; + } + + $validatorBuilder->addMethodCall('setApiVersion', array($api)); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index fcc9e254589da..639b98d6f40a8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -7,6 +7,14 @@ + + + + + + + + @@ -151,9 +159,15 @@ + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml index 930300f75c5d1..bf54f92603229 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml @@ -5,36 +5,34 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - Symfony\Component\Validator\Validator + Symfony\Component\Validator\ValidatorInterface + Symfony\Component\Validator\ValidatorBuilderInterface + Symfony\Component\Validator\Validation Symfony\Component\Validator\Mapping\ClassMetadataFactory Symfony\Component\Validator\Mapping\Cache\ApcCache - Symfony\Component\Validator\Mapping\Loader\LoaderChain - Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader - Symfony\Component\Validator\Mapping\Loader\AnnotationLoader - Symfony\Component\Validator\Mapping\Loader\XmlFilesLoader - Symfony\Component\Validator\Mapping\Loader\YamlFilesLoader Symfony\Bundle\FrameworkBundle\Validator\ConstraintValidatorFactory - - Symfony\Component\Validator\Constraints\ExpressionValidator Symfony\Component\Validator\Constraints\EmailValidator - - - - - %validator.translation_domain% - - + -