Skip to content

Commit f9cbf96

Browse files
committed
[Validator][DoctrineBridge][FWBundle] Automatic data validation
1 parent f62d40c commit f9cbf96

File tree

3 files changed

+273
-0
lines changed

3 files changed

+273
-0
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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+
}

Validator/DoctrineLoader.php

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 <[email protected]>
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 <[email protected]>
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+
}

0 commit comments

Comments
 (0)