8000 [Validator] Add AutoMapping constraint to enable or disable auto-validation by dunglas · Pull Request #32107 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Validator] Add AutoMapping constraint to enable or disable auto-validation #32107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ class DoctrineLoaderEntity extends DoctrineLoaderParentEntity
/** @ORM\Column(type="simple_array", length=100) */
public $simpleArrayField = [];

/**
* @ORM\Column(length=10)
* @Assert\DisableAutoMapping
*/
public $noAutoMapping;

public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$allowEmptyString = property_exists(Assert\Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : [];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\Doctrine\Tests\Fixtures;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
* @ORM\Entity
* @Assert\DisableAutoMapping
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class DoctrineLoaderNoAutoMappingEntity
{
/**
* @ORM\Id
* @ORM\Column
*/
public $id;

/**
* @ORM\Column(length=20, unique=true)
*/
public $maxLength;

/**
* @Assert\EnableAutoMapping
* @ORM\Column(length=20)
*/
public $autoMappingExplicitlyEnabled;
}
42 changes: 39 additions & 3 deletions src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEmbed;
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderNestedEmbed;
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderNoAutoMappingEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderParentEntity;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Bridge\Doctrine\Validator\DoctrineLoader;
use Symfony\Component\Validator\Constraints\DisableAutoMapping;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Mapping\CascadingStrategy;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Loader\AutoMappingTrait;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
use Symfony\Component\Validator\Tests\Fixtures\Entity;
use Symfony\Component\Validator\Validation;
Expand All @@ -33,12 +36,15 @@
*/
class DoctrineLoaderTest extends TestCase
{
public function testLoadClassMetadata()
protected function setUp(): void
{
if (!method_exists(ValidatorBuilder::class, 'addLoader')) {
$this->markTestSkipped('Auto-mapping requires symfony/validation 4.2+');
if (!trait_exists(AutoMappingTrait::class)) {
$this->markTestSkipped('Auto-mapping requires symfony/validation 4.4+');
}
}

public function testLoadClassMetadata()
{
$validator = Validation::createValidatorBuilder()
->addMethodMapping('loadValidatorMetadata')
->enableAnnotationMapping()
Expand Down Expand Up @@ -134,6 +140,12 @@ public function testLoadClassMetadata()
$this->assertCount(1, $textFieldConstraints);
$this->assertInstanceOf(Length::class, $textFieldConstraints[0]);
$this->assertSame(1000, $textFieldConstraints[0]->max);

$noAutoMappingMetadata = $classMetadata->getPropertyMetadata('noAutoMapping');
$this->assertCount(1, $noAutoMappingMetadata);
$noAutoMappingConstraints = $noAutoMappingMetadata[0]->getConstraints();
$this->assertCount(1, $noAutoMappingConstraints);
$this->assertInstanceOf(DisableAutoMapping::class, $noAutoMappingConstraints[0]);
}

public function testFieldMappingsConfiguration()
Expand Down Expand Up @@ -180,4 +192,28 @@ public function regexpProvider()
[false, '{^'.preg_quote(Entity::class).'$}'],
];
}

public function testClassNoAutoMapping()
{
if (!method_exists(ValidatorBuilder::class, 'addLoader')) {
$this->markTestSkipped('Auto-mapping requires symfony/validation 4.2+');
}

$validator = Validation::createValidatorBuilder()
->enableAnnotationMapping()
->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager()))
->getValidator();

$classMetadata = $validator->getMetadataFor(new DoctrineLoaderNoAutoMappingEntity());

$classConstraints = $classMetadata->getConstraints();
$this->assertCount(1, $classConstraints);
$this->assertInstanceOf(DisableAutoMapping::class, $classConstraints[0]);

$maxLengthMetadata = $classMetadata->getPropertyMetadata('maxLength');
$this->assertEmpty($maxLengthMetadata);

$autoMappingExplicitlyEnabledMetadata = $classMetadata->getPropertyMetadata('autoMappingExplicitlyEnabled');
$this->assertCount(2, $autoMappingExplicitlyEnabledMetadata[0]->constraints);
}
}
56 changes: 34 additions & 22 deletions src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Mapping\MappingException as OrmMappingException;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints\DisableAutoMapping;
use Symfony\Component\Validator\Constraints\EnableAutoMapping;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Loader\AutoMappingTrait;
use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;

/**
Expand All @@ -28,6 +31,8 @@
*/
final class DoctrineLoader implements LoaderInterface
{
use AutoMappingTrait;

private $entityManager;
private $classValidatorRegexp;

Expand All @@ -43,10 +48,6 @@ public function __construct(EntityManagerInterface $entityManager, string $class
public function loadClassMetadata(ClassMetadata $metadata): bool
{
$className = $metadata->getClassName();
if (null !== $this->classValidatorRegexp && !preg_match($this->classValidatorRegexp, $className)) {
return false;
}

try {
$doctrineMetadata = $this->entityManager->getClassMetadata($className);
} catch (MappingException | OrmMappingException $exception) {
Expand All @@ -57,6 +58,9 @@ public function loadClassMetadata(ClassMetadata $metadata): bool
return false;
}

$loaded = false;
$enabledForClass = $this->isAutoMappingEnabledForClass($metadata, $this->classValidatorRegexp);

/* Available keys:
- type
- scale
Expand All @@ -69,41 +73,49 @@ public function loadClassMetadata(ClassMetadata $metadata): bool

// Type and nullable aren't handled here, use the PropertyInfo Loader instead.
foreach ($doctrineMetadata->fieldMappings as $mapping) {
$enabledForProperty = $enabledForClass;
$lengthConstraint = null;
foreach ($metadata->getPropertyMetadata($mapping['fieldName']) as $propertyMetadata) {
foreach ($propertyMetadata->getConstraints() as $constraint) {
// Enabling or disabling auto-mapping explicitly always takes precedence
if ($constraint instanceof DisableAutoMapping) {
continue 3;
} elseif ($constraint instanceof EnableAutoMapping) {
$enabledForProperty = true;
} elseif ($constraint instanceof Length) {
$lengthConstraint = $constraint;
}
}
}

if (!$enabledForProperty) {
continue;
}

if (true === ($mapping['unique'] ?? false) && !isset($existingUniqueFields[$mapping['fieldName']])) {
$metadata->addConstraint(new UniqueEntity(['fields' => $mapping['fieldName']]));
$loaded = true;
}

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

$constraint = $this->getLengthConstraint($metadata, $mapping['fieldName']);
if (null === $constraint) {
if (null === $lengthConstraint) {
if (isset($mapping['originalClass']) && false === strpos($mapping['declaredField'], '.')) {
$metadata->addPropertyConstraint($mapping['declaredField'], new Valid());
$loaded = true;
} elseif (property_exists($className, $mapping['fieldName'])) {
$metadata->addPropertyConstraint($mapping['fieldName'], new Length(['max' => $mapping['length']]));
$loaded = true;
}
} elseif (null === $constraint->max) {
} elseif (null === $lengthConstraint->max) {
// If a Length constraint exists and no max length has been explicitly defined, set it
$constraint->max = $mapping['length'];
}
}

return true;
}

private function getLengthConstraint(ClassMetadata $metadata, string $fieldName): ?Length
{
foreach ($metadata->getPropertyMetadata($fieldName) as $propertyMetadata) {
foreach ($propertyMetadata->getConstraints() as $constraint) {
if ($constraint instanceof Length) {
return $constraint;
}
$lengthConstraint->max = $mapping['length'];
}
}

return null;
return $loaded;
}

private function getExistingUniqueFields(ClassMetadata $metadata): array
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Validator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
4.4.0
-----

* added `EnableAutoMapping` and `DisableAutoMapping` constraints to enable or disable auto mapping for class or a property
* using anything else than a `string` as the code of a `ConstraintViolation` is deprecated, a `string` type-hint will
be added to the constructor of the `ConstraintViolation` class and to the `ConstraintViolationBuilder::setCode()`
method in 5.0
Expand Down
45 changes: 45 additions & 0 deletions src/Symfony/Component/Validator/Constraints/DisableAutoMapping.php
10000
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;

/**
* Disables auto mapping.
*
* Using the annotations on a property has higher precedence than using it on a class,
* which has higher precedence than any configuration that might be defined outside the class.
*
* @Annotation
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class DisableAutoMapping extends Constraint
{
public function __construct($options = null)
{
if (\is_array($options) && \array_key_exists('groups', $options)) {
throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__));
}

parent::__construct($options);
}

/**
* {@inheritdoc}
*/
public function getTargets()
{
return [self::PROPERTY_CONSTRAINT, self::CLASS_CONSTRAINT];
}
}
45 changes: 45 additions & 0 deletions src/Symfony/Component/Validator/Constraints/EnableAutoMapping.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;

/**
* Enables auto mapping.
*
* Using the annotations on a property has higher precedence than using it on a class,
* which has higher precedence than any configuration that might be defined outside the class.
*
* @Annotation
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class EnableAutoMapping extends Constraint
{
public function __construct($options = null)
{
if (\is_array($options) && \array_key_exists('groups', $options)) {
throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__));
}

parent::__construct($options);
}

/**
* {@inheritdoc}
*/
public function getTargets()
{
return [self::PROPERTY_CONSTRAINT, self::CLASS_CONSTRAINT];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Mapping\Loader;

use Symfony\Component\Validator\Constraints\DisableAutoMapping;
use Symfony\Component\Validator\Constraints\EnableAutoMapping;
use Symfony\Component\Validator\Mapping\ClassMetadata;

/**
* Utility methods to create auto mapping loaders.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
trait AutoMappingTrait
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we mark the trait as internal?

Copy link
Member Author
@dunglas dunglas Oct 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, it helps to create custom loaders, and to prevent inconsistencies such as the one fixed by #33828

{
private function isAutoMappingEnabledForClass(ClassMetadata $metadata, string $classValidatorRegexp = null): bool
{
// Check if AutoMapping constraint is set first
foreach ($metadata->getConstraints() as $constraint) {
if ($constraint instanceof DisableAutoMapping) {
return false;
}

if ($constraint instanceof EnableAutoMapping) {
return true;
}
}

// Fallback on the config
return null === $classValidatorRegexp || preg_match($classValidatorRegexp, $metadata->getClassName());
}
}
Loading
0