8000 [Validator][DoctrineBridge][FWBundle] Automatic data validation · symfony/symfony@0f3cae5 · 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 0f3cae5

Browse files
committed
[Validator][DoctrineBridge][FWBundle] Automatic data validation
1 parent f54c89c commit 0f3cae5

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(array('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 array(
89+
array(true, null),
90+
array(true, '{^'.preg_quote(DoctrineLoaderEntity::class).'$|^'.preg_quote(Entity::class).'$}'),
91+
array(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(array('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(array('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 = array();
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'] = array();
805+
continue;
806+
}
807+
808+
if (!\is_array($v)) {
809+
$values[$v]['services'] = array();
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+
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