bug #11410 [Validator] Fixed object initializers in 2.5 version of the Validator (webmozart)

This PR was merged into the 2.5 branch.

Discussion
----------

[Validator] Fixed object initializers in 2.5 version of the Validator

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #11159
| License       | MIT
| Doc PR        | -

Object initializers were forgot in the 2.5 Validator API. This PR implements them.

A bug was fixed also in the old version which resulted in the initializers being called multiple times if an object was validated in multiple groups within the same validation run. This fix will be backported separately to older versions.

Commits
-------

ce04073 [Validator] Fixed object initializers in 2.5 version of the Validator
This commit is contained in:
Bernhard Schussek 2014-07-21 13:29:59 +02:00
commit 4c97420fef
12 changed files with 200 additions and 36 deletions

View File

@ -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]);
}
}

View File

@ -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)
{

View File

@ -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);
}
}

View File

@ -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());

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
/**

View File

@ -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

View File

@ -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

View File

@ -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
);
}

View File

@ -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);
}
}