8000 [Validator] Add AutoMapping constraint to enable or disable auto-vali… · dunglas/symfony@f6519ce · GitHub
[go: up one dir, main page]

Skip to content

Commit f6519ce

Browse files
dunglasxabbuh
authored andcommitted
[Validator] Add AutoMapping constraint to enable or disable auto-validation
1 parent 42be5f8 commit f6519ce

File tree

14 files changed

+406
-31
lines changed
  • Component/Validator
  • 14 files changed

    +406
    -31
    lines changed

    src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php

    Lines changed: 6 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -69,6 +69,12 @@ class DoctrineLoaderEntity extends DoctrineLoaderParentEntity
    6969
    /** @ORM\Column(type="simple_array", length=100) */
    7070
    public $simpleArrayField = [];
    7171

    72+
    /**
    73+
    * @ORM\Column(length=10)
    74+
    * @Assert\DisableAutoMapping
    75+
    */
    76+
    public $noAutoMapping;
    77+
    7278
    public static function loadValidatorMetadata(ClassMetadata $metadata): void
    7379
    {
    7480
    $allowEmptyString = property_exists(Assert\Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : [];
    Lines changed: 41 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,41 @@
    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\Component\Validator\Constraints as Assert;
    16+
    17+
    /**
    18+
    * @ORM\Entity
    19+
    * @Assert\DisableAutoMapping
    20+
    *
    21+
    * @author Kévin Dunglas <dunglas@gmail.com>
    22+
    */
    23+
    class DoctrineLoaderNoAutoMappingEntity
    24+
    {
    25+
    /**
    26+
    * @ORM\Id
    27+
    * @ORM\Column
    28+
    */
    29+
    public $id;
    30+
    31+
    /**
    32+
    * @ORM\Column(length=20, unique=true)
    33+
    */
    34+
    public $maxLength;
    35+
    36+
    /**
    37+
    * @Assert\EnableAutoMapping
    38+
    * @ORM\Column(length=20)
    39+
    */
    40+
    public $autoMappingExplicitlyEnabled;
    41+
    }

    src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php

    Lines changed: 39 additions & 3 deletions
    Original file line numberDiff line numberDiff line change
    @@ -17,12 +17,15 @@
    1717
    use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEmbed;
    1818
    use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEntity;
    1919
    use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderNestedEmbed;
    20+
    use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderNoAutoMappingEntity;
    2021
    use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderParentEntity;
    2122
    use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
    2223
    use Symfony\Bridge\Doctrine\Validator\DoctrineLoader;
    24+
    use Symfony\Component\Validator\Constraints\DisableAutoMapping;
    2325
    use Symfony\Component\Validator\Constraints\Length;
    2426
    use Symfony\Component\Validator\Mapping\CascadingStrategy;
    2527
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    28+
    use Symfony\Component\Validator\Mapping\Loader\AutoMappingTrait;
    2629
    use Symfony\Component\Validator\Mapping\TraversalStrategy;
    2730
    use Symfony\Component\Validator\Tests\Fixtures\Entity;
    2831
    use Symfony\Component\Validator\Validation;
    @@ -33,12 +36,15 @@
    3336
    */
    3437
    class DoctrineLoaderTest extends TestCase
    3538
    {
    36-
    public function testLoadClassMetadata()
    39+
    protected function setUp(): void
    3740
    {
    38-
    if (!method_exists(ValidatorBuilder::class, 'addLoader')) {
    39-
    $this->markTestSkipped('Auto-mapping requires symfony/validation 4.2+');
    41+
    if (!trait_exists(AutoMappingTrait::class)) {
    42+
    $this->markTestSkipped('Auto-mapping requires symfony/validation 4.4+');
    4043
    }
    44+
    }
    4145

    46+
    public function testLoadClassMetadata()
    47+
    {
    4248
    $validator = Validation::createValidatorBuilder()
    4349
    ->addMethodMapping('loadValidatorMetadata')
    4450
    ->enableAnnotationMapping()
    @@ -134,6 +140,12 @@ public function testLoadClassMetadata()
    134140
    $this->assertCount(1, $textFieldConstraints);
    135141
    $this->assertInstanceOf(Length::class, $textFieldConstraints[0]);
    136142
    $this->assertSame(1000, $textFieldConstraints[0]->max);
    143+
    144+
    $noAutoMappingMetadata = $classMetadata->getPropertyMetadata('noAutoMapping');
    145+
    $this->assertCount(1, $noAutoMappingMetadata);
    146+
    $noAutoMappingConstraints = $noAutoMappingMetadata[0]->getConstraints();
    147+
    $this->assertCount(1, $noAutoMappingConstraints);
    148+
    $this->assertInstanceOf(DisableAutoMapping::class, $noAutoMappingConstraints[0]);
    137149
    }
    138150

    139151
    public function testFieldMappingsConfiguration()
    @@ -180,4 +192,28 @@ public function regexpProvider()
    180192
    [false, '{^'.preg_quote(Entity::class).'$}'],
    181193
    ];
    182194
    }
    195+
    196+
    public function testClassNoAutoMapping()
    197+
    {
    198+
    if (!method_exists(ValidatorBuilder::class, 'addLoader')) {
    199+
    $this->markTestSkipped('Auto-mapping requires symfony/validation 4.2+');
    200+
    }
    201+
    202+
    $validator = Validation::createValidatorBuilder()
    203+
    ->enableAnnotationMapping()
    204+
    ->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager()))
    205+
    ->getValidator();
    206+
    207+
    $classMetadata = $validator->getMetadataFor(new DoctrineLoaderNoAutoMappingEntity());
    208+
    209+
    $classConstraints = $classMetadata->getConstraints();
    210+
    $this->assertCount(1, $classConstraints);
    211+
    $this->assertInstanceOf(DisableAutoMapping::class, $classConstraints[0]);
    212+
    213+
    $maxLengthMetadata = $classMetadata->getPropertyMetadata('maxLength');
    214+
    $this->assertEmpty($maxLengthMetadata);
    215+
    216+
    $autoMappingExplicitlyEnabledMetadata = $classMetadata->getPropertyMetadata('autoMappingExplicitlyEnabled');
    217+
    $this->assertCount(2, $autoMappingExplicitlyEnabledMetadata[0]->constraints);
    218+
    }
    183219
    }

    src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php

    Lines changed: 34 additions & 22 deletions
    Original file line numberDiff line numberDiff line change
    @@ -16,9 +16,12 @@
    1616
    use Doctrine\ORM\Mapping\ClassMetadataInfo;
    1717
    use Doctrine\ORM\Mapping\MappingException as OrmMappingException;
    1818
    use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
    19+
    use Symfony\Component\Validator\Constraints\DisableAutoMapping;
    20+
    use Symfony\Component\Validator\Constraints\EnableAutoMapping;
    1921
    use Symfony\Component\Validator\Constraints\Length;
    2022
    use Symfony\Component\Validator\Constraints\Valid;
    2123
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    24+
    use Symfony\Component\Validator\Mapping\Loader\AutoMappingTrait;
    2225
    use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;
    2326

    2427
    /**
    @@ -28,6 +31,8 @@
    2831
    */
    2932
    final class DoctrineLoader implements LoaderInterface
    3033
    {
    34+
    use AutoMappingTrait;
    35+
    3136
    private $entityManager;
    3237
    private $classValidatorRegexp;
    3338

    @@ -43,10 +48,6 @@ public function __construct(EntityManagerInterface $entityManager, string $class
    4348
    public function loadClassMetadata(ClassMetadata $metadata): bool
    4449
    {
    4550
    $className = $metadata->getClassName();
    46-
    if (null !== $this->classValidatorRegexp && !preg_match($this->classValidatorRegexp, $className)) {
    47-
    return false;
    48-
    }
    49-
    5051
    try {
    5152
    $doctrineMetadata = $this->entityManager->getClassMetadata($className);
    5253
    } catch (MappingException | OrmMappingException $exception) {
    @@ -57,6 +58,9 @@ public function loadClassMetadata(ClassMetadata $metadata): bool
    5758
    return false;
    5859
    }
    5960

    61+
    $loaded = false;
    62+
    $enabledForClass = $this->isAutoMappingEnabledForClass($metadata, $this->classValidatorRegexp);
    63+
    6064
    /* Available keys:
    6165
    - type
    6266
    - scale
    @@ -69,41 +73,49 @@ public function loadClassMetadata(ClassMetadata $metadata): bool
    6973

    7074
    // Type and nullable aren't handled here, use the PropertyInfo Loader instead.
    7175
    foreach ($doctrineMetadata->fieldMappings as $mapping) {
    76+
    $enabledForProperty = $enabledForClass;
    77+
    $lengthConstraint = null;
    78+
    foreach ($metadata->getPropertyMetadata($mapping['fieldName']) as $propertyMetadata) {
    79+
    foreach ($propertyMetadata->getConstraints() as $constraint) {
    80+
    // Enabling or disabling auto-mapping explicitly always takes precedence
    81+
    if ($constraint instanceof DisableAutoMapping) {
    82+
    continue 3;
    83+
    } elseif ($constraint instanceof EnableAutoMapping) {
    84+
    $enabledForProperty = true;
    85+
    } elseif ($constraint instanceof Length) {
    86+
    $lengthConstraint = $constraint;
    87+
    }
    88+
    }
    89+
    }
    90+
    91+
    if (!$enabledForProperty) {
    92+
    continue;
    93+
    }
    94+
    7295
    if (true === ($mapping['unique'] ?? false) && !isset($existingUniqueFields[$mapping['fieldName']])) {
    7396
    $metadata->addConstraint(new UniqueEntity(['fields' => $mapping['fieldName']]));
    97+
    $loaded = true;
    7498
    }
    7599

    76100
    if (null === ($mapping['length'] ?? null) || !\in_array($mapping['type'], ['string', 'text'], true)) {
    77101
    continue;
    78102
    }
    79103

    80-
    $constraint = $this->getLengthConstraint($metadata, $mapping['fieldName']);
    81-
    if (null === $constraint) {
    104+
    if (null === $lengthConstraint) {
    82105
    if (isset($mapping['originalClass']) && false === strpos($mapping['declaredField'], '.')) {
    83106
    $metadata->addPropertyConstraint($mapping['declaredField'], new Valid());
    107+
    $loaded = true;
    84108
    } elseif (property_exists($className, $mapping['fieldName'])) {
    85109
    $metadata->addPropertyConstraint($mapping['fieldName'], new Length(['max' => $mapping['length']]));
    110+
    $loaded = true;
    86111
    }
    87-
    } elseif (null === $constraint->max) {
    112+
    } elseif (null === $lengthConstraint->max) {
    88113
    // If a Length constraint exists and no max length has been explicitly defined, set it
    89-
    $constraint->max = $mapping['length'];
    90-
    }
    91-
    }
    92-
    93-
    return true;
    94-
    }
    95-
    96-
    private function getLengthConstraint(ClassMetadata $metadata, string $fieldName): ?Length
    97-
    {
    98-
    foreach ($metadata->getPropertyMetadata($fieldName) as $propertyMetadata) {
    99-
    foreach ($propertyMetadata->getConstraints() as $constraint) {
    100-
    if ($constraint instanceof Length) {
    101-
    return $constraint;
    102-
    }
    114+
    $lengthConstraint->max = $mapping['length'];
    103115
    }
    104116
    }
    105117

    106-
    return null;
    118+
    return $loaded;
    107119
    }
    108120

    109121
    private function getExistingUniqueFields(ClassMetadata $metadata): array

    src/Symfony/Component/Validator/CHANGELOG.md

    Lines changed: 1 addition & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -4,6 +4,7 @@ CHANGELOG
    44
    4.4.0
    55
    -----
    66

    7+
    * added `EnableAutoMapping` and `DisableAutoMapping` constraints to enable or disable auto mapping for class or a property
    78
    * using anything else than a `string` as the code of a `ConstraintViolation` is deprecated, a `string` type-hint will
    89
    be added to the constructor of the `ConstraintViolation` class and to the `ConstraintViolationBuilder::setCode()`
    910
    method in 5.0
    Lines changed: 45 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,45 @@
    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\Constraints;
    13+
    14+
    use Symfony\Component\Validator\Constraint;
    15+
    use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
    16+
    17+
    /**
    18+
    * Disables auto mapping.
    19+
    *
    20+
    * Using the annotations on a property has higher precedence than using it on a class,
    21+
    * which has higher precedence than any configuration that might be defined outside the class.
    22+
    *
    23+
    * @Annotation
    24+
    *
    25+
    * @author Kévin Dunglas <dunglas@gmail.com>
    26+
    */
    27+
    class DisableAutoMapping extends Constraint
    28+
    {
    29+
    public function __construct($options = null)
    30+
    {
    31+
    if (\is_array($options) && \array_key_exists('groups', $options)) {
    32+
    throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__));
    33+
    }
    34+
    35+
    parent::__construct($options);
    36+
    }
    37+
    38+
    /**
    39+
    * {@inheritdoc}
    40+
    */
    41+
    public function getTargets()
    42+
    {
    43+
    return [self::PROPERTY_CONSTRAINT, self::CLASS_CONSTRAINT];
    44+
    }
    45+
    }
    Lines changed: 45 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,45 @@
    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\Constraints;
    13+
    14+
    use Symfony\Component\Validator\Constraint;
    15+
    use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
    16+
    17+
    /**
    18+
    * Enables auto mapping.
    19+
    *
    20+
    * Using the annotations on a property has higher precedence than using it on a class,
    21+
    * which has higher precedence than any configuration that might be defined outside the class.
    22+
    *
    23+
    * @Annotation
    24+
    *
    25+
    * @author Kévin Dunglas <dunglas@gmail.com>
    26+
    */
    27+
    class EnableAutoMapping extends Constraint
    28+
    {
    29+
    public function __construct($options = null)
    30+
    {
    31+
    if (\is_array($options) && \array_key_exists('groups', $options)) {
    32+
    throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__));
    33+
    }
    34+
    35+
    parent::__construct($options);
    36+
    }
    37+
    38+
    /**
    39+
    * {@inheritdoc}
    40+
    */
    41+
    public function getTargets()
    42+
    {
    43+
    return [self::PROPERTY_CONSTRAINT, self::CLASS_CONSTRAINT];
    44+
    }
    45+
    }
    Lines changed: 41 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,41 @@
    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\Validator\Constraints\DisableAutoMapping;
    15+
    use Symfony\Component\Validator\Constraints\EnableAutoMapping;
    16+
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    17+
    18+
    /**
    19+
    * Utility methods to create auto mapping loaders.
    20+
    *
    21+
    * @author Kévin Dunglas <dunglas@gmail.com>
    22+
    */
    23+
    trait AutoMappingTrait
    24+
    {
    25+
    private function isAutoMappingEnabledForClass(ClassMetadata $metadata, string $classValidatorRegexp = null): bool
    26+
    {
    27+
    // Check if AutoMapping constraint is set first
    28+
    foreach ($metadata->getConstraints() as $constraint) {
    29+
    if ($constraint instanceof DisableAutoMapping) {
    30+
    return false;
    31+
    }
    32+
    33+
    if ($constraint instanceof EnableAutoMapping) {
    34+
    return true;
    35+
    }
    36+
    }
    37+
    38+
    // Fallback on the config
    39+
    return null === $classValidatorRegexp || preg_match($classValidatorRegexp, $metadata->getClassName());
    40+
    }
    41+
    }

    0 commit comments

    Comments
     (0)
    0