8000 feature #32107 [Validator] Add AutoMapping constraint to enable or di… · symfony/symfony@9e7ab8c · GitHub
[go: up one dir, main page]

Skip to content

Commit 9e7ab8c

Browse files
committed
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 ------- f6519ce [Validator] Add AutoMapping constraint to enable or disable auto-validation
2 parents 8e2bc5f + f6519ce commit 9e7ab8c

14 files changed

+406
-31
lines changed

src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ class DoctrineLoaderEntity extends DoctrineLoaderParentEntity
6969
/** @ORM\Column(type="simple_array", length=100) */
7070
public $simpleArrayField = [];
7171

72+
/**
73+
* @ORM\Column(length=10)
74+
* @Assert\DisableAutoMapping
75+
*/
76+
public $noAutoMapping;
77+
7278
public static function loadValidatorMetadata(ClassMetadata $metadata): void
7379
{
7480
$allowEmptyString = property_exists(Assert\Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : [];
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Doctrine\Tests\Fixtures;
13+
14+
use Doctrine\ORM\Mapping as ORM;
15+
use Symfony\Component\Validator\Constraints as Assert;
16+
17+
/**
18+
* @ORM\Entity
19+
* @Assert\DisableAutoMapping
20+
*
21+
* @author Kévin Dunglas <dunglas@gmail.com>
22+
*/
23+
class DoctrineLoaderNoAutoMappingEntity
24+
{
25+
/**
26+
* @ORM\Id
27+
* @ORM\Column
28+
*/
29+
public $id;
30+
31+
/**
32+
* @ORM\Column(length=20, unique=true)
33+
*/
34+
public $maxLength;
35+
36+
/**
37+
* @Assert\EnableAutoMapping
38+
* @ORM\Column(length=20)
39+
*/
40+
public $autoMappingExplicitlyEnabled;
41+
}

src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@
1717
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEmbed;
1818
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEntity;
1919
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderNestedEmbed;
20+
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderNoAutoMappingEntity;
2021
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderParentEntity;
2122
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
2223
use Symfony\Bridge\Doctrine\Validator\DoctrineLoader;
24+
use Symfony\Component\Validator\Constraints\DisableAutoMapping;
2325
use Symfony\Component\Validator\Constraints\Length;
2426
use Symfony\Component\Validator\Mapping\CascadingStrategy;
2527
use Symfony\Component\Validator\Mapping\ClassMetadata;
28+
use Symfony\Component\Validator\Mapping\Loader\AutoMappingTrait;
2629
use Symfony\Component\Validator\Mapping\TraversalStrategy;
2730
use Symfony\Component\Validator\Tests\Fixtures\Entity;
2831
use Symfony\Component\Validator\Validation;
@@ -33,12 +36,15 @@
3336
*/
3437
class DoctrineLoaderTest extends TestCase
3538
{
36-
public function testLoadClassMetadata()
39+
protected function setUp(): void
3740
{
38-
if (!method_exists(ValidatorBuilder::class, 'addLoader')) {
39-
$this->markTestSkipped('Auto-mapping requires symfony/validation 4.2+');
41+
if (!trait_exists(AutoMappingTrait::class)) {
42+
$this->markTestSkipped('Auto-mapping requires symfony/validation 4.4+');
4043
}
44+
}
4145

46+
public function testLoadClassMetadata()
47+
{
4248
$validator = Validation::createValidatorBuilder()
4349
->addMethodMapping('loadValidatorMetadata')
4450
->enableAnnotationMapping()
@@ -134,6 +140,12 @@ public function testLoadClassMetadata()
134140
$this->assertCount(1, $textFieldConstraints);
135141
$this->assertInstanceOf(Length::class, $textFieldConstraints[0]);
136142
$this->assertSame(1000, $textFieldConstraints[0]->max);
143+
144+
$noAutoMappingMetadata = $classMetadata->getPropertyMetadata('noAutoMapping');
145+
$this->assertCount(1, $noAutoMappingMetadata);
146+
$noAutoMappingConstraints = $noAutoMappingMetadata[0]->getConstraints();
147+
$this->assertCount(1, $noAutoMappingConstraints);
148+
$this->assertInstanceOf(DisableAutoMapping::class, $noAutoMappingConstraints[0]);
137149
}
138150

139151
public function testFieldMappingsConfiguration()
@@ -180,4 +192,28 @@ public function regexpProvider()
180192
[false, '{^'.preg_quote(Entity::class).'$}'],
181193
];
182194
}
195+
196+
public function testClassNoAutoMapping()
197+
{
198+
if (!method_exists(ValidatorBuilder::class, 'addLoader')) {
199+
$this->markTestSkipped('Auto-mapping requires symfony/validation 4.2+');
200+
}
201+
202+
$validator = Validation::createValidatorBuilder()
203+
->enableAnnotationMapping()
204+
->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager()))
205+
->getValidator();
206+
207+
$classMetadata = $validator->getMetadataFor(new DoctrineLoaderNoAutoMappingEntity());
208+
209+
$classConstraints = $classMetadata->getConstraints();
210+
$this->assertCount(1, $classConstraints);
211+
$this->assertInstanceOf(DisableAutoMapping::class, $classConstraints[0]);
212+
213+
$maxLengthMetadata = $classMetadata->getPropertyMetadata('maxLength');
214+
$this->assertEmpty($maxLengthMetadata);
215+
216+
$autoMappingExplicitlyEnabledMetadata = $classMetadata->getPropertyMetadata('autoMappingExplicitlyEnabled');
217+
$this->assertCount(2, $autoMappingExplicitlyEnabledMetadata[0]->constraints);
218+
}
183219
}

src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php

Copy file name to clipboard
Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616
use Doctrine\ORM\Mapping\ClassMetadataInfo;
1717
use Doctrine\ORM\Mapping\MappingException as OrmMappingException;
1818
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
19+
use Symfony\Component\Validator\Constraints\DisableAutoMapping;
20+
use Symfony\Component\Validator\Constraints\EnableAutoMapping;
1921
use Symfony\Component\Validator\Constraints\Length;
2022
use Symfony\Component\Validator\Constraints\Valid;
2123
use Symfony\Component\Validator\Mapping\ClassMetadata;
24+
use Symfony\Component\Validator\Mapping\Loader\AutoMappingTrait;
2225
use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;
2326

2427
/**
@@ -28,6 +31,8 @@
2831
*/
2932
final class DoctrineLoader implements LoaderInterface
3033
{
34+
use AutoMappingTrait;
35+
3136
private $entityManager;
3237
private $classValidatorRegexp;
3338

@@ -43,10 +48,6 @@ public function __construct(EntityManagerInterface $entityManager, string $class
4348
public function loadClassMetadata(ClassMetadata $metadata): bool
4449
{
4550
$className = $metadata->getClassName();
46-
if (null !== $this->classValidatorRegexp && !preg_match($this->classValidatorRegexp, $className)) {
47-
return false;
48-
}
49-
5051
try {
5152
$doctrineMetadata = $this->entityManager->getClassMetadata($className);
5253
} catch (MappingException | OrmMappingException $exception) {
@@ -57,6 +58,9 @@ public function loadClassMetadata(ClassMetadata $metadata): bool
5758
return false;
5859
}
5960

61+
$loaded = false;
62+
$enabledForClass = $this->isAutoMappingEnabledForClass($metadata, $this->classValidatorRegexp);
63+
6064
/* Available keys:
6165
- type
6266
- scale
@@ -69,41 +73,49 @@ public function loadClassMetadata(ClassMetadata $metadata): bool
6973

7074
// Type and nullable aren't handled here, use the PropertyInfo Loader instead.
7175
foreach ($doctrineMetadata->fieldMappings as $mapping) {
76+
$enabledForProperty = $enabledForClass;
77+
$lengthConstraint = null;
78+
foreach ($metadata->getPropertyMetadata($mapping['fieldName']) as $propertyMetadata) {
79+
foreach ($propertyMetadata->getConstraints() as $constraint) {
80+
// Enabling or disabling auto-mapping explicitly always takes precedence
81+
if ($constraint instanceof DisableAutoMapping) {
82+
continue 3;
83+
} elseif ($constraint instanceof EnableAutoMapping) {
84+
$enabledForProperty = true;
85+
} elseif ($constraint instanceof Length) {
86+
$lengthConstraint = $constraint;
87+
}
88+
}
89+
}
90+
91+
if (!$enabledForProperty) {
92+
continue;
93+
}
94+
7295
if (true === ($mapping['unique'] ?? false) && !isset($existingUniqueFields[$mapping['fieldName']])) {
7396
$metadata->addConstraint(new UniqueEntity(['fields' => $mapping['fieldName']]));
97+
$loaded = true;
7498
}
7599

76100
if (null === ($mapping['length'] ?? null) || !\in_array($mapping['type'], ['string', 'text'], true)) {
77101
continue;
78102
}
79103

80-
$constraint = $this->getLengthConstraint($metadata, $mapping['fieldName']);
81-
if (null === $constraint) {
104+
if (null === $lengthConstraint) {
82105
if (isset($mapping['originalClass']) && false === strpos($mapping['declaredField'], '.')) {
83106
$metadata->addPropertyConstraint($mapping['declaredField'], new Valid());
107+
$loaded = true;
84108
} elseif (property_exists($className, $mapping['fieldName'])) {
85109
$metadata->addPropertyConstraint($mapping['fieldName'], new Length(['max' => $mapping['length']]));
110+
$loaded = true;
86111
}
87-
} elseif (null === $constraint->max) {
112+
} elseif (null === $lengthConstraint->max) {
88113
// If a Length constraint exists and no max length has been explicitly defined, set it
89-
$constraint->max = $mapping['length'];
90-
}
91-
}
92-
93-
return true;
94-
}
95-
96-
private function getLengthConstraint(ClassMetadata $metadata, string $fieldName): ?Length
97-
{
98-
foreach ($metadata->getPropertyMetadata($fieldName) as $propertyMetadata) {
99-
foreach ($propertyMetadata->getConstraints() as $constraint) {
100-
if ($constraint instanceof Length) {
101-
return $constraint;
102-
}
114+
$lengthConstraint->max = $mapping['length'];
103115
}
104116
}
105117

106-
return null;
118+
return $loaded;
107119
}
108120

109121
private function< 10000 /span> getExistingUniqueFields(ClassMetadata $metadata): array

src/Symfony/Component/Validator/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
4.4.0
55
-----
66

7+
* added `EnableAutoMapping` and `DisableAutoMapping` constraints to enable or disable auto mapping for class or a property
78
* using anything else than a `string` as the code of a `ConstraintViolation` is deprecated, a `string` type-hint will
89
be added to the constructor of the `ConstraintViolation` class and to the `ConstraintViolationBuilder::setCode()`
910
method in 5.0
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
16+
17+
/**
18+
* Disables auto mapping.
19+
*
20+
* Using the annotations on a property has higher precedence than using 10000 it on a class,
21+
* which has higher precedence than any configuration that might be defined outside the class.
22+
*
23+
* @Annotation
24+
*
25+
* @author Kévin Dunglas <dunglas@gmail.com>
26+
*/
27+
class DisableAutoMapping extends Constraint
28+
{
29+
public function __construct($options = null)
30+
{
31+
if (\is_array($options) && \array_key_exists('groups', $options)) {
32+
throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__));
33+
}
34+
35+
parent::__construct($options);
36+
}
37+
38+
/**
39+
* {@inheritdoc}
40+
*/
41+
public function getTargets()
42+
{
43+
return [self::PROPERTY_CONSTRAINT, self::CLASS_CONSTRAINT];
44+
}
45+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
16+
17+
/**
18+
* Enables auto mapping.
19+
*
20+
* Using the annotations on a property has higher precedence than using it on a class,
21+
* which has higher precedence than any configuration that might be defined outside the class.
22+
*
23+
* @Annotation
24+
*
25+
* @author Kévin Dunglas <dunglas@gmail.com>
26+
*/
27+
class EnableAutoMapping extends Constraint
28+
{
29+
public function __construct($options = null)
30+
{
31+
if (\is_array($options) && \array_key_exists('groups', $options)) {
32+
throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__));
33+
}
34+
35+
parent::__construct($options);
36+
}
37+
38+
/**
39+
* {@inheritdoc}
40+
*/
41+
public function getTargets()
42+
{
43+
return [self::PROPERTY_CONSTRAINT, self::CLASS_CONSTRAINT];
44+
}
45+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Mapping\Loader;
13+
14+
use Symfony\Component\Validator\Constraints\DisableAutoMapping;
15+
use Symfony\Component\Validator\Constraints\EnableAutoMapping;
16+
use Symfony\Component\Validator\Mapping\ClassMetadata;
17+
18+
/**
19+
* Utility methods to create auto mapping loaders.
20+
*
21+
* @author Kévin Dunglas <dunglas@gmail.com>
22+
*/
23+
trait AutoMappingTrait
24+
{
25+
private function isAutoMappingEnabledForClass(ClassMetadata $metadata, string $classValidatorRegexp = null): bool
26+
{
27+
// Check if AutoMapping constraint is set first
28+
foreach ($metadata->getConstraints() as $constraint) {
29+
if ($constraint instanceof DisableAutoMapping) {
30+
return false;
31+
}
32+
33+
if ($constraint instanceof EnableAutoMapping) {
34+
return true;
35+
}
36+
}
37+
38+
// Fallback on the config
39+
return null === $classValidatorRegexp || preg_match($classValidatorRegexp, $metadata->getClassName());
40+
}
41+
}

0 commit comments

Comments
 (0)
0