Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1dd97e4

Browse files
committed
[Validator][DoctrineBridge][FWBundle] Automatic data validation
1 parent 84ada0c commit 1dd97e4

File tree

8 files changed

+291
-4
lines changed

8 files changed

+291
-4
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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\Mapping\ClassMetadataFactory;
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\ConstraintChecker;
22+
use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;
23+
24+
/**
25+
* Guesses and loads the appropriate constraints using Doctrine's metadata.
26+
*
27+
* @author Kévin Dunglas <[email protected]>
28+
*/
29+
final class DoctrineLoader implements LoaderInterface
30+
{
31+
use ConstraintChecker;
32+
33+
private $classMetadataFactory;
34+
35+
public function __construct(ClassMetadataFactory $classMetadataFactory)
36+
{
37+
$this->classMetadataFactory = $classMetadataFactory;
38+
}
39+
40+
/**
41+
* {@inheritdoc}
42+
*/
43+
public function loadClassMetadata(ClassMetadata $metadata): bool
44+
{
45+
try {
46+
$doctrineMetadata = $this->classMetadataFactory->getMetadataFor($metadata->getClassName());
47+
} catch (MappingException $exception) {
48+
return false;
49+
} catch (OrmMappingException $exception) {
50+
return false;
51+
}
52+
53+
if (!$doctrineMetadata instanceof ClassMetadataInfo) {
54+
return false;
55+
}
56+
57+
/* Available keys:
58+
- type
59+
- scale
60+
- length
61+
- unique
62+
- nullable
63+
- precision
64+
*/
65+
66+
$uniqueFields = array();
67+
68+
// Type and nullable aren't handled here, use the PropertyInfo Loader instead.
69+
foreach ($doctrineMetadata->fieldMappings as $mapping) {
70+
// TODO: Currently, I don't add a constraint if one of the same type already exists, but it's maybe safer to add both (min/max)?
71+
if (null !== $mapping['length'] && !$this->propertyHasConstraint($metadata, Length::class, $mapping['fieldName'])) {
72+
$metadata->addPropertyConstraint($mapping['fieldName'], new Length(array('max' => $mapping['length'])));
73+
}
74+
75+
if (true === $mapping['unique']) {
76+
$uniqueFields[] = $mapping['fieldName'];
77+
}
78+
}
79+
80+
if ($uniqueFields && !$this->classHasConstraint($metadata, UniqueEntity::class)) {
81+
$metadata->addConstraint(new UniqueEntity(array('fields' => $uniqueFields)));
82+
}
83+
84+
return true;
85+
}
86+
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Doctrine\Common\Annotations\Reader;
1515
use Doctrine\Common\Annotations\AnnotationRegistry;
16+
use Symfony\Bridge\Doctrine\Validator\DoctrineLoader;
1617
use Symfony\Bridge\Monolog\Processor\DebugProcessor;
1718
use Symfony\Bridge\Twig\Extension\CsrfExtension;
1819
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -72,6 +73,7 @@
7273
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
7374
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
7475
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
76+
use Symfony\Component\PropertyInfo\Type;
7577
use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader;
7678
use Symfony\Component\Routing\Loader\AnnotationFileLoader;
7779
use Symfony\Component\Security\Core\Security;
@@ -1080,6 +1082,23 @@ private function registerValidationConfiguration(array $config, ContainerBuilder
10801082
if (!$container->getParameter('kernel.debug')) {
10811083
$validatorBuilder->addMethodCall('setMetadataCache', array(new Reference('validator.mapping.cache.symfony')));
10821084
}
1085+
1086+
// TODO: add a configuration option to enable or disable this feature (validator.auto_validate: ['@validator.property_info_loader', '@doctrine_bundle.validator_loader'])?
1087+
if (class_exists(Type::class)) {
1088+
$validatorBuilder->addMethodCall('addLoader', array(new Reference('validator.property_info_loader')));
1089+
} else {
1090+
$container->removeDefinition('validator.property_info_loader');
1091+
}
1092+
1093+
// TODO: move this in a compiler pass in DoctrineBundle
1094+
// TODO: support multiple entity managers (reuse DoctrineBundle's extension logic)
1095+
// TODO: Use XML definitions instead
1096+
if (class_exists(DoctrineLoader::class)) {
1097+
$definition = $container->register('doctrine_loader', DoctrineLoader::class);
1098+
$definition->addArgument(new Reference('doctrine.orm.default_entity_manager.metadata_factory'));
1099+
1100+
$validatorBuilder->addMethodCall('addLoader', array(new Reference('doctrine_loader')));
1101+
}
10831102
}
10841103

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

src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,10 @@
6161
<argument></argument>
6262
<tag name="validator.constraint_validator" alias="Symfony\Component\Validator\Constraints\EmailValidator" />
6363
</service>
64+
65+
<service id="validator.property_info_loader" class="Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader">
66+
<argument type="service" id="Symfony\Component\PropertyInfo\PropertyListExtractorInterface" />
67+
<argument type="service" id="Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface" />
68+
</service>
6469
</services>
6570
</container>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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\Component\Validator;
13+
14+
use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;
15+
16+
/**
17+
* Allows to register extra metadata loaders (for instance the Doctrine one).
18+
*
19+
* @author Kévin Dunglas <[email protected]>
20+
*/
21+
interface ExtraLoaderValidatorBuilderInterface extends ValidatorBuilderInterface
22+
{
23+
/**
24+
* Adds a metadata loader at the end of the chain.
25+
*
26+
* @return $this
27+
*/
28+
public function addLoader(LoaderInterface $loader): self;
29+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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\Component\Validator\Mapping;
13+
14+
/**
15+
* Helper methods to check if a property or a class has a given constraint.
16+
*
17+
* @internal
18+
*
19+
* @author Kévin Dunglas <[email protected]>
20+
*/
21+
trait ConstraintChecker
22+
{
23+
private function propertyHasConstraint(ClassMetadata $metadata, string $constraintType, string $fieldName): bool
24+
{
25+
foreach ($metadata->getPropertyMetadata($fieldName) as $propertyMetadata) {
26+
foreach ($propertyMetadata->getConstraints() as $constraint) {
27+
if (is_a($constraint, $constraintType, true)) {
28+
return true;
29+
}
30+
}
31+
}
32+
33+
return false;
34+
}
35+
36+
private function classHasConstraint(ClassMetadata $metadata, string $constraintType): bool
37+
{
38+
foreach ($metadata->getConstraints() as $constraint) {
39+
if (is_a($constraint, $constraintType, true)) {
40+
return true;
41+
}
42+
}
43+
44+
return false;
45+
}
46+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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\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\NotNull;
18+
use Symfony\Component\Validator\Constraints\Type;
19+
use Symfony\Component\Validator\Mapping\ClassMetadata;
20+
use Symfony\Component\Validator\Mapping\ConstraintChecker;
21+
22+
/**
23+
* Guesses and loads the appropriate constraints using PropertyInfo.
24+
*
25+
* @author Kévin Dunglas <[email protected]>
26+
*/
27+
class PropertyInfoLoader implements LoaderInterface
28+
{
29+
use ConstraintChecker;
30+
31+
private $listExtractor;
32+
private $typeExtractor;
33+
34+
public function __construct(PropertyListExtractorInterface $listExtractor, PropertyTypeExtractorInterface $typeExtractor)
35+
{
36+
$this->listExtractor = $listExtractor;
37+
$this->typeExtractor = $typeExtractor;
38+
}
39+
40+
/**
41+
* {@inheritdoc}
42+
*/
43+
public function loadClassMetadata(ClassMetadata $metadata)
44+
{
45+
$class = $metadata->getClassName();
46+
if (!$properties = $this->listExtractor->getProperties($class)) {
47+
return;
48+
}
49+
50+
foreach ($properties as $property) {
51+
$types = $this->typeExtractor->getTypes($metadata->getClassName(), $property);
52+
if (null === $types) {
53+
continue;
54+
}
55+
56+
$builtinTypes = array();
57+
$nullable = false;
58+
$scalar = true;
59+
60+
foreach ($types as $type) {
61+
$builtinTypes[] = $type->getBuiltinType();
62+
63+
if ($scalar && !\in_array($type->getBuiltinType(), array(PropertyInfoType::BUILTIN_TYPE_INT, PropertyInfoType::BUILTIN_TYPE_FLOAT, PropertyInfoType::BUILTIN_TYPE_STRING, PropertyInfoType::BUILTIN_TYPE_BOOL))) {
64+
$scalar = false;
65+
}
66+
67+
if (!$nullable && $type->isNullable()) {
68+
$nullable = true;
69+
}
70+
}
71+
72+
if (!$this->propertyHasConstraint($metadata, Type::class, $property)) {
73+
if (1 === \count($builtinTypes)) {
74+
$metadata->addPropertyConstraint($property, new Type(array('type' => $builtinTypes[0])));
75+
} elseif ($scalar) {
76+
$metadata->addPropertyConstraint($property, new Type(array('type' => 'scalar')));
77+
}
78+
}
79+
80+
if (!$nullable && !$this->propertyHasConstraint($metadata, NotNull::class, $property)) {
81+
$metadata->addPropertyConstraint($property, new NotNull());
82+
}
83+
}
84+
}
85+
}

src/Symfony/Component/Validator/ValidatorBuilder.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,18 @@
3535
*
3636
* @author Bernhard Schussek <[email protected]>
3737
*/
38-
class ValidatorBuilder implements ValidatorBuilderInterface
38+
class ValidatorBuilder implements ExtraLoaderValidatorBuilderInterface
3939
{
4040
private $initializers = array();
4141
private $xmlMappings = array();
4242
private $yamlMappings = array();
4343
private $methodMappings = array();
4444

45+
/**
46+
* @var LoaderInterface[]
47+
*/
48+
private $extraLoaders = array();
49+
4550
/**
4651
* @var Reader|null
4752
*/
@@ -92,6 +97,16 @@ public function addObjectInitializers(array $initializers)
9297
return $this;
9398
}
9499

100+
/**
101+
* {@inheritdoc}
102+
*/
103+
public function addLoader(LoaderInterface $loader): ExtraLoaderValidatorBuilderInterface
104+
{
105+
$this->extraLoaders[] = $loader;
106+
107+
return $this;
108+
}
109+
95110
/**
96111
* {@inheritdoc}
97112
*/
@@ -213,8 +228,8 @@ public function disableAnnotationMapping()
213228
*/
214229
public function setMetadataFactory(MetadataFactoryInterface $metadataFactory)
215230
{
216-
if (count($this->xmlMappings) > 0 || count($this->yamlMappings) > 0 || count($this->methodMappings) > 0 || null !== $this->annotationReader) {
217-
throw new ValidatorException('You cannot set a custom metadata factory after adding custom mappings. You should do either of both.');
231+
if ($this->xmlMappings || $this->yamlMappings || $this->methodMappings || $this->extraLoaders || null !== $this->annotationReader) {
232+
throw new ValidatorException('You cannot set a custom metadata factory after adding custom mappings or extra loaders. You should do either of both.');
218233
}
219234

220235
$this->metadataFactory = $metadataFactory;
@@ -289,7 +304,7 @@ public function getLoaders()
289304
$loaders[] = new AnnotationLoader($this->annotationReader);
290305
}
291306

292-
return $loaders;
307+
return array_merge($loaders, $this->extraLoaders);
293308
}
294309

295310
/**

src/Symfony/Component/Validator/composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"symfony/expression-language": "~3.4|~4.0",
3333
"symfony/cache": "~3.4|~4.0",
3434
"symfony/property-access": "~3.4|~4.0",
35+
"symfony/property-info": "~3.4|~4.0",
3536
"doctrine/annotations": "~1.0",
3637
"doctrine/cache": "~1.0",
3738
"egulias/email-validator": "^1.2.8|~2.0"
@@ -53,6 +54,7 @@
5354
"symfony/config": "",
5455
"egulias/email-validator": "Strict (RFC compliant) email validation",
5556
"symfony/property-access": "For accessing properties within comparison constraints",
57+
"symfony/property-info": "To automatically add NotNull and Type constraints",
5658
"symfony/expression-language": "For using the Expression validator"
5759
},
5860
"autoload": {

0 commit comments

Comments
 (0)