From ce04073b4e9e2cdc6a757e137f0f4f9a3775d0dc Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Thu, 17 Jul 2014 16:35:52 +0200 Subject: [PATCH] [Validator] Fixed object initializers in 2.5 version of the Validator --- .../Validator/Context/ExecutionContext.php | 40 +++++++++++++++ .../Validator/Tests/Fixtures/Entity.php | 1 + .../Tests/Validator/Abstract2Dot5ApiTest.php | 50 ++++++++++++++++++- .../Tests/Validator/AbstractLegacyApiTest.php | 48 +++++++++++++++++- .../Validator/LegacyValidator2Dot5ApiTest.php | 4 +- .../LegacyValidatorLegacyApiTest.php | 4 +- .../RecursiveValidator2Dot5ApiTest.php | 4 +- .../Validator/Tests/ValidatorTest.php | 4 +- .../Component/Validator/ValidationVisitor.php | 17 ++++--- .../RecursiveContextualValidator.php | 30 ++++++++--- .../Validator/RecursiveValidator.php | 30 +++++++---- .../Component/Validator/ValidatorBuilder.php | 4 +- 12 files changed, 200 insertions(+), 36 deletions(-) diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index a02c587706..542ea33658 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -18,6 +18,7 @@ use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Exception\BadMethodCallException; use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; +use Symfony\Component\Validator\ObjectInitializerInterface; use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; @@ -113,6 +114,13 @@ class ExecutionContext implements ExecutionContextInterface */ private $validatedConstraints = array(); + /** + * Stores which objects have been initialized. + * + * @var array + */ + private $initializedObjects; + /** * Creates a new execution context. * @@ -360,4 +368,36 @@ class ExecutionContext implements ExecutionContextInterface { return isset($this->validatedConstraints[$cacheKey.':'.$constraintHash]); } + + /** + * Marks that an object was initialized. + * + * @param string $cacheKey The hash of the object + * + * @internal Used by the validator engine. Should not be called by user + * code. + * + * @see ObjectInitializerInterface + */ + public function markObjectAsInitialized($cacheKey) + { + $this->initializedObjects[$cacheKey] = true; + } + + /** + * Returns whether an object was initialized. + * + * @param string $cacheKey The hash of the object + * + * @return bool Whether the object was already initialized + * + * @internal Used by the validator engine. Should not be called by user + * code. + * + * @see ObjectInitializerInterface + */ + public function isObjectInitialized($cacheKey) + { + return isset($this->initializedObjects[$cacheKey]); + } } diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/Entity.php b/src/Symfony/Component/Validator/Tests/Fixtures/Entity.php index d841f5dc9d..2230d30424 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/Entity.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/Entity.php @@ -38,6 +38,7 @@ class Entity extends EntityParent implements EntityInterface public $reference2; private $internal; public $data = 'Overridden data'; + public $initialized = false; public function __construct($internal = null) { diff --git a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php index 0b011a4792..02fefda5f0 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/Abstract2Dot5ApiTest.php @@ -42,7 +42,7 @@ abstract class Abstract2Dot5ApiTest extends AbstractValidatorTest * * @return ValidatorInterface */ - abstract protected function createValidator(MetadataFactoryInterface $metadataFactory); + abstract protected function createValidator(MetadataFactoryInterface $metadataFactory, array $objectInitializers = array()); protected function setUp() { @@ -678,4 +678,52 @@ abstract class Abstract2Dot5ApiTest extends AbstractValidatorTest $this->assertTrue($called); } + + public function testInitializeObjectsOnFirstValidation() + { + $test = $this; + $entity = new Entity(); + $entity->initialized = false; + + // prepare initializers that set "initialized" to true + $initializer1 = $this->getMock('Symfony\\Component\\Validator\\ObjectInitializerInterface'); + $initializer2 = $this->getMock('Symfony\\Component\\Validator\\ObjectInitializerInterface'); + + $initializer1->expects($this->once()) + ->method('initialize') + ->with($entity) + ->will($this->returnCallback(function ($object) { + $object->initialized = true; + })); + + $initializer2->expects($this->once()) + ->method('initialize') + ->with($entity); + + $this->validator = $this->createValidator($this->metadataFactory, array( + $initializer1, + $initializer2 + )); + + // prepare constraint which + // * checks that "initialized" is set to true + // * validates the object again + $callback = function ($object, ExecutionContextInterface $context) use ($test) { + $test->assertTrue($object->initialized); + + // validate again in same group + $validator = $context->getValidator()->inContext($context); + + $validator->validate($object); + + // validate again in other group + $validator->validate($object, null, 'SomeGroup'); + }; + + $this->metadata->addConstraint(new Callback($callback)); + + $this->validate($entity); + + $this->assertTrue($entity->initialized); + } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php index 386c250a28..56e94313be 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractLegacyApiTest.php @@ -38,7 +38,7 @@ abstract class AbstractLegacyApiTest extends AbstractValidatorTest * * @return LegacyValidatorInterface */ - abstract protected function createValidator(MetadataFactoryInterface $metadataFactory); + abstract protected function createValidator(MetadataFactoryInterface $metadataFactory, array $objectInitializers = array()); protected function setUp() { @@ -238,6 +238,52 @@ abstract class AbstractLegacyApiTest extends AbstractValidatorTest $this->assertSame('Code', $violations[0]->getCode()); } + public function testInitializeObjectsOnFirstValidation() + { + $test = $this; + $entity = new Entity(); + $entity->initialized = false; + + // prepare initializers that set "initialized" to true + $initializer1 = $this->getMock('Symfony\\Component\\Validator\\ObjectInitializerInterface'); + $initializer2 = $this->getMock('Symfony\\Component\\Validator\\ObjectInitializerInterface'); + + $initializer1->expects($this->once()) + ->method('initialize') + ->with($entity) + ->will($this->returnCallback(function ($object) { + $object->initialized = true; + })); + + $initializer2->expects($this->once()) + ->method('initialize') + ->with($entity); + + $this->validator = $this->createValidator($this->metadataFactory, array( + $initializer1, + $initializer2 + )); + + // prepare constraint which + // * checks that "initialized" is set to true + // * validates the object again + $callback = function ($object, ExecutionContextInterface $context) use ($test) { + $test->assertTrue($object->initialized); + + // validate again in same group + $context->validate($object); + + // validate again in other group + $context->validate($object, '', 'SomeGroup'); + }; + + $this->metadata->addConstraint(new Callback($callback)); + + $this->validate($entity); + + $this->assertTrue($entity->initialized); + } + 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 index 7faeea6284..d468eea5d8 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidator2Dot5ApiTest.php @@ -28,10 +28,10 @@ class LegacyValidator2Dot5ApiTest extends Abstract2Dot5ApiTest parent::setUp(); } - protected function createValidator(MetadataFactoryInterface $metadataFactory) + protected function createValidator(MetadataFactoryInterface $metadataFactory, array $objectInitializers = array()) { $contextFactory = new LegacyExecutionContextFactory($metadataFactory, new DefaultTranslator()); - return new LegacyValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory()); + return new LegacyValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory(), $objectInitializers); } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php index 581e676839..af46ae3bdf 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/LegacyValidatorLegacyApiTest.php @@ -28,10 +28,10 @@ class LegacyValidatorLegacyApiTest extends AbstractLegacyApiTest parent::setUp(); } - protected function createValidator(MetadataFactoryInterface $metadataFactory) + protected function createValidator(MetadataFactoryInterface $metadataFactory, array $objectInitializers = array()) { $contextFactory = new LegacyExecutionContextFactory($metadataFactory, new DefaultTranslator()); - return new LegacyValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory()); + return new LegacyValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory(), $objectInitializers); } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php index da43279638..0fdce741d8 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidator2Dot5ApiTest.php @@ -19,10 +19,10 @@ use Symfony\Component\Validator\Validator\RecursiveValidator; class RecursiveValidator2Dot5ApiTest extends Abstract2Dot5ApiTest { - protected function createValidator(MetadataFactoryInterface $metadataFactory) + protected function createValidator(MetadataFactoryInterface $metadataFactory, array $objectInitializers = array()) { $contextFactory = new ExecutionContextFactory(new DefaultTranslator()); - return new RecursiveValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory()); + return new RecursiveValidator($contextFactory, $metadataFactory, new ConstraintValidatorFactory(), $objectInitializers); } } diff --git a/src/Symfony/Component/Validator/Tests/ValidatorTest.php b/src/Symfony/Component/Validator/Tests/ValidatorTest.php index 0f588fef8d..3cc36f5e23 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorTest.php @@ -21,9 +21,9 @@ use Symfony\Component\Validator\Validator as LegacyValidator; class ValidatorTest extends AbstractLegacyApiTest { - protected function createValidator(MetadataFactoryInterface $metadataFactory) + protected function createValidator(MetadataFactoryInterface $metadataFactory, array $objectInitializers = array()) { - return new LegacyValidator($metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator()); + return new LegacyValidator($metadataFactory, new ConstraintValidatorFactory(), new DefaultTranslator(), 'validators', $objectInitializers); } /** diff --git a/src/Symfony/Component/Validator/ValidationVisitor.php b/src/Symfony/Component/Validator/ValidationVisitor.php index a084c74b60..3af82496a1 100644 --- a/src/Symfony/Component/Validator/ValidationVisitor.php +++ b/src/Symfony/Component/Validator/ValidationVisitor.php @@ -129,16 +129,19 @@ class ValidationVisitor implements ValidationVisitorInterface, GlobalExecutionCo return; } + // Initialize if the object wasn't initialized before + if (!isset($this->validatedObjects[$hash])) { + foreach ($this->objectInitializers as $initializer) { + if (!$initializer instanceof ObjectInitializerInterface) { + throw new \LogicException('Validator initializers must implement ObjectInitializerInterface.'); + } + $initializer->initialize($value); + } + } + // Remember validating this object before starting and possibly // traversing the object graph $this->validatedObjects[$hash][$group] = true; - - foreach ($this->objectInitializers as $initializer) { - if (!$initializer instanceof ObjectInitializerInterface) { - throw new \LogicException('Validator initializers must implement ObjectInitializerInterface.'); - } - $initializer->initialize($value); - } } // Validate arrays recursively by default, otherwise every driver needs diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index d6c1b2500a..3738dab069 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -27,6 +27,7 @@ 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\ObjectInitializerInterface; use Symfony\Component\Validator\Util\PropertyPath; /** @@ -52,23 +53,30 @@ class RecursiveContextualValidator implements ContextualValidatorInterface */ private $validatorFactory; + /** + * @var ObjectInitializerInterface[] + */ + private $objectInitializers; + /** * 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 ConstraintValidatorFactoryInterface $validatorFactory The factory for creating - * constraint validators + * @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 + * @param ObjectInitializerInterface[] $objectInitializers The object initializers */ - public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory) + public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = array()) { $this->context = $context; $this->defaultPropertyPath = $context->getPropertyPath(); $this->defaultGroups = array($context->getGroup() ?: Constraint::DEFAULT_GROUP); $this->metadataFactory = $metadataFactory; $this->validatorFactory = $validatorFactory; + $this->objectInitializers = $objectInitializers; } /** @@ -432,6 +440,14 @@ class RecursiveContextualValidator implements ContextualValidatorInterface { $context->setNode($object, $object, $metadata, $propertyPath); + if (!$context->isObjectInitialized($cacheKey)) { + foreach ($this->objectInitializers as $initializer) { + $initializer->initialize($object); + } + + $context->markObjectAsInitialized($cacheKey); + } + 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 diff --git a/src/Symfony/Component/Validator/Validator/RecursiveValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveValidator.php index d0a66f3d8a..c7ec2793ec 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveValidator.php @@ -15,6 +15,7 @@ use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\MetadataFactoryInterface; +use Symfony\Component\Validator\ObjectInitializerInterface; /** * Recursive implementation of {@link ValidatorInterface}. @@ -39,22 +40,29 @@ class RecursiveValidator implements ValidatorInterface */ protected $validatorFactory; + /** + * @var ObjectInitializerInterface[] + */ + protected $objectInitializers; + /** * 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 ConstraintValidatorFactoryInterface $validatorFactory The factory for creating - * constraint validators + * @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 + * @param ObjectInitializerInterface[] $objectInitializers The object initializers */ - public function __construct(ExecutionContextFactoryInterface $contextFactory, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory) + public function __construct(ExecutionContextFactoryInterface $contextFactory, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = array()) { $this->contextFactory = $contextFactory; $this->metadataFactory = $metadataFactory; $this->validatorFactory = $validatorFactory; + $this->objectInitializers = $objectInitializers; } /** @@ -65,7 +73,8 @@ class RecursiveValidator implements ValidatorInterface return new RecursiveContextualValidator( $this->contextFactory->createContext($this, $root), $this->metadataFactory, - $this->validatorFactory + $this->validatorFactory, + $this->objectInitializers ); } @@ -77,7 +86,8 @@ class RecursiveValidator implements ValidatorInterface return new RecursiveContextualValidator( $context, $this->metadataFactory, - $this->validatorFactory + $this->validatorFactory, + $this->objectInitializers ); } diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index 896eaecbdf..3f82ee2078 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -411,9 +411,9 @@ class ValidatorBuilder implements ValidatorBuilderInterface $contextFactory = new LegacyExecutionContextFactory($metadataFactory, $translator, $this->translationDomain); if (Validation::API_VERSION_2_5 === $apiVersion) { - return new RecursiveValidator($contextFactory, $metadataFactory, $validatorFactory); + return new RecursiveValidator($contextFactory, $metadataFactory, $validatorFactory, $this->initializers); } - return new LegacyValidator($contextFactory, $metadataFactory, $validatorFactory); + return new LegacyValidator($contextFactory, $metadataFactory, $validatorFactory, $this->initializers); } }