Skip to content

Commit b197af4

Browse files
committed
[Validator][DoctrineBridge][FWBundle] Automatic data validation
1 parent 40f8423 commit b197af4

File tree

6 files changed

+539
-0
lines changed

6 files changed

+539
-0
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Reference;
17+
18+
/**
19+
* Injects the automapping configuration as last argument of loaders tagged with the "validator.auto_mapper" tag.
20+
*
21+
* @author Kévin Dunglas <dunglas@gmail.com>
22+
*/
23+
class AddAutoMappingConfigurationPass implements CompilerPassInterface
24+
{
25+
private $validatorBuilderService;
26+
private $tag;
27+
28+
public function __construct(string $validatorBuilderService = 'validator.builder', string $tag = 'validator.auto_mapper')
29+
{
30+
$this->validatorBuilderService = $validatorBuilderService;
31+
$this->tag = $tag;
32+
}
33+
34+
/**
35+
* {@inheritdoc}
36+
*/
37+
public function process(ContainerBuilder $container)
38+
{
39+
if (!$container->hasParameter('validator.auto_mapping') || !$container->hasDefinition($this->validatorBuilderService)) {
40+
return;
41+
}
42+
43+
$config = $container->getParameter('validator.auto_mapping');
44+
45+
$globalNamespaces = [];
46+
$servicesToNamespaces = [];
47+
foreach ($config as $namespace => $value) {
48+
if ([] === $value['services']) {
49+
$globalNamespaces[] = $namespace;
50+
51+
continue;
52+
}
53+
54+
foreach ($value['services'] as $service) {
55+
$servicesToNamespaces[$service][] = $namespace;
56+
}
57+
}
58+
59+
$validatorBuilder = $container->getDefinition($this->validatorBuilderService);
60+
foreach ($container->findTaggedServiceIds($this->tag) as $id => $tags) {
61+
$regexp = $this->getRegexp(array_merge($globalNamespaces, $servicesToNamespaces[$id] ?? []));
62+
63+
$container->getDefinition($id)->setArgument('$classValidatorRegexp', $regexp);
64+
$validatorBuilder->addMethodCall('addLoader', [new Reference($id)]);
65+
}
66+
67+
$container->getParameterBag()->remove('validator.auto_mapping');
68+
}
69+
70+
/**
71+
* Builds a regexp to check if a class is auto-mapped.
72+
*/
73+
private function getRegexp(array $patterns): string
74+
{
75+
$regexps = [];
76+
foreach ($patterns as $pattern) {
77+
// Escape namespace
78+
$regex = preg_quote(ltrim($pattern, '\\'));
79+
80+
// Wildcards * and **
81+
$regex = strtr($regex, ['\\*\\*' => '.*?', '\\*' => '[^\\\\]*?']);
82+
83+
// If this class does not end by a slash, anchor the end
84+
if ('\\' !== substr($regex, -1)) {
85+
$regex .= '$';
86+
}
87+
88+
$regexps[] = '^'.$regex;
89+
}
90+
91+
return sprintf('{%s}', implode('|', $regexps));
92+
}
93+
}

Mapping/Loader/PropertyInfoLoader.php

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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\PropertyInfo\PropertyListExtractorInterface;
15+
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
16+
use Symfony\Component\PropertyInfo\Type as PropertyInfoType;
17+
use Symfony\Component\Validator\Constraints\All;
18+
use Symfony\Component\Validator\Constraints\NotBlank;
19+
use Symfony\Component\Validator\Constraints\NotNull;
20+
use Symfony\Component\Validator\Constraints\Type;
21+
use Symfony\Component\Validator\Mapping\ClassMetadata;
22+
23+
/**
24+
* Guesses and loads the appropriate constraints using PropertyInfo.
25+
*
26+
* @author Kévin Dunglas <dunglas@gmail.com>
27+
*/
28+
final class PropertyInfoLoader implements LoaderInterface
29+
{
30+
private $listExtractor;
31+
private $typeExtractor;
32+
private $classValidatorRegexp;
33+
34+
public function __construct(PropertyListExtractorInterface $listExtractor, PropertyTypeExtractorInterface $typeExtractor, string $classValidatorRegexp = null)
35+
{
36+
$this->listExtractor = $listExtractor;
37+
$this->typeExtractor = $typeExtractor;
38+
$this->classValidatorRegexp = $classValidatorRegexp;
39+
}
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
public function loadClassMetadata(ClassMetadata $metadata)
45+
{
46+
$className = $metadata->getClassName();
47+
if (null !== $this->classValidatorRegexp && !preg_match($this->classValidatorRegexp, $className)) {
48+
return false;
49+
}
50+
51+
if (!$properties = $this->listExtractor->getProperties($className)) {
52+
return false;
53+
}
54+
55+
foreach ($properties as $property) {
56+
$types = $this->typeExtractor->getTypes($className, $property);
57+
if (null === $types) {
58+
continue;
59+
}
60+
61+
$hasTypeConstraint = false;
62+
$hasNotNullConstraint = false;
63+
$hasNotBlankConstraint = false;
64+
$allConstraint = null;
65+
foreach ($metadata->getPropertyMetadata($property) as $propertyMetadata) {
66+
foreach ($propertyMetadata->getConstraints() as $constraint) {
67+
if ($constraint instanceof Type) {
68+
$hasTypeConstraint = true;
69+
} elseif ($constraint instanceof NotNull) {
70+
$hasNotNullConstraint = true;
71+
} elseif ($constraint instanceof NotBlank) {
72+
$hasNotBlankConstraint = true;
73+
} elseif ($constraint instanceof All) {
74+
$allConstraint = $constraint;
75+
}
76+
}
77+
}
78+
79+
$builtinTypes = [];
80+
$nullable = false;
81+
$scalar = true;
82+
foreach ($types as $type) {
83+
$builtinTypes[] = $type->getBuiltinType();
84+
85+
if ($scalar && !\in_array($type->getBuiltinType(), [PropertyInfoType::BUILTIN_TYPE_INT, PropertyInfoType::BUILTIN_TYPE_FLOAT, PropertyInfoType::BUILTIN_TYPE_STRING, PropertyInfoType::BUILTIN_TYPE_BOOL], true)) {
86+
$scalar = false;
87+
}
88+
89+
if (!$nullable && $type->isNullable()) {
90+
$nullable = true;
91+
}
92+
}
93+
if (!$hasTypeConstraint) {
94+
if (1 === \count($builtinTypes)) {
95+
if ($types[0]->isCollection() && (null !== $collectionValueType = $types[0]->getCollectionValueType())) {
96+
$this->handleAllConstraint($property, $allConstraint, $collectionValueType, $metadata);
97+
}
98+
99+
$metadata->addPropertyConstraint($property, $this->getTypeConstraint($builtinTypes[0], $types[0]));
100+
} elseif ($scalar) {
101+
$metadata->addPropertyConstraint($property, new Type(['type' => 'scalar']));
102+
}
103+
}
104+
105+
if (!$nullable && !$hasNotBlankConstraint && !$hasNotNullConstraint) {
106+
$metadata->addPropertyConstraint($property, new NotNull());
107+
}
108+
}
109+
110+
return true;
111+
}
112+
113+
private function getTypeConstraint(string $builtinType, PropertyInfoType $type): Type
114+
{
115+
if (PropertyInfoType::BUILTIN_TYPE_OBJECT === $builtinType && null !== $className = $type->getClassName()) {
116+
return new Type(['type' => $className]);
117+
}
118+
119+
return new Type(['type' => $builtinType]);
120+
}
121+
122+
private function handleAllConstraint(string $property, ?All $allConstraint, PropertyInfoType $propertyInfoType, ClassMetadata $metadata)
123+
{
124+
$containsTypeConstraint = false;
125+
$containsNotNullConstraint = false;
126+
if (null !== $allConstraint) {
127+
foreach ($allConstraint->constraints as $constraint) {
128+
if ($constraint instanceof Type) {
129+
$containsTypeConstraint = true;
130+
} elseif ($constraint instanceof NotNull) {
131+
$containsNotNullConstraint = true;
132+
}
133+
}
134+
}
135+
136+
$constraints = [];
137+
if (!$containsNotNullConstraint && !$propertyInfoType->isNullable()) {
138+
$constraints[] = new NotNull();
139+
}
140+
141+
if (!$containsTypeConstraint) {
142+
$constraints[] = $this->getTypeConstraint($propertyInfoType->getBuiltinType(), $propertyInfoType);
143+
}
144+
145+
if (null === $allConstraint) {
146+
$metadata->addPropertyConstraint($property, new All(['constraints' => $constraints]));
147+
} else {
148+
$allConstraint->constraints = array_merge($allConstraint->constraints, $constraints);
149+
}
150+
}
151+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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\Tests\DependencyInjection;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\Validator\DependencyInjection\AddAutoMappingConfigurationPass;
17+
use Symfony\Component\Validator\Tests\Fixtures\PropertyInfoLoaderEntity;
18+
use Symfony\Component\Validator\ValidatorBuilder;
19+
20+
/**
21+
* @author Kévin Dunglas <dunglas@gmail.com>
22+
*/
23+
class AddAutoMappingConfigurationPassTest extends TestCase
24+
{
25+
public function testNoConfigParameter()
26+
{
27+
$container = new ContainerBuilder();
28+
(new AddAutoMappingConfigurationPass())->process($container);
29+
$this->assertCount(1, $container->getDefinitions());
30+
}
31+
32+
public function testNoValidatorBuilder()
33+
{
34+
$container = new ContainerBuilder();
35+
(new AddAutoMappingConfigurationPass())->process($container);
36+
$this->assertCount(1, $container->getDefinitions());
37+
}
38+
39+
/**
40+
* @dataProvider mappingProvider
41+
*/
42+
public function testProcess(string $namespace, array $services, string $expectedRegexp)
43+
{
44+
$container = new ContainerBuilder();
45+
$container->setParameter('validator.auto_mapping', [
46+
'App\\' => ['services' => []],
47+
$namespace => ['services' => $services],
48+
]);
49+
50+
$container->register('validator.builder', ValidatorBuilder::class);
51+
foreach ($services as $service) {
52+
$container->register($service)->addTag('validator.auto_mapper');
53+
}
54+
55+
(new AddAutoMappingConfigurationPass())->process($container);
56+
57+
foreach ($services as $service) {
58+
$this->assertSame($expectedRegexp, $container->getDefinition($service)->getArgument('$classValidatorRegexp'));
59+
}
60+
$this->assertCount(\count($services), $container->getDefinition('validator.builder')->getMethodCalls());
61+
}
62+
63+
public function mappingProvider(): array
64+
{
65+
return [
66+
['Foo\\', ['foo', 'baz'], '{^App\\\\|^Foo\\\\}'],
67+
[PropertyInfoLoaderEntity::class, ['class'], '{^App\\\\|^Symfony\\\\Component\\\\Validator\\\\Tests\\\\Fixtures\\\\PropertyInfoLoaderEntity$}'],
68+
['Symfony\Component\Validator\Tests\Fixtures\\', ['trailing_antislash'], '{^App\\\\|^Symfony\\\\Component\\\\Validator\\\\Tests\\\\Fixtures\\\\}'],
69+
['Symfony\Component\Validator\Tests\Fixtures\\*', ['trailing_star'], '{^App\\\\|^Symfony\\\\Component\\\\Validator\\\\Tests\\\\Fixtures\\\\[^\\\\]*?$}'],
70+
['Symfony\Component\Validator\Tests\Fixtures\\**', ['trailing_double_star'], '{^App\\\\|^Symfony\\\\Component\\\\Validator\\\\Tests\\\\Fixtures\\\\.*?$}'],
71+
];
72+
}
73+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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\Tests\Fixtures;
13+
14+
use Symfony\Component\Validator\Constraints as Assert;
15+
16+
/**
17+
* @author Kévin Dunglas <dunglas@gmail.com>
18+
*/
19+
class PropertyInfoLoaderEntity
20+
{
21+
public $nullableString;
22+
public $string;
23+
public $scalar;
24+
public $object;
25+
public $collection;
26+
27+
/**
28+
* @Assert\Type(type="int")
29+
*/
30+
public $alreadyMappedType;
31+
32+
/**
33+
* @Assert\NotNull
34+
*/
35+
public $alreadyMappedNotNull;
36+
37+
/**
38+
* @Assert\NotBlank
39+
*/
40+
public $alreadyMappedNotBlank;
41+
42+
/**
43+
* @Assert\All({
44+
* @Assert\Type(type="string"),
45+
* @Assert\Iban
46+
* })
47+
*/
48+
public $alreadyPartiallyMappedCollection;
49+
}

0 commit comments

Comments
 (0)