feature #32107 [Validator] Add AutoMapping constraint to enable or disable auto-validation (dunglas)
This PR was squashed before being merged into the 4.4 branch (closes #32107).
Discussion
----------
[Validator] Add AutoMapping constraint to enable or disable auto-validation
| Q | A
| ------------- | ---
| Branch? | 4.4
| Bug fix? | yes
| New feature? | no
| BC breaks? | no
| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass? | yes <!-- please add some, will be required by reviewers -->
| Fixed tickets | #32070, #32015 <!-- #-prefixed issue number(s), if any -->
| License | MIT
| Doc PR | todo
As discussed in #32070 and #32015, it's sometimes mandatory to prevent some classes or properties to be auto mapped (auto-validated). This PR introduces a new constraint, `@AutoMapping` allowing to do exactly that. Examples:
Class:
```php
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity
* @Assert\AutoMapping(false)
*/
class DoctrineLoaderNoAutoMappingEntity
{
/**
* @ORM\Id
* @ORM\Column
*/
public $id;
/**
* @ORM\Column(length=20, unique=true)
*/
public $maxLength;
}
```
Property:
```php
namespace Symfony\Bridge\Doctrine\Tests\Fixtures;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity
*/
class DoctrineLoaderEntity extends DoctrineLoaderParentEntity
{
/**
* @ORM\Id
* @ORM\Column
*/
public $id;
/**
* @ORM\Column(length=10)
* @Assert\AutoMapping(false)
*/
public $noAutoMapping;
}
```
The rules are the following:
* If the constraint is present on a property, and set to true, auto-mapping is always on, regardless of the config, and of any class level annotation
* If the constraint is present on a property, and set to false, auto-mapping is always off, regardless of the config, and of any class level annotation
* If the constraint is present on a class, and set to true, auto-mapping is always on except if a the annotation has been added to a specific property, and regardless of the config
* If the constraint is present on a class, and set to false, auto-mapping is always off except if a the annotation has been added to a specific property, and regardless of the config
Commits
-------
f6519ce88b
[Validator] Add AutoMapping constraint to enable or disable auto-validation
This commit is contained in:
commit
9e7ab8c003
@ -69,6 +69,12 @@ class DoctrineLoaderEntity extends DoctrineLoaderParentEntity
|
|||||||
/** @ORM\Column(type="simple_array", length=100) */
|
/** @ORM\Column(type="simple_array", length=100) */
|
||||||
public $simpleArrayField = [];
|
public $simpleArrayField = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(length=10)
|
||||||
|
* @Assert\DisableAutoMapping
|
||||||
|
*/
|
||||||
|
public $noAutoMapping;
|
||||||
|
|
||||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||||
{
|
{
|
||||||
$allowEmptyString = property_exists(Assert\Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : [];
|
$allowEmptyString = property_exists(Assert\Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : [];
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Bridge\Doctrine\Tests\Fixtures;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Entity
|
||||||
|
* @Assert\DisableAutoMapping
|
||||||
|
*
|
||||||
|
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||||
|
*/
|
||||||
|
class DoctrineLoaderNoAutoMappingEntity
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @ORM\Id
|
||||||
|
* @ORM\Column
|
||||||
|
*/
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(length=20, unique=true)
|
||||||
|
*/
|
||||||
|
public $maxLength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Assert\EnableAutoMapping
|
||||||
|
* @ORM\Column(length=20)
|
||||||
|
*/
|
||||||
|
public $autoMappingExplicitlyEnabled;
|
||||||
|
}
|
@ -17,12 +17,15 @@ use Symfony\Bridge\Doctrine\Tests\Fixtures\BaseUser;
|
|||||||
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEmbed;
|
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEmbed;
|
||||||
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEntity;
|
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEntity;
|
||||||
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderNestedEmbed;
|
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderNestedEmbed;
|
||||||
|
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderNoAutoMappingEntity;
|
||||||
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderParentEntity;
|
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderParentEntity;
|
||||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||||
use Symfony\Bridge\Doctrine\Validator\DoctrineLoader;
|
use Symfony\Bridge\Doctrine\Validator\DoctrineLoader;
|
||||||
|
use Symfony\Component\Validator\Constraints\DisableAutoMapping;
|
||||||
use Symfony\Component\Validator\Constraints\Length;
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
use Symfony\Component\Validator\Mapping\CascadingStrategy;
|
use Symfony\Component\Validator\Mapping\CascadingStrategy;
|
||||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||||
|
use Symfony\Component\Validator\Mapping\Loader\AutoMappingTrait;
|
||||||
use Symfony\Component\Validator\Mapping\TraversalStrategy;
|
use Symfony\Component\Validator\Mapping\TraversalStrategy;
|
||||||
use Symfony\Component\Validator\Tests\Fixtures\Entity;
|
use Symfony\Component\Validator\Tests\Fixtures\Entity;
|
||||||
use Symfony\Component\Validator\Validation;
|
use Symfony\Component\Validator\Validation;
|
||||||
@ -33,12 +36,15 @@ use Symfony\Component\Validator\ValidatorBuilder;
|
|||||||
*/
|
*/
|
||||||
class DoctrineLoaderTest extends TestCase
|
class DoctrineLoaderTest extends TestCase
|
||||||
{
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
if (!trait_exists(AutoMappingTrait::class)) {
|
||||||
|
$this->markTestSkipped('Auto-mapping requires symfony/validation 4.4+');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function testLoadClassMetadata()
|
public function testLoadClassMetadata()
|
||||||
{
|
{
|
||||||
if (!method_exists(ValidatorBuilder::class, 'addLoader')) {
|
|
||||||
$this->markTestSkipped('Auto-mapping requires symfony/validation 4.2+');
|
|
||||||
}
|
|
||||||
|
|
||||||
$validator = Validation::createValidatorBuilder()
|
$validator = Validation::createValidatorBuilder()
|
||||||
->addMethodMapping('loadValidatorMetadata')
|
->addMethodMapping('loadValidatorMetadata')
|
||||||
->enableAnnotationMapping()
|
->enableAnnotationMapping()
|
||||||
@ -134,6 +140,12 @@ class DoctrineLoaderTest extends TestCase
|
|||||||
$this->assertCount(1, $textFieldConstraints);
|
$this->assertCount(1, $textFieldConstraints);
|
||||||
$this->assertInstanceOf(Length::class, $textFieldConstraints[0]);
|
$this->assertInstanceOf(Length::class, $textFieldConstraints[0]);
|
||||||
$this->assertSame(1000, $textFieldConstraints[0]->max);
|
$this->assertSame(1000, $textFieldConstraints[0]->max);
|
||||||
|
|
||||||
|
$noAutoMappingMetadata = $classMetadata->getPropertyMetadata('noAutoMapping');
|
||||||
|
$this->assertCount(1, $noAutoMappingMetadata);
|
||||||
|
$noAutoMappingConstraints = $noAutoMappingMetadata[0]->getConstraints();
|
||||||
|
$this->assertCount(1, $noAutoMappingConstraints);
|
||||||
|
$this->assertInstanceOf(DisableAutoMapping::class, $noAutoMappingConstraints[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFieldMappingsConfiguration()
|
public function testFieldMappingsConfiguration()
|
||||||
@ -180,4 +192,28 @@ class DoctrineLoaderTest extends TestCase
|
|||||||
[false, '{^'.preg_quote(Entity::class).'$}'],
|
[false, '{^'.preg_quote(Entity::class).'$}'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testClassNoAutoMapping()
|
||||||
|
{
|
||||||
|
if (!method_exists(ValidatorBuilder::class, 'addLoader')) {
|
||||||
|
$this->markTestSkipped('Auto-mapping requires symfony/validation 4.2+');
|
||||||
|
}
|
||||||
|
|
||||||
|
$validator = Validation::createValidatorBuilder()
|
||||||
|
->enableAnnotationMapping()
|
||||||
|
->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager()))
|
||||||
|
->getValidator();
|
||||||
|
|
||||||
|
$classMetadata = $validator->getMetadataFor(new DoctrineLoaderNoAutoMappingEntity());
|
||||||
|
|
||||||
|
$classConstraints = $classMetadata->getConstraints();
|
||||||
|
$this->assertCount(1, $classConstraints);
|
||||||
|
$this->assertInstanceOf(DisableAutoMapping::class, $classConstraints[0]);
|
||||||
|
|
||||||
|
$maxLengthMetadata = $classMetadata->getPropertyMetadata('maxLength');
|
||||||
|
$this->assertEmpty($maxLengthMetadata);
|
||||||
|
|
||||||
|
$autoMappingExplicitlyEnabledMetadata = $classMetadata->getPropertyMetadata('autoMappingExplicitlyEnabled');
|
||||||
|
$this->assertCount(2, $autoMappingExplicitlyEnabledMetadata[0]->constraints);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,12 @@ use Doctrine\ORM\EntityManagerInterface;
|
|||||||
use Doctrine\ORM\Mapping\ClassMetadataInfo;
|
use Doctrine\ORM\Mapping\ClassMetadataInfo;
|
||||||
use Doctrine\ORM\Mapping\MappingException as OrmMappingException;
|
use Doctrine\ORM\Mapping\MappingException as OrmMappingException;
|
||||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||||
|
use Symfony\Component\Validator\Constraints\DisableAutoMapping;
|
||||||
|
use Symfony\Component\Validator\Constraints\EnableAutoMapping;
|
||||||
use Symfony\Component\Validator\Constraints\Length;
|
use Symfony\Component\Validator\Constraints\Length;
|
||||||
use Symfony\Component\Validator\Constraints\Valid;
|
use Symfony\Component\Validator\Constraints\Valid;
|
||||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||||
|
use Symfony\Component\Validator\Mapping\Loader\AutoMappingTrait;
|
||||||
use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;
|
use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,6 +31,8 @@ use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;
|
|||||||
*/
|
*/
|
||||||
final class DoctrineLoader implements LoaderInterface
|
final class DoctrineLoader implements LoaderInterface
|
||||||
{
|
{
|
||||||
|
use AutoMappingTrait;
|
||||||
|
|
||||||
private $entityManager;
|
private $entityManager;
|
||||||
private $classValidatorRegexp;
|
private $classValidatorRegexp;
|
||||||
|
|
||||||
@ -43,10 +48,6 @@ final class DoctrineLoader implements LoaderInterface
|
|||||||
public function loadClassMetadata(ClassMetadata $metadata): bool
|
public function loadClassMetadata(ClassMetadata $metadata): bool
|
||||||
{
|
{
|
||||||
$className = $metadata->getClassName();
|
$className = $metadata->getClassName();
|
||||||
if (null !== $this->classValidatorRegexp && !preg_match($this->classValidatorRegexp, $className)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$doctrineMetadata = $this->entityManager->getClassMetadata($className);
|
$doctrineMetadata = $this->entityManager->getClassMetadata($className);
|
||||||
} catch (MappingException | OrmMappingException $exception) {
|
} catch (MappingException | OrmMappingException $exception) {
|
||||||
@ -57,6 +58,9 @@ final class DoctrineLoader implements LoaderInterface
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$loaded = false;
|
||||||
|
$enabledForClass = $this->isAutoMappingEnabledForClass($metadata, $this->classValidatorRegexp);
|
||||||
|
|
||||||
/* Available keys:
|
/* Available keys:
|
||||||
- type
|
- type
|
||||||
- scale
|
- scale
|
||||||
@ -69,41 +73,49 @@ final class DoctrineLoader implements LoaderInterface
|
|||||||
|
|
||||||
// Type and nullable aren't handled here, use the PropertyInfo Loader instead.
|
// Type and nullable aren't handled here, use the PropertyInfo Loader instead.
|
||||||
foreach ($doctrineMetadata->fieldMappings as $mapping) {
|
foreach ($doctrineMetadata->fieldMappings as $mapping) {
|
||||||
|
$enabledForProperty = $enabledForClass;
|
||||||
|
$lengthConstraint = null;
|
||||||
|
foreach ($metadata->getPropertyMetadata($mapping['fieldName']) as $propertyMetadata) {
|
||||||
|
foreach ($propertyMetadata->getConstraints() as $constraint) {
|
||||||
|
// Enabling or disabling auto-mapping explicitly always takes precedence
|
||||||
|
if ($constraint instanceof DisableAutoMapping) {
|
||||||
|
continue 3;
|
||||||
|
} elseif ($constraint instanceof EnableAutoMapping) {
|
||||||
|
$enabledForProperty = true;
|
||||||
|
} elseif ($constraint instanceof Length) {
|
||||||
|
$lengthConstraint = $constraint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$enabledForProperty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (true === ($mapping['unique'] ?? false) && !isset($existingUniqueFields[$mapping['fieldName']])) {
|
if (true === ($mapping['unique'] ?? false) && !isset($existingUniqueFields[$mapping['fieldName']])) {
|
||||||
$metadata->addConstraint(new UniqueEntity(['fields' => $mapping['fieldName']]));
|
$metadata->addConstraint(new UniqueEntity(['fields' => $mapping['fieldName']]));
|
||||||
|
$loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null === ($mapping['length'] ?? null) || !\in_array($mapping['type'], ['string', 'text'], true)) {
|
if (null === ($mapping['length'] ?? null) || !\in_array($mapping['type'], ['string', 'text'], true)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$constraint = $this->getLengthConstraint($metadata, $mapping['fieldName']);
|
if (null === $lengthConstraint) {
|
||||||
if (null === $constraint) {
|
|
||||||
if (isset($mapping['originalClass']) && false === strpos($mapping['declaredField'], '.')) {
|
if (isset($mapping['originalClass']) && false === strpos($mapping['declaredField'], '.')) {
|
||||||
$metadata->addPropertyConstraint($mapping['declaredField'], new Valid());
|
$metadata->addPropertyConstraint($mapping['declaredField'], new Valid());
|
||||||
|
$loaded = true;
|
||||||
} elseif (property_exists($className, $mapping['fieldName'])) {
|
} elseif (property_exists($className, $mapping['fieldName'])) {
|
||||||
$metadata->addPropertyConstraint($mapping['fieldName'], new Length(['max' => $mapping['length']]));
|
$metadata->addPropertyConstraint($mapping['fieldName'], new Length(['max' => $mapping['length']]));
|
||||||
|
$loaded = true;
|
||||||
}
|
}
|
||||||
} elseif (null === $constraint->max) {
|
} elseif (null === $lengthConstraint->max) {
|
||||||
// If a Length constraint exists and no max length has been explicitly defined, set it
|
// If a Length constraint exists and no max length has been explicitly defined, set it
|
||||||
$constraint->max = $mapping['length'];
|
$lengthConstraint->max = $mapping['length'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return $loaded;
|
||||||
}
|
|
||||||
|
|
||||||
private function getLengthConstraint(ClassMetadata $metadata, string $fieldName): ?Length
|
|
||||||
{
|
|
||||||
foreach ($metadata->getPropertyMetadata($fieldName) as $propertyMetadata) {
|
|
||||||
foreach ($propertyMetadata->getConstraints() as $constraint) {
|
|
||||||
if ($constraint instanceof Length) {
|
|
||||||
return $constraint;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getExistingUniqueFields(ClassMetadata $metadata): array
|
private function getExistingUniqueFields(ClassMetadata $metadata): array
|
||||||
|
@ -4,6 +4,7 @@ CHANGELOG
|
|||||||
4.4.0
|
4.4.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
* added `EnableAutoMapping` and `DisableAutoMapping` constraints to enable or disable auto mapping for class or a property
|
||||||
* using anything else than a `string` as the code of a `ConstraintViolation` is deprecated, a `string` type-hint will
|
* using anything else than a `string` as the code of a `ConstraintViolation` is deprecated, a `string` type-hint will
|
||||||
be added to the constructor of the `ConstraintViolation` class and to the `ConstraintViolationBuilder::setCode()`
|
be added to the constructor of the `ConstraintViolation` class and to the `ConstraintViolationBuilder::setCode()`
|
||||||
method in 5.0
|
method in 5.0
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables auto mapping.
|
||||||
|
*
|
||||||
|
* Using the annotations on a property has higher precedence than using it on a class,
|
||||||
|
* which has higher precedence than any configuration that might be defined outside the class.
|
||||||
|
*
|
||||||
|
* @Annotation
|
||||||
|
*
|
||||||
|
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||||
|
*/
|
||||||
|
class DisableAutoMapping extends Constraint
|
||||||
|
{
|
||||||
|
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 getTargets()
|
||||||
|
{
|
||||||
|
return [self::PROPERTY_CONSTRAINT, self::CLASS_CONSTRAINT];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables auto mapping.
|
||||||
|
*
|
||||||
|
* Using the annotations on a property has higher precedence than using it on a class,
|
||||||
|
* which has higher precedence than any configuration that might be defined outside the class.
|
||||||
|
*
|
||||||
|
* @Annotation
|
||||||
|
*
|
||||||
|
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||||
|
*/
|
||||||
|
class EnableAutoMapping extends Constraint
|
||||||
|
{
|
||||||
|
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 getTargets()
|
||||||
|
{
|
||||||
|
return [self::PROPERTY_CONSTRAINT, self::CLASS_CONSTRAINT];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Validator\Mapping\Loader;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraints\DisableAutoMapping;
|
||||||
|
use Symfony\Component\Validator\Constraints\EnableAutoMapping;
|
||||||
|
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility methods to create auto mapping loaders.
|
||||||
|
*
|
||||||
|
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||||
|
*/
|
||||||
|
trait AutoMappingTrait
|
||||||
|
{
|
||||||
|
private function isAutoMappingEnabledForClass(ClassMetadata $metadata, string $classValidatorRegexp = null): bool
|
||||||
|
{
|
||||||
|
// Check if AutoMapping constraint is set first
|
||||||
|
foreach ($metadata->getConstraints() as $constraint) {
|
||||||
|
if ($constraint instanceof DisableAutoMapping) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($constraint instanceof EnableAutoMapping) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback on the config
|
||||||
|
return null === $classValidatorRegexp || preg_match($classValidatorRegexp, $metadata->getClassName());
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,8 @@ use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
|
|||||||
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
|
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
|
||||||
use Symfony\Component\PropertyInfo\Type as PropertyInfoType;
|
use Symfony\Component\PropertyInfo\Type as PropertyInfoType;
|
||||||
use Symfony\Component\Validator\Constraints\All;
|
use Symfony\Component\Validator\Constraints\All;
|
||||||
|
use Symfony\Component\Validator\Constraints\DisableAutoMapping;
|
||||||
|
use Symfony\Component\Validator\Constraints\EnableAutoMapping;
|
||||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
use Symfony\Component\Validator\Constraints\NotNull;
|
use Symfony\Component\Validator\Constraints\NotNull;
|
||||||
use Symfony\Component\Validator\Constraints\Type;
|
use Symfony\Component\Validator\Constraints\Type;
|
||||||
@ -28,6 +30,8 @@ use Symfony\Component\Validator\Mapping\ClassMetadata;
|
|||||||
*/
|
*/
|
||||||
final class PropertyInfoLoader implements LoaderInterface
|
final class PropertyInfoLoader implements LoaderInterface
|
||||||
{
|
{
|
||||||
|
use AutoMappingTrait;
|
||||||
|
|
||||||
private $listExtractor;
|
private $listExtractor;
|
||||||
private $typeExtractor;
|
private $typeExtractor;
|
||||||
private $accessExtractor;
|
private $accessExtractor;
|
||||||
@ -47,14 +51,12 @@ final class PropertyInfoLoader implements LoaderInterface
|
|||||||
public function loadClassMetadata(ClassMetadata $metadata): bool
|
public function loadClassMetadata(ClassMetadata $metadata): bool
|
||||||
{
|
{
|
||||||
$className = $metadata->getClassName();
|
$className = $metadata->getClassName();
|
||||||
if (null !== $this->classValidatorRegexp && !preg_match($this->classValidatorRegexp, $className)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$properties = $this->listExtractor->getProperties($className)) {
|
if (!$properties = $this->listExtractor->getProperties($className)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$loaded = false;
|
||||||
|
$enabledForClass = $this->isAutoMappingEnabledForClass($metadata, $this->classValidatorRegexp);
|
||||||
foreach ($properties as $property) {
|
foreach ($properties as $property) {
|
||||||
if (false === $this->accessExtractor->isWritable($className, $property)) {
|
if (false === $this->accessExtractor->isWritable($className, $property)) {
|
||||||
continue;
|
continue;
|
||||||
@ -69,12 +71,22 @@ final class PropertyInfoLoader implements LoaderInterface
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$enabledForProperty = $enabledForClass;
|
||||||
$hasTypeConstraint = false;
|
$hasTypeConstraint = false;
|
||||||
$hasNotNullConstraint = false;
|
$hasNotNullConstraint = false;
|
||||||
$hasNotBlankConstraint = false;
|
$hasNotBlankConstraint = false;
|
||||||
$allConstraint = null;
|
$allConstraint = null;
|
||||||
foreach ($metadata->getPropertyMetadata($property) as $propertyMetadata) {
|
foreach ($metadata->getPropertyMetadata($property) as $propertyMetadata) {
|
||||||
foreach ($propertyMetadata->getConstraints() as $constraint) {
|
foreach ($propertyMetadata->getConstraints() as $constraint) {
|
||||||
|
// Enabling or disabling auto-mapping explicitly always takes precedence
|
||||||
|
if ($constraint instanceof DisableAutoMapping) {
|
||||||
|
continue 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($constraint instanceof EnableAutoMapping) {
|
||||||
|
$enabledForProperty = true;
|
||||||
|
}
|
||||||
|
|
||||||
if ($constraint instanceof Type) {
|
if ($constraint instanceof Type) {
|
||||||
$hasTypeConstraint = true;
|
$hasTypeConstraint = true;
|
||||||
} elseif ($constraint instanceof NotNull) {
|
} elseif ($constraint instanceof NotNull) {
|
||||||
@ -87,6 +99,11 @@ final class PropertyInfoLoader implements LoaderInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$enabledForProperty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$loaded = true;
|
||||||
$builtinTypes = [];
|
$builtinTypes = [];
|
||||||
$nullable = false;
|
$nullable = false;
|
||||||
$scalar = true;
|
$scalar = true;
|
||||||
@ -118,7 +135,7 @@ final class PropertyInfoLoader implements LoaderInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return $loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getTypeConstraint(string $builtinType, PropertyInfoType $type): Type
|
private function getTypeConstraint(string $builtinType, PropertyInfoType $type): Type
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* 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 PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Validator\Constraints\DisableAutoMapping;
|
||||||
|
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||||
|
*/
|
||||||
|
class DisableAutoMappingTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGroups()
|
||||||
|
{
|
||||||
|
$this->expectException(ConstraintDefinitionException::class);
|
||||||
|
$this->expectExceptionMessage(sprintf('The option "groups" is not supported by the constraint "%s".', DisableAutoMapping::class));
|
||||||
|
|
||||||
|
new DisableAutoMapping(['groups' => 'foo']);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* 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 PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Validator\Constraints\EnableAutoMapping;
|
||||||
|
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||||
|
*/
|
||||||
|
class EnableAutoMappingTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGroups()
|
||||||
|
{
|
||||||
|
$this->expectException(ConstraintDefinitionException::class);
|
||||||
|
$this->expectExceptionMessage(sprintf('The option "groups" is not supported by the constraint "%s".', EnableAutoMapping::class));
|
||||||
|
|
||||||
|
new EnableAutoMapping(['groups' => 'foo']);
|
||||||
|
}
|
||||||
|
}
|
@ -49,6 +49,11 @@ class PropertyInfoLoaderEntity
|
|||||||
|
|
||||||
public $readOnly;
|
public $readOnly;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Assert\DisableAutoMapping
|
||||||
|
*/
|
||||||
|
public $noAutoMapping;
|
||||||
|
|
||||||
public function setNonExistentField()
|
public function setNonExistentField()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* 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\Constraints as Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Assert\DisableAutoMapping
|
||||||
|
*
|
||||||
|
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||||
|
*/
|
||||||
|
class PropertyInfoLoaderNoAutoMappingEntity
|
||||||
|
{
|
||||||
|
public $string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Assert\EnableAutoMapping
|
||||||
|
*/
|
||||||
|
public $autoMappingExplicitlyEnabled;
|
||||||
|
}
|
@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
|
|||||||
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
|
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
|
||||||
use Symfony\Component\PropertyInfo\Type;
|
use Symfony\Component\PropertyInfo\Type;
|
||||||
use Symfony\Component\Validator\Constraints\All;
|
use Symfony\Component\Validator\Constraints\All;
|
||||||
|
use Symfony\Component\Validator\Constraints\DisableAutoMapping;
|
||||||
use Symfony\Component\Validator\Constraints\Iban;
|
use Symfony\Component\Validator\Constraints\Iban;
|
||||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
use Symfony\Component\Validator\Constraints\NotNull;
|
use Symfony\Component\Validator\Constraints\NotNull;
|
||||||
@ -23,6 +24,7 @@ use Symfony\Component\Validator\Mapping\ClassMetadata;
|
|||||||
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
|
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
|
||||||
use Symfony\Component\Validator\Tests\Fixtures\Entity;
|
use Symfony\Component\Validator\Tests\Fixtures\Entity;
|
||||||
use Symfony\Component\Validator\Tests\Fixtures\PropertyInfoLoaderEntity;
|
use Symfony\Component\Validator\Tests\Fixtures\PropertyInfoLoaderEntity;
|
||||||
|
use Symfony\Component\Validator\Tests\Fixtures\PropertyInfoLoaderNoAutoMappingEntity;
|
||||||
use Symfony\Component\Validator\Validation;
|
use Symfony\Component\Validator\Validation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,6 +49,7 @@ class PropertyInfoLoaderTest extends TestCase
|
|||||||
'alreadyPartiallyMappedCollection',
|
'alreadyPartiallyMappedCollection',
|
||||||
'readOnly',
|
'readOnly',
|
||||||
'nonExistentField',
|
'nonExistentField',
|
||||||
|
'noAutoMapping',
|
||||||
])
|
])
|
||||||
;
|
;
|
||||||
$propertyInfoStub
|
$propertyInfoStub
|
||||||
@ -61,6 +64,7 @@ class PropertyInfoLoaderTest extends TestCase
|
|||||||
[new Type(Type::BUILTIN_TYPE_STRING, true)],
|
[new Type(Type::BUILTIN_TYPE_STRING, true)],
|
||||||
[new Type(Type::BUILTIN_TYPE_STRING, true)],
|
[new Type(Type::BUILTIN_TYPE_STRING, true)],
|
||||||
[new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, null, new Type(Type::BUILTIN_TYPE_FLOAT))],
|
[new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, null, new Type(Type::BUILTIN_TYPE_FLOAT))],
|
||||||
|
[new Type(Type::BUILTIN_TYPE_STRING)],
|
||||||
[new Type(Type::BUILTIN_TYPE_STRING)]
|
[new Type(Type::BUILTIN_TYPE_STRING)]
|
||||||
))
|
))
|
||||||
;
|
;
|
||||||
@ -76,7 +80,8 @@ class PropertyInfoLoaderTest extends TestCase
|
|||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
false
|
false,
|
||||||
|
true
|
||||||
))
|
))
|
||||||
;
|
;
|
||||||
|
|
||||||
@ -158,6 +163,12 @@ class PropertyInfoLoaderTest extends TestCase
|
|||||||
|
|
||||||
$readOnlyMetadata = $classMetadata->getPropertyMetadata('readOnly');
|
$readOnlyMetadata = $classMetadata->getPropertyMetadata('readOnly');
|
||||||
$this->assertEmpty($readOnlyMetadata);
|
$this->assertEmpty($readOnlyMetadata);
|
||||||
|
|
||||||
|
$noAutoMappingMetadata = $classMetadata->getPropertyMetadata('noAutoMapping');
|
||||||
|
$this->assertCount(1, $noAutoMappingMetadata);
|
||||||
|
$noAutoMappingConstraints = $noAutoMappingMetadata[0]->getConstraints();
|
||||||
|
$this->assertCount(1, $noAutoMappingConstraints);
|
||||||
|
$this->assertInstanceOf(DisableAutoMapping::class, $noAutoMappingConstraints[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -189,4 +200,30 @@ class PropertyInfoLoaderTest extends TestCase
|
|||||||
[false, '{^'.preg_quote(Entity::class).'$}'],
|
[false, '{^'.preg_quote(Entity::class).'$}'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testClassNoAutoMapping()
|
||||||
|
{
|
||||||
|
$propertyInfoStub = $this->createMock(PropertyInfoExtractorInterface::class);
|
||||||
|
$propertyInfoStub
|
||||||
|
->method('getProperties')
|
||||||
|
->willReturn(['string', 'autoMappingExplicitlyEnabled'])
|
||||||
|
;
|
||||||
|
$propertyInfoStub
|
||||||
|
->method('getTypes')
|
||||||
|
->willReturnOnConsecutiveCalls(
|
||||||
|
[new Type(Type::BUILTIN_TYPE_STRING)],
|
||||||
|
[new Type(Type::BUILTIN_TYPE_BOOL)]
|
||||||
|
);
|
||||||
|
|
||||||
|
$propertyInfoLoader = new PropertyInfoLoader($propertyInfoStub, $propertyInfoStub, $propertyInfoStub);
|
||||||
|
$validator = Validation::createValidatorBuilder()
|
||||||
|
->enableAnnotationMapping()
|
||||||
|
->addLoader($propertyInfoLoader)
|
||||||
|
->getValidator()
|
||||||
|
;
|
||||||
|
|
||||||
|
$classMetadata = $validator->getMetadataFor(new PropertyInfoLoaderNoAutoMappingEntity());
|
||||||
|
$this->assertEmpty($classMetadata->getPropertyMetadata('string'));
|
||||||
|
$this->assertCount(3, $classMetadata->getPropertyMetadata('autoMappingExplicitlyEnabled')[0]->constraints);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user