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

Skip to content

Commit 74de5e6

Browse files
committed
[Validator][DoctrineBridge][FWBundle] Automatic data validation
1 parent f54c89c commit 74de5e6

File tree

19 files changed

+937
-3
lines changed

19 files changed

+937
-3
lines changed
Lines 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 10000
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
@@ -789,6 +789,45 @@ private function addValidationSection(ArrayNodeDefinition $rootNode)
789789
->end()
790790
->end()
791791
->end()
792+
->arrayNode('auto_mapping')
793+
->useAttributeAsKey('namespace')
794+
->normalizeKeys(false)
795+
->beforeNormalization()
796+
->ifArray()
797+
->then(function (array $values): array {
798+
foreach ($values as $k => $v) {
799+
if (isset($v['service'])) {
800+
continue;
801+
}
802+
803+
if (isset($v['namespace'])) {
804+
$values[$k]['services'] = [];
805+
continue;
806+
}
807+
808+
if (!\is_array($v)) {
809+
$values[$v]['services'] = [];
810+
unset($values[$k]);
811+
continue;
812+
}
813+
814+
$tmp = $v;
815+
unset($values[$k]);
816+
$values[$k]['services'] = $tmp;
817+
}
818+
819+
return $values;
820+
})
821+
->end()
822+
->arrayPrototype()
823+
->fixXmlConfig('service')
824+
->children()
825+
->arrayNode('services')
826+
->prototype('scalar')->end()
827+
->end()
828+
->end()
829+
->end()
830+
->end()
792831
->end()
793832
->end()
794833
->end()

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
104104
use Symfony\Component\Translation\Translator;
105105
use Symfony\Component\Validator\ConstraintValidatorInterface;
106+
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
106107
use Symfony\Component\Validator\ObjectInitializerInterface;
107108
use Symfony\Component\WebLink\HttpHeaderSerializer;
108109
use Symfony\Component\Workflow;
@@ -272,7 +273,8 @@ public function load(array $configs, ContainerBuilder $container)
272273
$container->removeDefinition('console.command.messenger_debug');
273274
}
274275

275-
$this->registerValidationConfiguration($config['validation'], $container, $loader);
276+
$propertyInfoEnabled = $this->isConfigEnabled($container, $config['property_info']);
277+
$this->registerValidationConfiguration($config['validation'], $container, $loader, $propertyInfoEnabled);
276278
$this->registerEsiConfiguration($config['esi'], $container, $loader);
277279
$this->registerSsiConfiguration($config['ssi'], $container, $loader);
278280
$this->registerFragmentsConfiguration($config['fragments'], $container, $loader);
@@ -293,7 +295,7 @@ public function load(array $configs, ContainerBuilder $container)
293295
$this->registerSerializerConfiguration($config['serializer'], $container, $loader);
294296
}
295297

296-
if ($this->isConfigEnabled($container, $config['property_info'])) {
298+
if ($propertyInfoEnabled) {
297299
$this->registerPropertyInfoConfiguration($container, $loader);
298300
}
299301

@@ -1117,7 +1119,7 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder
11171119
}
11181120
}
11191121

1120-
private function registerValidationConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
1122+
private function registerValidationConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, bool $propertyInfoEnabled)
11211123
{
11221124
if (!$this->validatorConfigEnabled = $this->isConfigEnabled($container, $config)) {
11231125
return;
@@ -1168,6 +1170,11 @@ private function registerValidationConfiguration(array $config, ContainerBuilder
11681170
if (!$container->getParameter('kernel.debug')) {
11691171
$validatorBuilder->addMethodCall('setMetadataCache', [new Reference('validator.mapping.cache.symfony')]);
11701172
}
1173+
1174+
$container->setParameter('validator.auto_mapping', $config['auto_mapping']);
1175+
ECC1 if (!$propertyInfoEnabled || !$config['auto_mapping'] || !class_exists(PropertyInfoLoader::class)) {
1176+
$container->removeDefinition('validator.property_info_loader');
1177+
}
11711178
}
11721179

11731180
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
@@ -52,6 +52,7 @@
5252
use Symfony\Component\Translation\DependencyInjection\TranslationExtractorPass;
5353
use Symfony\Component\Translation\DependencyInjection\TranslatorPass;
5454
use Symfony\Component\Translation\DependencyInjection\TranslatorPathsPass;
55+
use Symfony\Component\Validator\DependencyInjection\AddAutoMappingConfigurationPass;
5556
use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass;
5657
use Symfony\Component\Validator\DependencyInjection\AddValidatorInitializersPass;
5758
use Symfony\Component\Workflow\DependencyInjection\ValidateWorkflowsPass;
@@ -125,6 +126,7 @@ public function build(ContainerBuilder $container)
125126
$container->addCompilerPass(new TestServiceContainerRealRefPass(), PassConfig::TYPE_AFTER_REMOVING);
126127
$this->addCompilerPassIfExists($container, AddMimeTypeGuesserPass::class);
127128
$this->addCompilerPassIfExists($container, MessengerPass::class);
129+
$this->addCompilerPassIfExists($container, AddAutoMappingConfigurationPass::class);
128130

129131
if ($container->getParameter('kernel.debug')) {
130132
$container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);

0 commit comments

Comments
 (0)
0