mirror of
https://github.com/symfony/validator.git
synced 2026-03-24 01:12:13 +01:00
Use ClockInterface and DatePoint in AbstractComparisonValidator and RangeValidator so that relative date strings (e.g. "now", "-40 days") are resolved against the injected clock instead of the system clock. This makes date comparisons testable with MockClock while remaining fully backward compatible (falls back to system clock when no clock is injected).
193 lines
7.4 KiB
PHP
193 lines
7.4 KiB
PHP
<?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\Clock\ClockInterface;
|
|
use Symfony\Component\Clock\DatePoint;
|
|
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
|
|
use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
|
|
use Symfony\Component\PropertyAccess\PropertyAccess;
|
|
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
|
use Symfony\Component\Validator\Constraint;
|
|
use Symfony\Component\Validator\ConstraintValidator;
|
|
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
|
|
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
|
|
|
/**
|
|
* @author Bernhard Schussek <bschussek@gmail.com>
|
|
*/
|
|
class RangeValidator extends ConstraintValidator
|
|
{
|
|
public function __construct(
|
|
private ?PropertyAccessorInterface $propertyAccessor = null,
|
|
private ?ClockInterface $clock = null,
|
|
) {
|
|
}
|
|
|
|
public function validate(mixed $value, Constraint $constraint): void
|
|
{
|
|
if (!$constraint instanceof Range) {
|
|
throw new UnexpectedTypeException($constraint, Range::class);
|
|
}
|
|
|
|
if (null === $value) {
|
|
return;
|
|
}
|
|
|
|
$min = $this->getLimit($constraint->minPropertyPath, $constraint->min, $constraint);
|
|
$max = $this->getLimit($constraint->maxPropertyPath, $constraint->max, $constraint);
|
|
|
|
if (!is_numeric($value) && !$value instanceof \DateTimeInterface) {
|
|
if ($this->isParsableDatetimeString($min) && $this->isParsableDatetimeString($max)) {
|
|
$this->context->buildViolation($constraint->invalidDateTimeMessage)
|
|
->setParameter('{{ value }}', $this->formatValue($value, self::PRETTY_DATE))
|
|
->setCode(Range::INVALID_CHARACTERS_ERROR)
|
|
->addViolation();
|
|
} else {
|
|
$this->context->buildViolation($constraint->invalidMessage)
|
|
->setParameter('{{ value }}', $this->formatValue($value, self::PRETTY_DATE))
|
|
->setCode(Range::INVALID_CHARACTERS_ERROR)
|
|
->addViolation();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// 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 instanceof \DateTimeInterface) {
|
|
if (\is_string($min)) {
|
|
try {
|
|
$min = $this->clock ? $value::createFromInterface(new DatePoint($min, null, $this->clock->now())) : new $value($min);
|
|
} catch (\Exception) {
|
|
throw new ConstraintDefinitionException(\sprintf('The min value "%s" could not be converted to a "%s" instance in the "%s" constraint.', $min, get_debug_type($value), get_debug_type($constraint)));
|
|
}
|
|
}
|
|
|
|
if (\is_string($max)) {
|
|
try {
|
|
$max = $this->clock ? $value::createFromInterface(new DatePoint($max, null, $this->clock->now())) : new $value($max);
|
|
} catch (\Exception) {
|
|
throw new ConstraintDefinitionException(\sprintf('The max value "%s" could not be converted to a "%s" instance in the "%s" constraint.', $max, get_debug_type($value), get_debug_type($constraint)));
|
|
}
|
|
}
|
|
}
|
|
|
|
$hasLowerLimit = null !== $min;
|
|
$hasUpperLimit = null !== $max;
|
|
|
|
if ($hasLowerLimit && $hasUpperLimit && ($value < $min || $value > $max)) {
|
|
$message = $constraint->notInRangeMessage;
|
|
$code = Range::NOT_IN_RANGE_ERROR;
|
|
|
|
$violationBuilder = $this->context->buildViolation($message)
|
|
->setParameter('{{ value }}', $this->formatValue($value, self::PRETTY_DATE))
|
|
->setParameter('{{ min }}', $this->formatValue($min, self::PRETTY_DATE))
|
|
->setParameter('{{ max }}', $this->formatValue($max, self::PRETTY_DATE))
|
|
->setCode($code);
|
|
|
|
if (null !== $constraint->maxPropertyPath) {
|
|
$violationBuilder->setParameter('{{ max_limit_path }}', $constraint->maxPropertyPath);
|
|
}
|
|
|
|
if (null !== $constraint->minPropertyPath) {
|
|
$violationBuilder->setParameter('{{ min_limit_path }}', $constraint->minPropertyPath);
|
|
}
|
|
|
|
$violationBuilder->addViolation();
|
|
|
|
return;
|
|
}
|
|
|
|
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))
|
|
->setCode(Range::TOO_HIGH_ERROR);
|
|
|
|
if (null !== $constraint->maxPropertyPath) {
|
|
$violationBuilder->setParameter('{{ max_limit_path }}', $constraint->maxPropertyPath);
|
|
}
|
|
|
|
if (null !== $constraint->minPropertyPath) {
|
|
$violationBuilder->setParameter('{{ min_limit_path }}', $constraint->minPropertyPath);
|
|
}
|
|
|
|
$violationBuilder->addViolation();
|
|
|
|
return;
|
|
}
|
|
|
|
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))
|
|
->setCode(Range::TOO_LOW_ERROR);
|
|
|
|
if (null !== $constraint->maxPropertyPath) {
|
|
$violationBuilder->setParameter('{{ max_limit_path }}', $constraint->maxPropertyPath);
|
|
}
|
|
|
|
if (null !== $constraint->minPropertyPath) {
|
|
$violationBuilder->setParameter('{{ min_limit_path }}', $constraint->minPropertyPath);
|
|
}
|
|
|
|
$violationBuilder->addViolation();
|
|
}
|
|
}
|
|
|
|
private function getLimit(?string $propertyPath, mixed $default, Constraint $constraint): mixed
|
|
{
|
|
if (null === $propertyPath) {
|
|
return $default;
|
|
}
|
|
|
|
if (null === $object = $this->context->getObject()) {
|
|
return $default;
|
|
}
|
|
|
|
try {
|
|
return $this->getPropertyAccessor()->getValue($object, $propertyPath);
|
|
} catch (NoSuchPropertyException $e) {
|
|
throw new ConstraintDefinitionException(\sprintf('Invalid property path "%s" provided to "%s" constraint: ', $propertyPath, get_debug_type($constraint)).$e->getMessage(), 0, $e);
|
|
} catch (UninitializedPropertyException) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private function getPropertyAccessor(): PropertyAccessorInterface
|
|
{
|
|
return $this->propertyAccessor ??= PropertyAccess::createPropertyAccessor();
|
|
}
|
|
|
|
private function isParsableDatetimeString(mixed $boundary): bool
|
|
{
|
|
if (null === $boundary) {
|
|
return true;
|
|
}
|
|
|
|
if (!\is_string($boundary)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
new \DateTimeImmutable($boundary);
|
|
} catch (\Exception) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|