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
  • Tests/DependencyInjection
  • Component/Validator
  • 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 F438 (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
    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+
    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