8000 [Validator][DoctrineBridge][FWBundle] Automatic data validation · symfony/symfony@2d64e70 · GitHub
[go: up one dir, main page]

Skip to content
Sign in

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 2d64e70

Browse files
committed
[Validator][DoctrineBridge][FWBundle] Automatic data validation
1 parent af28965 commit 2d64e70

File tree

19 files changed

+937
-3
lines changed

19 files changed

+937
-3
lines changed
L F438 ines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
16+
use Symfony\Component\Validator\Constraints as Assert;
17+
18+
/**
19+
* @ORM\Entity
20+
* @UniqueEntity(fields={"alreadyMappedUnique"})
21+
*
22+
* @author Kévin Dunglas <dunglas@gmail.com>
23+
*/
24+
class DoctrineLoaderEntity
25+
{
26+
/**
27+
* @ORM\Id
28+
* @ORM\Column
29+
*/
30+
public $id;
31+
32+
/**
33+
* @ORM\Column(length=20)
34+
*/
35+
public $maxLength;
36+
37+
/**
38+
* @ORM\Column(length=20)
39+
* @Assert\Length(min=5)
40+
*/
41+
public $mergedMaxLength;
42+
43+
/**
44+
* @ORM\Column(length=20)
45+
* @Assert\Length(min=1, max=10)
46+
*/
47+
public $alreadyMappedMaxLength;
48+
49+
/**
50+
* @ORM\Column(unique=true)
51+
*/
52+
public $unique;
53+
54+
/**
55+
* @ORM\Column(unique=true)
56+
*/
57+
public $alreadyMappedUnique;
58+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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\Validator;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper;
16+
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEntity;
17+
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
18+
use Symfony\Bridge\Doctrine\Validator\DoctrineLoader;
19+
use Symfony\Component\Validator\Constraints\Length;
20+
use Symfony\Component\Validator\Mapping\ClassMetadata;
21+
use Symfony\Component\Validator\Tests\Fixtures\Entity;
22+
use Symfony\Component\Validator\Validation;
23+
use Symfony\Component\Validator\ValidatorBuilder;
24+
25+
/**
26+
* @author Kévin Dunglas <dunglas@gmail.com>
27+
*/
28+
class DoctrineLoaderTest extends TestCase
29+
{
30+
public function testLoadClassMetadata()
31+
{
32+
if (!method_exists(ValidatorBuilder::class, 'addLoader')) {
33+
$this->markTestSkipped('Auto-mapping requires symfony/validation 4.2+');
34+
}
35+
36+
$validator = Validation::createValidatorBuilder()
37+
->enableAnnotationMapping()
38+
->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager()))
39+
->getValidator()
40+
;
41+
42+
$classMetadata = $validator->getMetadataFor(new DoctrineLoaderEntity());
43+
44+
$classConstraints = $classMetadata->getConstraints();
45+
$this->assertCount(2, $classConstraints);
46+
$this->assertInstanceOf(UniqueEntity::class, $classConstraints[0]);
47+
$this->assertInstanceOf(UniqueEntity::class, $classConstraints[1]);
48+
$this->assertSame(['alreadyMappedUnique'], $classConstraints[0]->fields);
49+
$this->assertSame('unique', $classConstraints[1]->fields);
50+
51+
$maxLengthMetadata = $classMetadata->getPropertyMetadata('maxLength');
52+
$this->assertCount(1, $maxLengthMetadata);
53+
$maxLengthConstraints = $maxLengthMetadata[0]->getConstraints();
54+
$this->assertCount(1, $maxLengthConstraints);
55+
$this->assertInstanceOf(Length::class, $maxLengthConstraints[0]);
56+
$this->assertSame(20, $maxLengthConstraints[0]->max);
57+
58+
$mergedMaxLengthMetadata = $classMetadata->getPropertyMetadata('mergedMaxLength');
59+
$this->assertCount(1, $mergedMaxLengthMetadata);
60+
$mergedMaxLengthConstraints = $mergedMaxLengthMetadata[0]->getConstraints();
61+
$this->assertCount(1, $mergedMaxLengthConstraints);
62+
$this->assertInstanceOf(Length::class, $mergedMaxLengthConstraints[0]);
63+
$this->assertSame(20, $mergedMaxLengthConstraints[0]->max);
64+
$this->assertSame(5, $mergedMaxLengthConstraints[0]->min);
65+
66+
$alreadyMappedMaxLengthMetadata = $classMetadata->getPropertyMetadata('alreadyMappedMaxLength');
67+
$this->assertCount(1, $alreadyMappedMaxLengthMetadata);
68+
$alreadyMappedMaxLengthConstraints = $alreadyMappedMaxLengthMetadata[0]->getConstraints();
69+
$this->assertCount(1, $alreadyMappedMaxLengthConstraints);
70+
$this->assertInstanceOf(Length::class, $alreadyMappedMaxLengthConstraints[0]);
71+
$this->assertSame(10, $alreadyMappedMaxLengthConstraints[0]->max);
72+
$this->assertSame(1, $alreadyMappedMaxLengthConstraints[0]->min);
73+
}
74+
75+
/**
76+
* @dataProvider regexpProvider
77+
*/
78+
public function testClassValidator(bool $expected, string $classValidatorRegexp = null)
79+
{
80+
$doctrineLoader = new DoctrineLoader(DoctrineTestHelper::createTestEntityManager(), $classValidatorRegexp);
81+
82+
$classMetadata = new ClassMetadata(DoctrineLoaderEntity::class);
83+
$this->assertSame($expected, $doctrineLoader->loadClassMetadata($classMetadata));
84+
}
85+
86+
public function regexpProvider()
87+
{
88+
return [
89+
[true, null],
90+
[true, '{^'.preg_quote(DoctrineLoaderEntity::class).'$|^'.preg_quote(Entity::class).'$}'],
91+
[false, '{^'.preg_quote(Entity::class).'$}'],
92+
];
93+
}
94+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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\Validator;
13+
14+
use Doctrine\Common\Persistence\Mapping\MappingException;
15+
use Doctrine\ORM\EntityManagerInterface;
16+
use Doctrine\ORM\Mapping\ClassMetadataInfo;
17+
use Doctrine\ORM\Mapping\MappingException as OrmMappingException;
18+
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
19+
use Symfony\Component\Validator\Constraints\Length;
20+
use Symfony\Component\Validator\Mapping\ClassMetadata;
21+
use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;
22+
23+
/**
24+
* Guesses and loads the appropriate constraints using Doctrine's metadata.
25+
*
26+
* @author Kévin Dunglas <dunglas@gmail.com>
27+
*/
28+
final class DoctrineLoader implements LoaderInterface
29+
{
30+
private $entityManager;
31+
private $classValidatorRegexp;
32+
33+
public function __construct(EntityManagerInterface $entityManager, string $classValidatorRegexp = null)
34+
{
35+
$this->entityManager = $entityManager;
36+
$this->classValidatorRegexp = $classValidatorRegexp;
37+
}
38+
39+
/**
40+
* {@inheritdoc}
41+
*/
42+
public function loadClassMetadata(ClassMetadata $metadata): bool
43+
{
44+
$className = $metadata->getClassName();
45+
if (null !== $this->classValidatorRegexp && !preg_match($this->classValidatorRegexp, $className)) {
46+
return false;
47+
}
48+
49+
try {
50+
$doctrineMetadata = $this->entityManager->getClassMetadata($className);
51+
} catch (MappingException | OrmMappingException $exception) {
52+
return false;
53+
}
54+
55+
if (!$doctrineMetadata instanceof ClassMetadataInfo) {
56+
return false;
57+
}
58+
59+
/* Available keys:
60+
- type
61+
- scale
62+
- length
63+
- unique
64+
- nullable
65+
- precision
66+
*/
67+
$existingUniqueFields = $this->getExistingUniqueFields($metadata);
68+
69+
// Type and nullable aren't handled here, use the PropertyInfo Loader instead.
70+
foreach ($doctrineMetadata->fieldMappings as $mapping) {
71+
if (true === $mapping['unique'] && !isset($existingUniqueFields[$mapping['fieldName']])) {
72+
$metadata->addConstraint(new UniqueEntity(['fields' => $mapping['fieldName']]));
73+
}
74+
75+
if (null === $mapping['length']) {
76+
continue;
77+
}
78+
79+
$constraint = $this->getLengthConstraint($metadata, $mapping['fieldName']);
80+
if (null === $constraint) {
81+
$metadata->addPropertyConstraint($mapping['fieldName'], new Length(['max' => $mapping['length']]));
82+
} elseif (null === $constraint->max) {
83+
// If a Length constraint exists and no max length has been explicitly defined, set it
84+
$constraint->max = $mapping['length'];
85+
}
86+
}
87+
88+
return true;
89+
}
90+
91+
private function getLengthConstraint(ClassMetadata $metadata, string $fieldName): ?Length
92+
{
93+
foreach ($metadata->getPropertyMetadata($fieldName) as $propertyMetadata) {
94+
foreach ($propertyMetadata->getConstraints() as $constraint) {
95+
if ($constraint instanceof Length) {
96+
return $constraint;
97+
}
98+
}
99+
}
100+
101+
return null;
102+
}
103+
104+
private function getExistingUniqueFields(ClassMetadata $metadata): array
105+
{
106+
$fields = [];
107+
foreach ($metadata->getConstraints() as $constraint) {
108+
if (!$constraint instanceof UniqueEntity) {
109+
continue;
110+
}
111+
112+
if (\is_string($constraint->fields)) {
113+
$fields[$constraint->fields] = true;
114+
} elseif (\is_array($constraint->fields) && 1 === \count($constraint->fields)) {
115+
$fields[$constraint->fields[0]] = true;
116+
}
117+
}
118+
119+
return $fields;
120+
}
121+
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,45 @@ private function addValidationSection(ArrayNodeDefinition $rootNode)
792792
->end()
793793
->end()
794794
->end()
795+
->arrayNode('auto_mapping')
796+
->useAttributeAsKey('namespace')
797+
->normalizeKeys(false)
798+
->beforeNormalization()
799+
->ifArray()
800+
->then(function (array $values): array {
801+
foreach ($values as $k => $v) {
802+
if (isset($v['service'])) {
803+
continue;
804+
}
805+
806+
if (isset($v['namespace'])) {
807+
$values[$k]['services'] = [];
808+
continue;
809+
}
810+
811+
if (!\is_array($v)) {
812+
$values[$v]['services'] = [];
813+
unset($values[$k]);
814+
continue;
815+
}
816+
817+
$tmp = $v;
818+
unset($values[$k]);
819+
$values[$k]['services'] = $tmp;
820+
}
821+
822+
return $values;
823+
})
824+
->end()
825+
->arrayPrototype()
826+
->fixXmlConfig('service')
827+
->children()
828+
->arrayNode('services')
829+
->prototype('scalar')->end()
830+
->end()
831+
->end()
832+
->end()
833+
->end()
795834
->end()
796835
->end()
797836
->end()

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
108108
use Symfony\Component\Translation\Translator;
109109
use Symfony\Component\Validator\ConstraintValidatorInterface;
110+
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
110111
use Symfony\Component\Validator\ObjectInitializerInterface;
111112
use Symfony\Component\WebLink\HttpHeaderSerializer;
112113
use Symfony\Component\Workflow;
@@ -280,7 +281,8 @@ public function load(array $configs, ContainerBuilder $container)
280281
$container->removeDefinition('console.command.messenger_debug');
281282
}
282283

283-
$this->registerValidationConfiguration($config['validation'], $container, $loader);
284+
$propertyInfoEnabled = $this->isConfigEnabled($container, $config['property_info']);
285+
$this->registerValidationConfiguration($config['validation'], $container, $loader, $propertyInfoEnabled);
284286
$this->registerEsiConfiguration($config['esi'], $container, $loader);
285287
$this->registerSsiConfiguration($config['ssi'], $container, $loader);
286288
$this->registerFragmentsConfiguration($config['fragments'], $container, $loader);
@@ -301,7 +303,7 @@ public function load(array $configs, ContainerBuilder $container)
301303
$this->registerSerializerConfiguration($config['serializer'], $container, $loader);
302304
}
303305

304-
if ($this->isConfigEnabled($container, $config['property_info'])) {
306+
if ($propertyInfoEnabled) {
305307
$this->registerPropertyInfoConfiguration($container, $loader);
306308
}
307309

@@ -1152,7 +1154,7 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder
11521154
}
11531155
}
11541156

1155-
private function registerValidationConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
1157+
private function registerValidationConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, bool $propertyInfoEnabled)
11561158
{
11571159
if (!$this->validatorConfigEnabled = $this->isConfigEnabled($container, $config)) {
11581160
return;
@@ -1203,6 +1205,11 @@ private function registerValidationConfiguration(array $config, ContainerBuilder
12031205
if (!$container->getParameter('kernel.debug')) {
12041206
$validatorBuilder->addMethodCall('setMetadataCache', [new Reference('validator.mapping.cache.symfony')]);
12051207
}
1208+
1209+
$container->setParameter('validator.auto_mapping', $config['auto_mapping']);
1210+
if (!$propertyInfoEnabled || !$config['auto_mapping'] || !class_exists(PropertyInfoLoader::class)) {
1211+
$container->removeDefinition('validator.property_info_loader');
1212+
}
12061213
}
12071214

12081215
private function registerValidatorMapping(ContainerBuilder $container, array $config, array &$files)

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
use Symfony\Component\Translation\DependencyInjection\TranslationExtractorPass;
5454
use Symfony\Component\Translation\DependencyInjection\TranslatorPass;
5555
use Symfony\Component\Translation\DependencyInjection\TranslatorPathsPass;
56+
use Symfony\Component\Validator\DependencyInjection\AddAutoMappingConfigurationPass;
5657
use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass;
5758
use Symfony\Component\Validator\DependencyInjection\AddValidatorInitializersPass;
5859

@@ -124,6 +125,7 @@ public function build(ContainerBuilder $container)
124125
$container->addCompilerPass(new TestServiceContainerRealRefPass(), PassConfig::TYPE_AFTER_REMOVING);
125126
$this->addCompilerPassIfExists($container, AddMimeTypeGuesserPass::class);
126127
$this->addCompilerPassIfExists($container, MessengerPass::class);
128+
$this->addCompilerPassIfExists($container, AddAutoMappingConfigurationPass::class);
127129
$container->addCompilerPass(new RegisterReverseContainerPass(true));
128130
$container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING);
129131

0 commit comments

Comments
 (0)
0