diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md index d3de7b168c..168b78a47f 100644 --- a/UPGRADE-4.4.md +++ b/UPGRADE-4.4.md @@ -87,3 +87,7 @@ Validator * Deprecated passing an `ExpressionLanguage` instance as the second argument of `ExpressionValidator::__construct()`. Pass it as the first argument instead. + * The `Length` constraint expects the `allowEmptyString` option to be defined + when the `min` option is used. + Set it to `true` to keep the current behavior and `false` to reject empty strings. + In 5.0, it'll become optional and will default to `false`. diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/BaseUser.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/BaseUser.php index abf8819a4c..50b5845581 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/BaseUser.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/BaseUser.php @@ -2,6 +2,9 @@ namespace Symfony\Bridge\Doctrine\Tests\Fixtures; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Mapping\ClassMetadata; + /** * Class BaseUser. */ @@ -46,4 +49,15 @@ class BaseUser { return $this->username; } + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $allowEmptyString = property_exists(Assert\Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : []; + + $metadata->addPropertyConstraint('username', new Assert\Length([ + 'min' => 2, + 'max' => 120, + 'groups' => ['Registration'], + ] + $allowEmptyString)); + } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php index dc06d37fa3..9a2111f2b9 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php @@ -14,6 +14,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Fixtures; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Mapping\ClassMetadata; /** * @ORM\Entity @@ -36,13 +37,11 @@ class DoctrineLoaderEntity extends DoctrineLoaderParentEntity /** * @ORM\Column(length=20) - * @Assert\Length(min=5) */ public $mergedMaxLength; /** * @ORM\Column(length=20) - * @Assert\Length(min=1, max=10) */ public $alreadyMappedMaxLength; @@ -69,4 +68,12 @@ class DoctrineLoaderEntity extends DoctrineLoaderParentEntity /** @ORM\Column(type="simple_array", length=100) */ public $simpleArrayField = []; + + public static function loadValidatorMetadata(ClassMetadata $metadata): void + { + $allowEmptyString = property_exists(Assert\Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : []; + + $metadata->addPropertyConstraint('mergedMaxLength', new Assert\Length(['min' => 5] + $allowEmptyString)); + $metadata->addPropertyConstraint('alreadyMappedMaxLength', new Assert\Length(['min' => 1, 'max' => 10] + $allowEmptyString)); + } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Resources/validator/BaseUser.xml b/src/Symfony/Bridge/Doctrine/Tests/Resources/validator/BaseUser.xml index bf64b92ca4..ddb8a13bc1 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Resources/validator/BaseUser.xml +++ b/src/Symfony/Bridge/Doctrine/Tests/Resources/validator/BaseUser.xml @@ -9,11 +9,6 @@ - - - - - diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php index 2dcab2533d..45cae2da41 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php @@ -40,6 +40,7 @@ class DoctrineLoaderTest extends TestCase } $validator = Validation::createValidatorBuilder() + ->addMethodMapping('loadValidatorMetadata') ->enableAnnotationMapping() ->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager(), '{^Symfony\\\\Bridge\\\\Doctrine\\\\Tests\\\\Fixtures\\\\DoctrineLoader}')) ->getValidator() @@ -142,6 +143,7 @@ class DoctrineLoaderTest extends TestCase } $validator = Validation::createValidatorBuilder() + ->addMethodMapping('loadValidatorMetadata') ->enableAnnotationMapping() ->addXmlMappings([__DIR__.'/../Resources/validator/BaseUser.xml']) ->addLoader( diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php index 57f92b6574..a920e3be5b 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php @@ -57,13 +57,15 @@ class FormTypeValidatorExtensionTest extends BaseValidatorExtensionTest public function testGroupSequenceWithConstraintsOption() { + $allowEmptyString = property_exists(Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : []; + $form = Forms::createFormFactoryBuilder() ->addExtension(new ValidatorExtension(Validation::createValidator())) ->getFormFactory() ->create(FormTypeTest::TESTED_TYPE, null, (['validation_groups' => new GroupSequence(['First', 'Second'])])) ->add('field', TextTypeTest::TESTED_TYPE, [ 'constraints' => [ - new Length(['min' => 10, 'groups' => ['First']]), + new Length(['min' => 10, 'groups' => ['First']] + $allowEmptyString), new Email(['groups' => ['Second']]), ], ]) diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorTypeGuesserTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorTypeGuesserTest.php index 878bbfad21..fd11342bea 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorTypeGuesserTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorTypeGuesserTest.php @@ -61,11 +61,13 @@ class ValidatorTypeGuesserTest extends TestCase public function guessRequiredProvider() { + $allowEmptyString = property_exists(Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : []; + return [ [new NotNull(), new ValueGuess(true, Guess::HIGH_CONFIDENCE)], [new NotBlank(), new ValueGuess(true, Guess::HIGH_CONFIDENCE)], [new IsTrue(), new ValueGuess(true, Guess::HIGH_CONFIDENCE)], - [new Length(10), new ValueGuess(false, Guess::LOW_CONFIDENCE)], + [new Length(['min' => 10, 'max' => 10] + $allowEmptyString), new ValueGuess(false, Guess::LOW_CONFIDENCE)], [new Range(['min' => 1, 'max' => 20]), new ValueGuess(false, Guess::LOW_CONFIDENCE)], ]; } @@ -101,7 +103,9 @@ class ValidatorTypeGuesserTest extends TestCase public function testGuessMaxLengthForConstraintWithMinValue() { - $constraint = new Length(['min' => '2']); + $allowEmptyString = property_exists(Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : []; + + $constraint = new Length(['min' => '2'] + $allowEmptyString); $result = $this->guesser->guessMaxLengthForConstraint($constraint); $this->assertNull($result); diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 8a85ee35ef..a86679dd1c 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * added the `compared_value_path` parameter in violations when using any comparison constraint with the `propertyPath` option. * added support for checking an array of types in `TypeValidator` + * added a new `allowEmptyString` option to the `Length` constraint to allow rejecting empty strings when `min` is set, by setting it to `false`. 4.3.0 ----- diff --git a/src/Symfony/Component/Validator/Constraints/Length.php b/src/Symfony/Component/Validator/Constraints/Length.php index 0edd0e97e0..d9b0d1f1c5 100644 --- a/src/Symfony/Component/Validator/Constraints/Length.php +++ b/src/Symfony/Component/Validator/Constraints/Length.php @@ -41,6 +41,7 @@ class Length extends Constraint public $min; public $charset = 'UTF-8'; public $normalizer; + public $allowEmptyString; public function __construct($options = null) { @@ -56,6 +57,13 @@ class Length extends Constraint parent::__construct($options); + if (null === $this->allowEmptyString) { + $this->allowEmptyString = true; + if (null !== $this->min) { + @trigger_error(sprintf('Using the "%s" constraint with the "min" option without setting the "allowEmptyString" one is deprecated and defaults to true. In 5.0, it will become optional and default to false.', self::class), E_USER_DEPRECATED); + } + } + if (null === $this->min && null === $this->max) { throw new MissingOptionsException(sprintf('Either option "min" or "max" must be given for constraint %s', __CLASS__), ['min', 'max']); } diff --git a/src/Symfony/Component/Validator/Constraints/LengthValidator.php b/src/Symfony/Component/Validator/Constraints/LengthValidator.php index f3cf245cf4..b1b5d7c770 100644 --- a/src/Symfony/Component/Validator/Constraints/LengthValidator.php +++ b/src/Symfony/Component/Validator/Constraints/LengthValidator.php @@ -30,7 +30,7 @@ class LengthValidator extends ConstraintValidator throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\Length'); } - if (null === $value || '' === $value) { + if (null === $value || ('' === $value && $constraint->allowEmptyString)) { return; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LengthTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LengthTest.php index 6a20ff541f..b7aa2339aa 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LengthTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LengthTest.php @@ -21,7 +21,7 @@ class LengthTest extends TestCase { public function testNormalizerCanBeSet() { - $length = new Length(['min' => 0, 'max' => 10, 'normalizer' => 'trim']); + $length = new Length(['min' => 0, 'max' => 10, 'normalizer' => 'trim', 'allowEmptyString' => false]); $this->assertEquals('trim', $length->normalizer); } @@ -32,7 +32,7 @@ class LengthTest extends TestCase */ public function testInvalidNormalizerThrowsException() { - new Length(['min' => 0, 'max' => 10, 'normalizer' => 'Unknown Callable']); + new Length(['min' => 0, 'max' => 10, 'normalizer' => 'Unknown Callable', 'allowEmptyString' => false]); } /** @@ -41,6 +41,6 @@ class LengthTest extends TestCase */ public function testInvalidNormalizerObjectThrowsException() { - new Length(['min' => 0, 'max' => 10, 'normalizer' => new \stdClass()]); + new Length(['min' => 0, 'max' => 10, 'normalizer' => new \stdClass(), 'allowEmptyString' => false]); } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LengthValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LengthValidatorTest.php index 96c388ae5b..6e94a0233e 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LengthValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LengthValidatorTest.php @@ -22,26 +22,47 @@ class LengthValidatorTest extends ConstraintValidatorTestCase return new LengthValidator(); } - public function testNullIsValid() + public function testLegacyNullIsValid() { - $this->validator->validate(null, new Length(6)); + $this->validator->validate(null, new Length(['value' => 6, 'allowEmptyString' => false])); $this->assertNoViolation(); } - public function testEmptyStringIsValid() + /** + * @group legacy + * @expectedDeprecation Using the "Symfony\Component\Validator\Constraints\Length" constraint with the "min" option without setting the "allowEmptyString" one is deprecated and defaults to true. In 5.0, it will become optional and default to false. + */ + public function testLegacyEmptyStringIsValid() { $this->validator->validate('', new Length(6)); $this->assertNoViolation(); } + public function testEmptyStringIsInvalid() + { + $this->validator->validate('', new Length([ + 'value' => $limit = 6, + 'allowEmptyString' => false, + 'exactMessage' => 'myMessage', + ])); + + $this->buildViolation('myMessage') + ->setParameter('{{ value }}', '""') + ->setParameter('{{ limit }}', $limit) + ->setInvalidValue('') + ->setPlural($limit) + ->setCode(Length::TOO_SHORT_ERROR) + ->assertRaised(); + } + /** * @expectedException \Symfony\Component\Validator\Exception\UnexpectedValueException */ public function testExpectsStringCompatibleType() { - $this->validator->validate(new \stdClass(), new Length(5)); + $this->validator->validate(new \stdClass(), new Length(['value' => 5, 'allowEmptyString' => false])); } public function getThreeOrLessCharacters() @@ -109,7 +130,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase */ public function testValidValuesMin($value) { - $constraint = new Length(['min' => 5]); + $constraint = new Length(['min' => 5, 'allowEmptyString' => false]); $this->validator->validate($value, $constraint); $this->assertNoViolation(); @@ -131,7 +152,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase */ public function testValidValuesExact($value) { - $constraint = new Length(4); + $constraint = new Length(['value' => 4, 'allowEmptyString' => false]); $this->validator->validate($value, $constraint); $this->assertNoViolation(); @@ -142,7 +163,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase */ public function testValidNormalizedValues($value) { - $constraint = new Length(['min' => 3, 'max' => 3, 'normalizer' => 'trim']); + $constraint = new Length(['min' => 3, 'max' => 3, 'normalizer' => 'trim', 'allowEmptyString' => false]); $this->validator->validate($value, $constraint); $this->assertNoViolation(); @@ -156,6 +177,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase $constraint = new Length([ 'min' => 4, 'minMessage' => 'myMessage', + 'allowEmptyString' => false, ]); $this->validator->validate($value, $constraint); @@ -199,6 +221,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase 'min' => 4, 'max' => 4, 'exactMessage' => 'myMessage', + 'allowEmptyString' => false, ]); $this->validator->validate($value, $constraint); @@ -221,6 +244,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase 'min' => 4, 'max' => 4, 'exactMessage' => 'myMessage', + 'allowEmptyString' => false, ]); $this->validator->validate($value, $constraint); @@ -244,6 +268,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase 'max' => 1, 'charset' => $charset, 'charsetMessage' => 'myMessage', + 'allowEmptyString' => false, ]); $this->validator->validate($value, $constraint); @@ -262,7 +287,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase public function testConstraintDefaultOption() { - $constraint = new Length(5); + $constraint = new Length(['value' => 5, 'allowEmptyString' => false]); $this->assertEquals(5, $constraint->min); $this->assertEquals(5, $constraint->max); @@ -270,7 +295,7 @@ class LengthValidatorTest extends ConstraintValidatorTestCase public function testConstraintAnnotationDefaultOption() { - $constraint = new Length(['value' => 5, 'exactMessage' => 'message']); + $constraint = new Length(['value' => 5, 'exactMessage' => 'message', 'allowEmptyString' => false]); $this->assertEquals(5, $constraint->min); $this->assertEquals(5, $constraint->max); diff --git a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php index 8109b6b9bf..c0c7c3e96d 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php @@ -103,7 +103,7 @@ class RecursiveValidatorTest extends AbstractTest public function testCollectionConstraintValidateAllGroupsForNestedConstraints() { $this->metadata->addPropertyConstraint('data', new Collection(['fields' => [ - 'one' => [new NotBlank(['groups' => 'one']), new Length(['min' => 2, 'groups' => 'two'])], + 'one' => [new NotBlank(['groups' => 'one']), new Length(['min' => 2, 'groups' => 'two', 'allowEmptyString' => false])], 'two' => [new NotBlank(['groups' => 'two'])], ]])); @@ -121,7 +121,7 @@ class RecursiveValidatorTest extends AbstractTest { $this->metadata->addPropertyConstraint('data', new All(['constraints' => [ new NotBlank(['groups' => 'one']), - new Length(['min' => 2, 'groups' => 'two']), + new Length(['min' => 2, 'groups' => 'two', 'allowEmptyString' => false]), ]])); $entity = new Entity(); @@ -129,8 +129,9 @@ class RecursiveValidatorTest extends AbstractTest $violations = $this->validator->validate($entity, null, ['one', 'two']); - $this->assertCount(2, $violations); + $this->assertCount(3, $violations); $this->assertInstanceOf(NotBlank::class, $violations->get(0)->getConstraint()); $this->assertInstanceOf(Length::class, $violations->get(1)->getConstraint()); + $this->assertInstanceOf(Length::class, $violations->get(2)->getConstraint()); } }