diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index d3bebec61e0c..75bb15a9ec01 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * added option `alpha3` to `Country` constraint * allow to define a reusable set of constraints by extending the `Compound` constraint * added `Sequentially` constraint, to sequentially validate a set of constraints (any violation raised will prevent further validation of the nested constraints) + * added support to validate `\DateInterval` instances with relative string date formats in the `Range` and all comparisons constraints 5.0.0 ----- diff --git a/src/Symfony/Component/Validator/ConstraintValidator.php b/src/Symfony/Component/Validator/ConstraintValidator.php index 4d66776839be..ae09e4daf76d 100644 --- a/src/Symfony/Component/Validator/ConstraintValidator.php +++ b/src/Symfony/Component/Validator/ConstraintValidator.php @@ -31,6 +31,12 @@ abstract class ConstraintValidator implements ConstraintValidatorInterface */ const OBJECT_TO_STRING = 2; + /** + * Whether to format {@link \DateInterval} objects as human readable strings + * eg: 6 hours, 1 minute and 2 seconds. + */ + const PRETTY_DATE_INTERVAL = 4; + /** * @var ExecutionContextInterface */ @@ -98,6 +104,37 @@ protected function formatValue($value, int $format = 0) return $value->format('Y-m-d H:i:s'); } + if (($format & self::PRETTY_DATE_INTERVAL) && $value instanceof \DateInterval) { + $formattedValueParts = []; + foreach ([ + 'y' => 'year', + 'm' => 'month', + 'd' => 'day', + 'h' => 'hour', + 'i' => 'minute', + 's' => 'second', + 'f' => 'microsecond', + ] as $p => $label) { + if (!$formattedValue = $value->format('%'.$p)) { + continue; + } + + if ($formattedValue > 1) { + $label .= 's'; + } + + $formattedValueParts[] = $formattedValue.' '.$label; + } + + if (!$formattedValueParts) { + return '0'; + } + + $lastFormattedValuePart = array_pop($formattedValueParts); + + return $value->format('%r').(!$formattedValueParts ? $lastFormattedValuePart : implode(', ', $formattedValueParts).' and '.$lastFormattedValuePart); + } + if (\is_object($value)) { if (($format & self::OBJECT_TO_STRING) && method_exists($value, '__toString')) { return $value->__toString(); diff --git a/src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php b/src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php index a3897fcf86f1..cb6fbd08679b 100644 --- a/src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php +++ b/src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php @@ -18,6 +18,7 @@ use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Util\DateIntervalComparisonHelper; /** * Provides a base class for the validation of property comparisons. @@ -61,11 +62,14 @@ public function validate($value, Constraint $constraint) $comparedValue = $constraint->value; } - // Convert strings to DateTimes if comparing another DateTime - // This allows to compare with any date/time value supported by - // the DateTime constructor: - // https://php.net/datetime.formats + $isDateIntervalComparison = false; + if (\is_string($comparedValue) && $value instanceof \DateTimeInterface) { + // Convert strings to DateTimes if comparing another DateTime + // This allows to compare with any date/time value supported by + // the DateTime constructor: + // https://php.net/datetime.formats + // If $value is immutable, convert the compared value to a DateTimeImmutable too, otherwise use DateTime $dateTimeClass = $value instanceof \DateTimeImmutable ? \DateTimeImmutable::class : \DateTime::class; @@ -74,13 +78,22 @@ public function validate($value, Constraint $constraint) } catch (\Exception $e) { throw new ConstraintDefinitionException(sprintf('The compared value "%s" could not be converted to a "%s" instance in the "%s" constraint.', $comparedValue, $dateTimeClass, \get_class($constraint))); } + } elseif ($isDateIntervalComparison = DateIntervalComparisonHelper::supports($value, $comparedValue)) { + $originalValue = $value; + $value = DateIntervalComparisonHelper::convertValue($dateIntervalReference = new \DateTimeImmutable(), $value); + + try { + $comparedValue = DateIntervalComparisonHelper::convertComparedValue($dateIntervalReference, $comparedValue); + } catch (\InvalidArgumentException $e) { + throw new ConstraintDefinitionException(sprintf('The compared value "%s" could not be converted to a "DateTimeImmutable" instance in the "%s" constraint.', $comparedValue, \get_class($constraint))); + } } if (!$this->compareValues($value, $comparedValue)) { $violationBuilder = $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $this->formatValue($value, self::OBJECT_TO_STRING | self::PRETTY_DATE)) - ->setParameter('{{ compared_value }}', $this->formatValue($comparedValue, self::OBJECT_TO_STRING | self::PRETTY_DATE)) - ->setParameter('{{ compared_value_type }}', $this->formatTypeOf($comparedValue)) + ->setParameter('{{ value }}', $this->formatValue(!$isDateIntervalComparison ? $value : $originalValue, self::OBJECT_TO_STRING | self::PRETTY_DATE | self::PRETTY_DATE_INTERVAL)) + ->setParameter('{{ compared_value }}', $this->formatValue($messageComparedValue = (!$isDateIntervalComparison ? $comparedValue : $dateIntervalReference->diff($comparedValue)), self::OBJECT_TO_STRING | self::PRETTY_DATE | self::PRETTY_DATE_INTERVAL)) + ->setParameter('{{ compared_value_type }}', $this->formatTypeOf($messageComparedValue)) ->setCode($this->getErrorCode()); if (null !== $path) { diff --git a/src/Symfony/Component/Validator/Constraints/RangeValidator.php b/src/Symfony/Component/Validator/Constraints/RangeValidator.php index e976ff30a76a..6334eca19864 100644 --- a/src/Symfony/Component/Validator/Constraints/RangeValidator.php +++ b/src/Symfony/Component/Validator/Constraints/RangeValidator.php @@ -18,6 +18,7 @@ use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Util\DateIntervalComparisonHelper; /** * @author Bernhard Schussek @@ -44,7 +45,7 @@ public function validate($value, Constraint $constraint) return; } - if (!is_numeric($value) && !$value instanceof \DateTimeInterface) { + if (!is_numeric($value) && !$value instanceof \DateTimeInterface && !$value instanceof \DateInterval) { $this->context->buildViolation($constraint->invalidMessage) ->setParameter('{{ value }}', $this->formatValue($value, self::PRETTY_DATE)) ->setCode(Range::INVALID_CHARACTERS_ERROR) @@ -56,11 +57,15 @@ public function validate($value, Constraint $constraint) $min = $this->getLimit($constraint->minPropertyPath, $constraint->min, $constraint); $max = $this->getLimit($constraint->maxPropertyPath, $constraint->max, $constraint); - // Convert strings to DateTimes if comparing another DateTime - // This allows to compare with any date/time value supported by - // the DateTime constructor: - // https://php.net/datetime.formats + $minIsDateIntervalComparison = false; + $maxIsDateIntervalComparison = false; + if ($value instanceof \DateTimeInterface) { + // Convert strings to DateTimes if comparing another DateTime + // This allows to compare with any date/time value supported by + // the DateTime constructor: + // https://php.net/datetime.formats + $dateTimeClass = null; if (\is_string($min)) { @@ -82,6 +87,27 @@ public function validate($value, Constraint $constraint) throw new ConstraintDefinitionException(sprintf('The max value "%s" could not be converted to a "%s" instance in the "%s" constraint.', $max, $dateTimeClass, \get_class($constraint))); } } + } elseif (($minIsDateIntervalComparison = DateIntervalComparisonHelper::supports($value, $min)) || ($maxIsDateIntervalComparison = DateIntervalComparisonHelper::supports($value, $max))) { + $originalValue = $value; + $value = DateIntervalComparisonHelper::convertValue($dateIntervalReference = new \DateTimeImmutable(), $value); + + if ($minIsDateIntervalComparison) { + try { + $min = DateIntervalComparisonHelper::convertComparedValue($dateIntervalReference, $min); + } catch (\InvalidArgumentException $e) { + throw new ConstraintDefinitionException(sprintf('The max value "%s" could not be converted to a "DateTimeImmutable" instance in the "%s" constraint.', $max, \get_class($constraint))); + } + + $maxIsDateIntervalComparison = DateIntervalComparisonHelper::supports($originalValue, $max); + } + + if ($maxIsDateIntervalComparison) { + try { + $max = DateIntervalComparisonHelper::convertComparedValue($dateIntervalReference, $max); + } catch (\InvalidArgumentException $e) { + throw new ConstraintDefinitionException(sprintf('The min value "%s" could not be converted to a "DateTimeImmutable" instance in the "%s" constraint.', $min, \get_class($constraint))); + } + } } $hasLowerLimit = null !== $min; @@ -89,9 +115,9 @@ public function validate($value, Constraint $constraint) if ($hasLowerLimit && $hasUpperLimit && ($value < $min || $value > $max)) { $violationBuilder = $this->context->buildViolation($constraint->notInRangeMessage) - ->setParameter('{{ value }}', $this->formatValue($value, self::PRETTY_DATE)) - ->setParameter('{{ min }}', $this->formatValue($min, self::PRETTY_DATE)) - ->setParameter('{{ max }}', $this->formatValue($max, self::PRETTY_DATE)) + ->setParameter('{{ value }}', $this->formatValue(!$minIsDateIntervalComparison && !$maxIsDateIntervalComparison ? $value : $originalValue, self::PRETTY_DATE | self::PRETTY_DATE_INTERVAL)) + ->setParameter('{{ min }}', $this->formatValue(!$minIsDateIntervalComparison ? $min : $dateIntervalReference->diff($min), self::PRETTY_DATE | self::PRETTY_DATE_INTERVAL)) + ->setParameter('{{ max }}', $this->formatValue(!$maxIsDateIntervalComparison ? $max : $dateIntervalReference->diff($max), self::PRETTY_DATE | self::PRETTY_DATE_INTERVAL)) ->setCode(Range::NOT_IN_RANGE_ERROR); if (null !== $constraint->maxPropertyPath) { @@ -109,8 +135,8 @@ public function validate($value, Constraint $constraint) if ($hasUpperLimit && $value > $max) { $violationBuilder = $this->context->buildViolation($constraint->maxMessage) - ->setParameter('{{ value }}', $this->formatValue($value, self::PRETTY_DATE)) - ->setParameter('{{ limit }}', $this->formatValue($max, self::PRETTY_DATE)) + ->setParameter('{{ value }}', $this->formatValue(!$minIsDateIntervalComparison && !$maxIsDateIntervalComparison ? $value : $originalValue, self::PRETTY_DATE | self::PRETTY_DATE_INTERVAL)) + ->setParameter('{{ limit }}', $this->formatValue(!$maxIsDateIntervalComparison ? $max : $dateIntervalReference->diff($max), self::PRETTY_DATE | self::PRETTY_DATE_INTERVAL)) ->setCode(Range::TOO_HIGH_ERROR); if (null !== $constraint->maxPropertyPath) { @@ -128,8 +154,8 @@ public function validate($value, Constraint $constraint) if ($hasLowerLimit && $value < $min) { $violationBuilder = $this->context->buildViolation($constraint->minMessage) - ->setParameter('{{ value }}', $this->formatValue($value, self::PRETTY_DATE)) - ->setParameter('{{ limit }}', $this->formatValue($min, self::PRETTY_DATE)) + ->setParameter('{{ value }}', $this->formatValue(!$minIsDateIntervalComparison && !$maxIsDateIntervalComparison ? $value : $originalValue, self::PRETTY_DATE | self::PRETTY_DATE_INTERVAL)) + ->setParameter('{{ limit }}', $this->formatValue(!$minIsDateIntervalComparison ? $min : $dateIntervalReference->diff($min), self::PRETTY_DATE | self::PRETTY_DATE_INTERVAL)) ->setCode(Range::TOO_LOW_ERROR); if (null !== $constraint->maxPropertyPath) { diff --git a/src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php b/src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php index 6ca3eab41fd6..0f21e552cf26 100644 --- a/src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php @@ -30,6 +30,9 @@ public function formatValueProvider() $defaultTimezone = date_default_timezone_get(); date_default_timezone_set('Europe/Moscow'); // GMT+3 + $negativeDateInterval = new \DateInterval('PT30S'); + $negativeDateInterval->invert = 1; + $data = [ ['true', true], ['false', false], @@ -44,6 +47,13 @@ public function formatValueProvider() [class_exists(\IntlDateFormatter::class) ? 'Feb 2, 1971, 8:00 AM' : '1971-02-02 08:00:00', $dateTime, ConstraintValidator::PRETTY_DATE], [class_exists(\IntlDateFormatter::class) ? 'Jan 1, 1970, 6:00 AM' : '1970-01-01 06:00:00', new \DateTimeImmutable('1970-01-01T06:00:00Z'), ConstraintValidator::PRETTY_DATE], [class_exists(\IntlDateFormatter::class) ? 'Jan 1, 1970, 3:00 PM' : '1970-01-01 15:00:00', (new \DateTimeImmutable('1970-01-01T23:00:00'))->setTimezone(new \DateTimeZone('America/New_York')), ConstraintValidator::PRETTY_DATE], + ['object', new \DateInterval('PT30S')], + ['1 year, 1 month, 1 day, 1 hour, 1 minute and 1 second', new \DateInterval('P1Y1M1DT1H1M1S'), ConstraintValidator::PRETTY_DATE_INTERVAL], + ['3 months and 4 seconds', new \DateInterval('P3MT4S'), ConstraintValidator::PRETTY_DATE_INTERVAL], + ['0', new \DateInterval('PT0S'), ConstraintValidator::PRETTY_DATE_INTERVAL], + ['0', ($dateTime = new \DateTimeImmutable())->diff($dateTime), ConstraintValidator::PRETTY_DATE_INTERVAL], + ['7 days', new \DateInterval('P1W'), ConstraintValidator::PRETTY_DATE_INTERVAL], + ['-30 seconds', $negativeDateInterval, ConstraintValidator::PRETTY_DATE_INTERVAL], ]; date_default_timezone_set($defaultTimezone); diff --git a/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php index 70832fc04b13..31e642b3115a 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php @@ -40,6 +40,9 @@ protected function getErrorCode(): ?string */ public function provideValidComparisons(): array { + $negativeDateInterval = new \DateInterval('PT1H'); + $negativeDateInterval->invert = 1; + return [ [3, 3], [3, '3'], @@ -49,6 +52,9 @@ public function provideValidComparisons(): array [new \DateTime('2000-01-01 UTC'), '2000-01-01 UTC'], [new ComparisonTest_Class(5), new ComparisonTest_Class(5)], [null, 1], + ['1 == 1 (string)' => new \DateInterval('PT1H'), '+1 hour'], + ['1 == 1 (\DateInterval instance)' => new \DateInterval('PT1H'), new \DateInterval('PT1H')], + ['-1 == -1' => $negativeDateInterval, '-1 hour'], ]; } @@ -67,6 +73,9 @@ public function provideValidComparisonsToPropertyPath(): array */ public function provideInvalidComparisons(): array { + $negativeDateInterval = new \DateInterval('PT1H'); + $negativeDateInterval->invert = 1; + return [ [1, '1', 2, '2', 'integer'], ['22', '"22"', '333', '"333"', 'string'], @@ -74,6 +83,9 @@ public function provideInvalidComparisons(): array [new \DateTime('2001-01-01'), 'Jan 1, 2001, 12:00 AM', '2000-01-01', 'Jan 1, 2000, 12:00 AM', 'DateTime'], [new \DateTime('2001-01-01 UTC'), 'Jan 1, 2001, 12:00 AM', '2000-01-01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'], [new ComparisonTest_Class(4), '4', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'], + ['1 != 2 (string)' => new \DateInterval('PT1H'), '1 hour', '+2 hour', '2 hours', \DateInterval::class], + ['1 != 2 (\DateInterval instance)' => new \DateInterval('PT1H'), '1 hour', new \DateInterval('PT2H'), '2 hours', \DateInterval::class], + ['-1 != -2' => $negativeDateInterval, '-1 hour', '-2 hours', '-2 hours', \DateInterval::class], ]; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php index 44924c276767..621c0a2b451c 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php @@ -40,6 +40,9 @@ protected function getErrorCode(): ?string */ public function provideValidComparisons(): array { + $negativeDateInterval = new \DateInterval('PT30S'); + $negativeDateInterval->invert = 1; + return [ [3, 2], [1, 1], @@ -52,6 +55,12 @@ public function provideValidComparisons(): array ['a', 'a'], ['z', 'a'], [null, 1], + ['30 > 29 (string)' => new \DateInterval('PT30S'), '+29 seconds'], + ['30 > 29 (\DateInterval instance)' => new \DateInterval('PT30S'), new \DateInterval('PT29S')], + ['30 = 30 (string)' => new \DateInterval('PT30S'), '+30 seconds'], + ['30 = 30 (\DateInterval instance)' => new \DateInterval('PT30S'), new \DateInterval('PT30S')], + ['-30 > -31' => $negativeDateInterval, '-31 seconds'], + ['-30 = -30' => $negativeDateInterval, '-30 seconds'], ]; } @@ -71,12 +80,18 @@ public function provideValidComparisonsToPropertyPath(): array */ public function provideInvalidComparisons(): array { + $negativeDateInterval = new \DateInterval('PT30S'); + $negativeDateInterval->invert = 1; + return [ [1, '1', 2, '2', 'integer'], [new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', new \DateTime('2005/01/01'), 'Jan 1, 2005, 12:00 AM', 'DateTime'], [new \DateTime('2000/01/01'), 'Jan 1, 2000, 12:00 AM', '2005/01/01', 'Jan 1, 2005, 12:00 AM', 'DateTime'], [new \DateTime('2000/01/01 UTC'), 'Jan 1, 2000, 12:00 AM', '2005/01/01 UTC', 'Jan 1, 2005, 12:00 AM', 'DateTime'], ['b', '"b"', 'c', '"c"', 'string'], + ['30 < 31 (string)' => new \DateInterval('PT30S'), '30 seconds', '+31 seconds', '31 seconds', \DateInterval::class], + ['30 < 31 (\DateInterval instance)' => new \DateInterval('PT30S'), '30 seconds', new \DateInterval('PT31S'), '31 seconds', \DateInterval::class], + ['-30 < -29' => $negativeDateInterval, '-30 seconds', '-29 seconds', '-29 seconds', \DateInterval::class], ]; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorWithPositiveOrZeroConstraintTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorWithPositiveOrZeroConstraintTest.php index 8db8eddf7c05..b25765fd5e4a 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorWithPositiveOrZeroConstraintTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorWithPositiveOrZeroConstraintTest.php @@ -38,6 +38,8 @@ public function provideValidComparisons(): array ['0', '0'], ['333', '0'], [null, 0], + ['30 >= 0' => new \DateInterval('PT30S'), 0], + ['0 >= 0' => new \DateInterval('PT0S'), 0], ]; } @@ -46,10 +48,14 @@ public function provideValidComparisons(): array */ public function provideInvalidComparisons(): array { + $negativeDateInterval = new \DateInterval('PT45S'); + $negativeDateInterval->invert = 1; + return [ [-1, '-1', 0, '0', 'integer'], [-2, '-2', 0, '0', 'integer'], [-2.5, '-2.5', 0, '0', 'integer'], + ['-45 < 0' => $negativeDateInterval, '-45 seconds', 0, '0', \DateInterval::class], ]; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php index eef12d5570bd..b2d115e0974d 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php @@ -40,6 +40,9 @@ protected function getErrorCode(): ?string */ public function provideValidComparisons(): array { + $negativeDateInterval = new \DateInterval('PT30S'); + $negativeDateInterval->invert = 1; + return [ [2, 1], [new \DateTime('2005/01/01'), new \DateTime('2001/01/01')], @@ -48,6 +51,9 @@ public function provideValidComparisons(): array [new ComparisonTest_Class(5), new ComparisonTest_Class(4)], ['333', '22'], [null, 1], + ['30 > 29 (string)' => new \DateInterval('PT30S'), '+29 seconds'], + ['30 > 29 (\DateInterval instance)' => new \DateInterval('PT30S'), new \DateInterval('PT29S')], + ['-30 > -31' => $negativeDateInterval, '-31 seconds'], ]; } @@ -66,6 +72,9 @@ public function provideValidComparisonsToPropertyPath(): array */ public function provideInvalidComparisons(): array { + $negativeDateInterval = new \DateInterval('PT30S'); + $negativeDateInterval->invert = 1; + return [ [1, '1', 2, '2', 'integer'], [2, '2', 2, '2', 'integer'], @@ -79,6 +88,9 @@ public function provideInvalidComparisons(): array [new ComparisonTest_Class(5), '5', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'], ['22', '"22"', '333', '"333"', 'string'], ['22', '"22"', '22', '"22"', 'string'], + ['30 < 31 (string)' => new \DateInterval('PT30S'), '30 seconds', '+31 seconds', '31 seconds', \DateInterval::class], + ['30 < 31 (\DateInterval instance)' => new \DateInterval('PT30S'), '30 seconds', new \DateInterval('PT31S'), '31 seconds', \DateInterval::class], + ['-30 < -29' => $negativeDateInterval, '-30 seconds', '-29 seconds', '-29 seconds', \DateInterval::class], ]; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorWithPositiveConstraintTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorWithPositiveConstraintTest.php index ef10787bcccc..dd7b27b7906f 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorWithPositiveConstraintTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorWithPositiveConstraintTest.php @@ -35,6 +35,7 @@ public function provideValidComparisons(): array [2.5, 0], ['333', '0'], [null, 0], + ['1 > 0' => new \DateInterval('P1W'), 0], ]; } @@ -43,11 +44,15 @@ public function provideValidComparisons(): array */ public function provideInvalidComparisons(): array { + $negativeDateInterval = new \DateInterval('P1W'); + $negativeDateInterval->invert = 1; + return [ [0, '0', 0, '0', 'integer'], [-1, '-1', 0, '0', 'integer'], [-2, '-2', 0, '0', 'integer'], [-2.5, '-2.5', 0, '0', 'integer'], + ['-1 < 0' => $negativeDateInterval, '-7 days', 0, '0', \DateInterval::class], ]; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php index e7856a8b99af..b5d00561b93a 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php @@ -67,6 +67,12 @@ public function provideValidComparisons(): array $immutableDate = new \DateTimeImmutable('2000-01-01'); $comparisons[] = [$immutableDate, $immutableDate]; + $comparisons['\DateInterval instance === \DateInterval instance'] = [$dateInterval = new \DateInterval('P2Y'), $dateInterval]; + + $negativeDateInterval = new \DateInterval('P2Y'); + $negativeDateInterval->invert = 1; + $comparisons['negative \DateInterval instance === negative \DateInterval instance'] = [$negativeDateInterval, $negativeDateInterval]; + return $comparisons; } @@ -85,6 +91,9 @@ public function provideValidComparisonsToPropertyPath(): array */ public function provideInvalidComparisons(): array { + $negativeDateInterval = new \DateInterval('P2Y'); + $negativeDateInterval->invert = 1; + return [ [1, '1', 2, '2', 'integer'], [2, '2', '2', '"2"', 'string'], @@ -92,6 +101,8 @@ public function provideInvalidComparisons(): array [new \DateTime('2001-01-01'), 'Jan 1, 2001, 12:00 AM', new \DateTime('2001-01-01'), 'Jan 1, 2001, 12:00 AM', 'DateTime'], [new \DateTime('2001-01-01'), 'Jan 1, 2001, 12:00 AM', new \DateTime('1999-01-01'), 'Jan 1, 1999, 12:00 AM', 'DateTime'], [new ComparisonTest_Class(4), '4', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'], + ['\DateInterval instance !== same string' => new \DateInterval('P22M'), '22 months', '+22 months', '1 year and 10 months', \DateInterval::class], + ['negative \DateInterval instance !== same negative string' => $negativeDateInterval, '-2 years', '-2 years', '-2 years', \DateInterval::class], ]; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php index ab05e3ca64c0..c22a839c005d 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php @@ -40,6 +40,9 @@ protected function getErrorCode(): ?string */ public function provideValidComparisons(): array { + $negativeDateInterval = new \DateInterval('PT30H'); + $negativeDateInterval->invert = 1; + return [ [1, 2], [1, 1], @@ -54,6 +57,12 @@ public function provideValidComparisons(): array ['a', 'a'], ['a', 'z'], [null, 1], + ['30 < 31 (string)' => new \DateInterval('PT30H'), '+31 hours'], + ['30 < 31 (\DateInterval instance)' => new \DateInterval('PT30H'), new \DateInterval('PT31H')], + ['30 = 30' => new \DateInterval('PT30H'), '+30 hours'], + ['30 = 30' => new \DateInterval('PT30H'), new \DateInterval('PT30H')], + ['-30 < -29' => $negativeDateInterval, '-29 hours'], + ['-30 = -30' => $negativeDateInterval, '-30 hours'], ]; } @@ -73,6 +82,9 @@ public function provideValidComparisonsToPropertyPath(): array */ public function provideInvalidComparisons(): array { + $negativeDateInterval = new \DateInterval('PT30H'); + $negativeDateInterval->invert = 1; + return [ [2, '2', 1, '1', 'integer'], [new \DateTime('2010-01-01'), 'Jan 1, 2010, 12:00 AM', new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', 'DateTime'], @@ -80,6 +92,9 @@ public function provideInvalidComparisons(): array [new \DateTime('2010-01-01 UTC'), 'Jan 1, 2010, 12:00 AM', '2000-01-01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'], [new ComparisonTest_Class(5), '5', new ComparisonTest_Class(4), '4', __NAMESPACE__.'\ComparisonTest_Class'], ['c', '"c"', 'b', '"b"', 'string'], + ['30 > 29 (string)' => new \DateInterval('PT30H'), '30 hours', '+29 hours', '1 day and 5 hours', \DateInterval::class], + ['30 > 29 (\DateInterval instance)' => new \DateInterval('PT30H'), '30 hours', new \DateInterval('PT29H'), '1 day and 5 hours', \DateInterval::class], + ['-30 > -31' => $negativeDateInterval, '-30 hours', '-31 hours', '-1 day and 7 hours', \DateInterval::class], ]; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorWithNegativeOrZeroConstraintTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorWithNegativeOrZeroConstraintTest.php index 5e4fff253b4d..60bd1ca9f9ec 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorWithNegativeOrZeroConstraintTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorWithNegativeOrZeroConstraintTest.php @@ -30,12 +30,17 @@ protected function createConstraint(array $options = null): Constraint */ public function provideValidComparisons(): array { + $negativeDateInterval = new \DateInterval('PT30S'); + $negativeDateInterval->invert = 1; + return [ [0, 0], [-1, 0], [-2, 0], [-2.5, 0], [null, 0], + ['-30 <= 0' => $negativeDateInterval, 0], + ['0 <= 0' => new \DateInterval('PT0S'), 0], ]; } @@ -48,6 +53,7 @@ public function provideInvalidComparisons(): array [2, '2', 0, '0', 'integer'], [2.5, '2.5', 0, '0', 'integer'], [333, '333', 0, '0', 'integer'], + ['1 > 0' => new \DateInterval('P1Y'), '1 year', 0, '0', \DateInterval::class], ]; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php index d8aaa99a982c..722ccee50d1c 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php @@ -40,6 +40,9 @@ protected function getErrorCode(): ?string */ public function provideValidComparisons(): array { + $negativeDateInterval = new \DateInterval('PT30S'); + $negativeDateInterval->invert = 1; + return [ [1, 2], [new \DateTime('2000-01-01'), new \DateTime('2010-01-01')], @@ -48,6 +51,9 @@ public function provideValidComparisons(): array [new ComparisonTest_Class(4), new ComparisonTest_Class(5)], ['22', '333'], [null, 1], + ['30 < 31 (string)' => new \DateInterval('PT30S'), '+31 seconds'], + ['30 < 31 (\DateInterval instance)' => new \DateInterval('PT30S'), new \DateInterval('PT31S')], + ['-30 < -29' => $negativeDateInterval, '-29 seconds'], ]; } @@ -66,6 +72,9 @@ public function provideValidComparisonsToPropertyPath(): array */ public function provideInvalidComparisons(): array { + $negativeDateInterval = new \DateInterval('PT30S'); + $negativeDateInterval->invert = 1; + return [ [3, '3', 2, '2', 'integer'], [2, '2', 2, '2', 'integer'], @@ -78,6 +87,9 @@ public function provideInvalidComparisons(): array [new ComparisonTest_Class(5), '5', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'], [new ComparisonTest_Class(6), '6', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'], ['333', '"333"', '22', '"22"', 'string'], + ['30 > 29 (string)' => new \DateInterval('PT30S'), '30 seconds', '+29 seconds', '29 seconds', \DateInterval::class], + ['30 > 29 (\DateInterval instance)' => new \DateInterval('PT30S'), '30 seconds', new \DateInterval('PT29S'), '29 seconds', \DateInterval::class], + ['-30 > -31' => $negativeDateInterval, '-30 seconds', '-31 seconds', '-31 seconds', \DateInterval::class], ]; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorWithNegativeConstraintTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorWithNegativeConstraintTest.php index 642bd8341f29..fe8ea9e1b99a 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorWithNegativeConstraintTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorWithNegativeConstraintTest.php @@ -30,11 +30,15 @@ protected function createConstraint(array $options = null): Constraint */ public function provideValidComparisons(): array { + $negativeDateInterval = new \DateInterval('PT440S'); + $negativeDateInterval->invert = 1; + return [ [-1, 0], [-2, 0], [-2.5, 0], [null, 0], + ['-440 < 0' => $negativeDateInterval, 0], ]; } @@ -48,6 +52,7 @@ public function provideInvalidComparisons(): array [2, '2', 0, '0', 'integer'], [2.5, '2.5', 0, '0', 'integer'], [333, '333', 0, '0', 'integer'], + ['1 > 0' => new \DateInterval('P1D'), '1 day', 0, '0', \DateInterval::class], ]; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php index 22f9b3b1107d..fbfeedab21c5 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php @@ -40,6 +40,9 @@ protected function getErrorCode(): ?string */ public function provideValidComparisons(): array { + $negativeDateInterval = new \DateInterval('PT1H'); + $negativeDateInterval->invert = 1; + return [ [1, 2], ['22', '333'], @@ -48,6 +51,9 @@ public function provideValidComparisons(): array [new \DateTime('2001-01-01 UTC'), '2000-01-01 UTC'], [new ComparisonTest_Class(6), new ComparisonTest_Class(5)], [null, 1], + ['1 != 2 (string)' => new \DateInterval('PT1H'), '+2 hours'], + ['1 != 2 (\DateInterval instance)' => new \DateInterval('PT1H'), new \DateInterval('PT2H')], + ['-1 != -2' => $negativeDateInterval, '-2 hours'], ]; } @@ -66,6 +72,9 @@ public function provideValidComparisonsToPropertyPath(): array */ public function provideInvalidComparisons(): array { + $negativeDateInterval = new \DateInterval('PT1H'); + $negativeDateInterval->invert = 1; + return [ [3, '3', 3, '3', 'integer'], ['2', '"2"', 2, '2', 'integer'], @@ -74,6 +83,9 @@ public function provideInvalidComparisons(): array [new \DateTime('2000-01-01'), 'Jan 1, 2000, 12:00 AM', '2000-01-01', 'Jan 1, 2000, 12:00 AM', 'DateTime'], [new \DateTime('2000-01-01 UTC'), 'Jan 1, 2000, 12:00 AM', '2000-01-01 UTC', 'Jan 1, 2000, 12:00 AM', 'DateTime'], [new ComparisonTest_Class(5), '5', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'], + ['1 == 1 (string)' => new \DateInterval('PT1H'), '1 hour', '+1 hour', '1 hour', \DateInterval::class], + ['1 == 1 (\DateInterval instance)' => new \DateInterval('PT1H'), '1 hour', new \DateInterval('PT1H'), '1 hour', \DateInterval::class], + ['-1 == -1' => $negativeDateInterval, '-1 hour', '-1 hour', '-1 hour', \DateInterval::class], ]; } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php index 8a36828f1eff..3c7179633160 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php @@ -40,6 +40,9 @@ protected function getErrorCode(): ?string */ public function provideValidComparisons(): array { + $negativeDateInterval = new \DateInterval('P2Y'); + $negativeDateInterval->invert = 1; + return [ [1, 2], ['2', 2], @@ -51,6 +54,8 @@ public function provideValidComparisons(): array [new \DateTime('2001-01-01'), '2000-01-01'], [new \DateTime('2000-01-01 UTC'), '2000-01-01 UTC'], [null, 1], + ['\DateInterval instance !== same string' => new \DateInterval('P22M'), '22 months', '+22 months', '1 year and 10 months', \DateInterval::class], + ['negative \DateInterval instance !== same negative string' => $negativeDateInterval, '-2 years', '-2 years', '-2 years', \DateInterval::class], ]; } @@ -85,11 +90,16 @@ public function provideInvalidComparisons(): array $date = new \DateTime('2000-01-01'); $object = new ComparisonTest_Class(2); + $negativeDateInterval = new \DateInterval('P2Y'); + $negativeDateInterval->invert = 1; + $comparisons = [ [3, '3', 3, '3', 'integer'], ['a', '"a"', 'a', '"a"', 'string'], [$date, 'Jan 1, 2000, 12:00 AM', $date, 'Jan 1, 2000, 12:00 AM', 'DateTime'], [$object, '2', $object, '2', __NAMESPACE__.'\ComparisonTest_Class'], + '\DateInterval instance === \DateInterval instance' => [$dateInterval = new \DateInterval('P1W'), '7 days', $dateInterval, '7 days', \DateInterval::class], + 'negative \DateInterval instance === negative \DateInterval instance' => [$negativeDateInterval, '-2 years', $negativeDateInterval, '-2 years', \DateInterval::class], ]; return $comparisons; diff --git a/src/Symfony/Component/Validator/Tests/Constraints/RangeValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/RangeValidatorTest.php index ef70c1614571..87611380bbd2 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/RangeValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/RangeValidatorTest.php @@ -754,6 +754,182 @@ public function testInvalidDatesCombinedMinPropertyPath($value, $dateTimeAsStrin ->setCode(Range::NOT_IN_RANGE_ERROR) ->assertRaised(); } + + /** + * @dataProvider validDateIntervalsMinOnlyProvider + */ + public function testValidDateIntervalsMinOnly(\DateInterval $value, $min) + { + $this->validator->validate($value, new Range([ + 'min' => $min, + ])); + + $this->assertNoViolation(); + } + + public function validDateIntervalsMinOnlyProvider() + { + $negative = new \DateInterval('PT30S'); + $negative->invert = 1; + + return [ + ['30 > 20 (\DateInterval instance)' => new \DateInterval('PT30S'), new \DateInterval('PT20S')], + ['30 > 20 (string)' => new \DateInterval('PT30S'), '+20 seconds'], + ['30 === 30' => new \DateInterval('PT30S'), '+30 seconds'], + ['30 > 0' => new \DateInterval('PT30S'), 0], + ['-30 > -31' => $negative, '-31 seconds'], + ]; + } + + /** + * @dataProvider invalidDateIntervalsMinOnlyProvider + */ + public function testInvalidDateIntervalsMinOnly(\DateInterval $value, $min, string $expectedValue, string $expectedLimit) + { + $this->validator->validate($value, new Range([ + 'min' => $min, + 'minMessage' => 'foo', + ])); + + $this->buildViolation('foo') + ->setParameter('{{ value }}', $expectedValue) + ->setParameter('{{ limit }}', $expectedLimit) + ->setCode(Range::TOO_LOW_ERROR) + ->assertRaised(); + } + + public function invalidDateIntervalsMinOnlyProvider() + { + $negative = new \DateInterval('PT30S'); + $negative->invert = 1; + + return [ + ['30 < 31 (\DateInterval instance)' => new \DateInterval('PT30S'), new \DateInterval('PT31S'), '30 seconds', '31 seconds'], + ['30 < 31 (string)' => new \DateInterval('PT30S'), '+31 seconds', '30 seconds', '31 seconds'], + ['-30 < 0' => $negative, 0, '-30 seconds', '0'], + ['-30 < -29' => $negative, '-29 seconds', '-30 seconds', '-29 seconds'], + ]; + } + + /** + * @dataProvider validDateIntervalsMaxOnlyProvider + */ + public function testValidDateIntervalsMaxOnly(\DateInterval $value, $max) + { + $this->validator->validate($value, new Range([ + 'max' => $max, + ])); + + $this->assertNoViolation(); + } + + public function validDateIntervalsMaxOnlyProvider() + { + $negative = new \DateInterval('PT30S'); + $negative->invert = 1; + + return [ + ['30 < 31 (\DateInterval instance)' => new \DateInterval('PT30S'), new \DateInterval('PT31S')], + ['30 < 31 (string)' => new \DateInterval('PT30S'), '+31 seconds'], + ['30 === 30' => new \DateInterval('PT30S'), '+30 seconds'], + ['-30 < 0' => $negative, 0], + ['-30 < -20' => $negative, '-20 seconds'], + ]; + } + + /** + * @dataProvider invalidDateIntervalsMaxOnlyProvider + */ + public function testInvalidDateIntervalsMaxOnly(\DateInterval $value, $max, string $expectedValue, string $expectedLimit) + { + $this->validator->validate($value, new Range([ + 'max' => $max, + 'maxMessage' => 'foo', + ])); + + $this->buildViolation('foo') + ->setParameter('{{ value }}', $expectedValue) + ->setParameter('{{ limit }}', $expectedLimit) + ->setCode(Range::TOO_HIGH_ERROR) + ->assertRaised(); + } + + public function invalidDateIntervalsMaxOnlyProvider() + { + $negative = new \DateInterval('PT30S'); + $negative->invert = 1; + + return [ + ['30 > 29 (\DateInterval instance)' => new \DateInterval('PT30S'), new \DateInterval('PT29S'), '30 seconds', '29 seconds'], + ['30 > 29 (string)' => new \DateInterval('PT30S'), '+29 seconds', '30 seconds', '29 seconds'], + ['30 > 0' => new \DateInterval('PT30S'), 0, '30 seconds', '0'], + ['-30 > -31' => $negative, '-31 seconds', '-30 seconds', '-31 seconds'], + ]; + } + + /** + * @dataProvider validDateIntervalsCombinedProvider + */ + public function testValidDateIntervalsCombined(\DateInterval $value, $min, $max) + { + $this->validator->validate($value, new Range([ + 'min' => $min, + 'max' => $max, + ])); + + $this->assertNoViolation(); + } + + public function validDateIntervalsCombinedProvider() + { + $negative = new \DateInterval('PT30S'); + $negative->invert = 1; + + return [ + ['31 < 30 < 29 (2 \DateInterval instances)' => new \DateInterval('PT30S'), new \DateInterval('PT29S'), new \DateInterval('PT31S')], + ['31 < 30 < 29 (\DateInterval instances & string)' => new \DateInterval('PT30S'), new \DateInterval('PT29S'), '+31 seconds'], + ['31 < 30 < 29 (string & \DateInterval instance)' => new \DateInterval('PT30S'), '+29 seconds', new \DateInterval('PT31S')], + ['31 < 30 < 29 (2 strings)' => new \DateInterval('PT30S'), '+29 seconds', '+31 seconds'], + ['31 < 30 < 0' => new \DateInterval('PT30S'), 0, '+31 seconds'], + ['0 < -30 < -31' => $negative, '-31 seconds', 0], + ['-29 < -30 < -31' => $negative, '-31 seconds', '-29 seconds'], + ]; + } + + /** + * @dataProvider invalidDateIntervalsCombinedProvider + */ + public function testInvalidDateIntervalsCombined(\DateInterval $value, $min, $max, string $expectedValue, string $expectedMin, string $expectedMax) + { + $this->validator->validate($value, new Range([ + 'min' => $min, + 'max' => $max, + 'notInRangeMessage' => 'foo', + ])); + + $this->buildViolation('foo') + ->setParameter('{{ value }}', $expectedValue) + ->setParameter('{{ min }}', $expectedMin) + ->setParameter('{{ max }}', $expectedMax) + ->setCode(Range::NOT_IN_RANGE_ERROR) + ->assertRaised(); + } + + public function invalidDateIntervalsCombinedProvider() + { + $negative = new \DateInterval('PT30S'); + $negative->invert = 1; + + return [ + ['30 < 31 && 30 > 29 (2 \DateInterval instances)' => new \DateInterval('PT30S'), new \DateInterval('PT31S'), new \DateInterval('PT29S'), '30 seconds', '31 seconds', '29 seconds'], + ['30 < 31 && 30 > 29 (\DateInterval instances & string)' => new \DateInterval('PT30S'), new \DateInterval('PT31S'), '+29 seconds', '30 seconds', '31 seconds', '29 seconds'], + ['30 < 31 && 30 > 29 (string & \DateInterval instance)' => new \DateInterval('PT30S'), '+31 seconds', new \DateInterval('PT29S'), '30 seconds', '31 seconds', '29 seconds'], + ['30 < 31 && 30 > 29 (2 strings)' => new \DateInterval('PT30S'), '+31 seconds', '+29 seconds', '30 seconds', '31 seconds', '29 seconds'], + ['30 < 31 && 30 > 0' => new \DateInterval('PT30S'), '+31 seconds', 0, '30 seconds', '31 seconds', '0'], + ['-30 < 0 && -30 > -31' => $negative, 0, '-31 seconds', '-30 seconds', '0', '-31 seconds'], + ['-30 < -29 && -30 > -31' => $negative, '-29 seconds', '-31 seconds', '-30 seconds', '-29 seconds', '-31 seconds'], + ]; + } } final class Limit diff --git a/src/Symfony/Component/Validator/Tests/Util/DateIntervalComparisonHelperTest.php b/src/Symfony/Component/Validator/Tests/Util/DateIntervalComparisonHelperTest.php new file mode 100644 index 000000000000..9f6c158980bf --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Util/DateIntervalComparisonHelperTest.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Util; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Util\DateIntervalComparisonHelper; + +final class DateIntervalComparisonHelperTest extends TestCase +{ + /** + * @dataProvider supportsProvider + */ + public function testSupports(bool $expected, $value, $comparedValue) + { + $this->assertSame($expected, DateIntervalComparisonHelper::supports($value, $comparedValue)); + } + + public function supportsProvider() + { + return [ + [false, 'foo', 'bar'], + [false, $dateInterval = new \DateInterval('PT30S'), new \stdClass()], + [false, $dateInterval, $dateInterval], + [false, $dateInterval, 2], + [true, $dateInterval, 'foo'], + [true, $dateInterval, new \DateInterval('PT2S')], + [true, $dateInterval, 0], + ]; + } + + public function testConvertValue() + { + $this->assertEquals(new \DateTimeImmutable('@0'), DateIntervalComparisonHelper::convertValue(new \DateTimeImmutable('@0-30 seconds'), new \DateInterval('PT30S'))); + } + + public function testConvertComparedValueWhenTheStringComparedValueIsInvalid() + { + $this->expectException(\InvalidArgumentException::class); + + DateIntervalComparisonHelper::convertComparedValue(new \DateTimeImmutable(), 'foo'); + } + + /** + * @dataProvider convertComparedValueProvider + */ + public function testConvertComparedValue($expected, \DateTimeImmutable $reference, $comparedValue, bool $strict = false) + { + $convertedComparedValue = DateIntervalComparisonHelper::convertComparedValue($reference, $comparedValue); + + if (!$strict) { + $this->assertEquals($expected, $convertedComparedValue); + } else { + $this->assertSame($expected, $convertedComparedValue); + } + } + + public function convertComparedValueProvider() + { + return [ + [new \DateTimeImmutable('@0-45 minutes'), new \DateTimeImmutable('@0'), '-45 minutes'], + [new \DateTimeImmutable('@0'), new \DateTimeImmutable('@0-1 year'), new \DateInterval('P1Y')], + [$reference = new \DateTimeImmutable(), $reference, 0], + ]; + } +} diff --git a/src/Symfony/Component/Validator/Util/DateIntervalComparisonHelper.php b/src/Symfony/Component/Validator/Util/DateIntervalComparisonHelper.php new file mode 100644 index 000000000000..bb63be330145 --- /dev/null +++ b/src/Symfony/Component/Validator/Util/DateIntervalComparisonHelper.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Util; + +/** + * @internal + */ +final class DateIntervalComparisonHelper +{ + private function __construct() + { + } + + public static function supports($value, $comparedValue): bool + { + return $value instanceof \DateInterval && ( + \is_string($comparedValue) || + ($comparedValue instanceof \DateInterval && $value !== $comparedValue) || + 0 === $comparedValue + ); + } + + public static function convertValue(\DateTimeImmutable $reference, \DateInterval $value): \DateTimeImmutable + { + return \DateTimeImmutable::createFromMutable(self::getMutableReference($reference))->add($value); + } + + public static function convertComparedValue(\DateTimeImmutable $reference, $comparedValue): \DateTimeImmutable + { + if (\is_string($comparedValue)) { + $reference = \DateTimeImmutable::createFromMutable(self::getMutableReference($reference)); + + set_error_handler(function (int $errno, string $errstr): void { + throw new \InvalidArgumentException($errstr); + }); + + try { + return $reference->modify($comparedValue); + } finally { + restore_error_handler(); + } + } + + if ($comparedValue instanceof \DateInterval) { + return \DateTimeImmutable::createFromMutable(self::getMutableReference($reference)->add($comparedValue)); + } + + if (0 === $comparedValue) { + return $reference; + } + + throw new \LogicException(); + } + + private static function getMutableReference(\DateTimeImmutable $reference): \DateTime + { + if (\PHP_VERSION_ID >= 70300) { + return \DateTime::createFromImmutable($reference); + } + + return \DateTime::createFromFormat($format = 'U.u', $reference->format($format)); + } +}