From 4079ab5e0aaa5dd7fff81f274f1d24433ed2d9f6 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 2 Aug 2024 10:08:15 +0200 Subject: [PATCH] [Validator] Add `Week` constraint --- src/Symfony/Component/Validator/CHANGELOG.md | 1 + .../Component/Validator/Constraints/Week.php | 66 ++++++++ .../Validator/Constraints/WeekValidator.php | 82 ++++++++++ .../Validator/Tests/Constraints/WeekTest.php | 101 +++++++++++++ .../Tests/Constraints/WeekValidatorTest.php | 142 ++++++++++++++++++ 5 files changed, 392 insertions(+) create mode 100644 src/Symfony/Component/Validator/Constraints/Week.php create mode 100644 src/Symfony/Component/Validator/Constraints/WeekValidator.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/WeekTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/WeekValidatorTest.php diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 46709ac0ac814..d8ae5e6cc1671 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG * Add `errorPath` to Unique constraint * Add the `format` option to the `Ulid` constraint to allow accepting different ULID formats * Add the `WordCount` constraint + * Add the `Week` constraint 7.1 --- diff --git a/src/Symfony/Component/Validator/Constraints/Week.php b/src/Symfony/Component/Validator/Constraints/Week.php new file mode 100644 index 0000000000000..46b8869752fbe --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/Week.php @@ -0,0 +1,66 @@ + + * + * 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\Attribute\HasNamedArguments; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; + +/** + * @author Alexandre Daubois + */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +final class Week extends Constraint +{ + public const INVALID_FORMAT_ERROR = '19012dd1-01c8-4ce8-959f-72ad22684f5f'; + public const INVALID_WEEK_NUMBER_ERROR = 'd67ebfc9-45fe-4e4c-a038-5eaa56895ea3'; + public const TOO_LOW_ERROR = '9b506423-77a3-4749-aa34-c822a08be978'; + public const TOO_HIGH_ERROR = '85156377-d1e6-42cd-8f6e-dc43c2ecb72b'; + + protected const ERROR_NAMES = [ + self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR', + self::INVALID_WEEK_NUMBER_ERROR => 'INVALID_WEEK_NUMBER_ERROR', + self::TOO_LOW_ERROR => 'TOO_LOW_ERROR', + self::TOO_HIGH_ERROR => 'TOO_HIGH_ERROR', + ]; + + #[HasNamedArguments] + public function __construct( + public ?string $min = null, + public ?string $max = null, + public string $invalidFormatMessage = 'This value does not represent a valid week in the ISO 8601 format.', + public string $invalidWeekNumberMessage = 'The week "{{ value }}" is not a valid week.', + public string $tooLowMessage = 'The value should not be before week "{{ min }}".', + public string $tooHighMessage = 'The value should not be after week "{{ max }}".', + ?array $groups = null, + mixed $payload = null, + ) { + parent::__construct(null, $groups, $payload); + + if (null !== $min && !preg_match('/^\d{4}-W(0[1-9]|[1-4][0-9]|5[0-3])$/', $min)) { + throw new ConstraintDefinitionException(\sprintf('The "%s" constraint requires the min week to be in the ISO 8601 format if set.', __CLASS__)); + } + + if (null !== $max && !preg_match('/^\d{4}-W(0[1-9]|[1-4][0-9]|5[0-3])$/', $max)) { + throw new ConstraintDefinitionException(\sprintf('The "%s" constraint requires the max week to be in the ISO 8601 format if set.', __CLASS__)); + } + + if (null !== $min && null !== $max) { + [$minYear, $minWeekNumber] = \explode('-W', $min, 2); + [$maxYear, $maxWeekNumber] = \explode('-W', $max, 2); + + if ($minYear > $maxYear || ($minYear === $maxYear && $minWeekNumber > $maxWeekNumber)) { + throw new ConstraintDefinitionException(\sprintf('The "%s" constraint requires the min week to be less than or equal to the max week.', __CLASS__)); + } + } + } +} diff --git a/src/Symfony/Component/Validator/Constraints/WeekValidator.php b/src/Symfony/Component/Validator/Constraints/WeekValidator.php new file mode 100644 index 0000000000000..83052c1a9cb20 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/WeekValidator.php @@ -0,0 +1,82 @@ + + * + * 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\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; + +/** + * @author Alexandre Daubois + */ +final class WeekValidator extends ConstraintValidator +{ + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof Week) { + throw new UnexpectedTypeException($constraint, Week::class); + } + + if (null === $value) { + return; + } + + if (!\is_string($value) && !$value instanceof \Stringable) { + throw new UnexpectedValueException($value, 'string'); + } + + if (!preg_match('/^\d{4}-W(0[1-9]|[1-4][0-9]|5[0-3])$/D', $value)) { + $this->context->buildViolation($constraint->invalidFormatMessage) + ->setCode(Week::INVALID_FORMAT_ERROR) + ->addViolation(); + + return; + } + + [$year, $weekNumber] = \explode('-W', $value, 2); + $weeksInYear = (int) \date('W', \mktime(0, 0, 0, 12, 28, $year)); + + if ($weekNumber > $weeksInYear) { + $this->context->buildViolation($constraint->invalidWeekNumberMessage) + ->setCode(Week::INVALID_WEEK_NUMBER_ERROR) + ->setParameter('{{ value }}', $value) + ->addViolation(); + + return; + } + + if ($constraint->min) { + [$minYear, $minWeekNumber] = \explode('-W', $constraint->min, 2); + if ($year < $minYear || ($year === $minYear && $weekNumber < $minWeekNumber)) { + $this->context->buildViolation($constraint->tooLowMessage) + ->setCode(Week::TOO_LOW_ERROR) + ->setInvalidValue($value) + ->setParameter('{{ min }}', $constraint->min) + ->addViolation(); + + return; + } + } + + if ($constraint->max) { + [$maxYear, $maxWeekNumber] = \explode('-W', $constraint->max, 2); + if ($year > $maxYear || ($year === $maxYear && $weekNumber > $maxWeekNumber)) { + $this->context->buildViolation($constraint->tooHighMessage) + ->setCode(Week::TOO_HIGH_ERROR) + ->setInvalidValue($value) + ->setParameter('{{ max }}', $constraint->max) + ->addViolation(); + } + } + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/WeekTest.php b/src/Symfony/Component/Validator/Tests/Constraints/WeekTest.php new file mode 100644 index 0000000000000..0fc9aac627178 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/WeekTest.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Constraints\Week; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; + +class WeekTest extends TestCase +{ + public function testWithoutArgument() + { + $week = new Week(); + + $this->assertNull($week->min); + $this->assertNull($week->max); + } + + public function testConstructor() + { + $week = new Week(min: '2010-W01', max: '2010-W02'); + + $this->assertSame('2010-W01', $week->min); + $this->assertSame('2010-W02', $week->max); + } + + public function testMinYearIsAfterMaxYear() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Week" constraint requires the min week to be less than or equal to the max week.'); + + new Week(min: '2011-W01', max: '2010-W02'); + } + + public function testMinWeekIsAfterMaxWeek() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Week" constraint requires the min week to be less than or equal to the max week.'); + + new Week(min: '2010-W02', max: '2010-W01'); + } + + public function testMinAndMaxWeeksAreTheSame() + { + $week = new Week(min: '2010-W01', max: '2010-W01'); + + $this->assertSame('2010-W01', $week->min); + $this->assertSame('2010-W01', $week->max); + } + + public function testMinIsBadlyFormatted() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Week" constraint requires the min week to be in the ISO 8601 format if set.'); + + new Week(min: '2010-01'); + } + + public function testMaxIsBadlyFormatted() + { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Week" constraint requires the max week to be in the ISO 8601 format if set.'); + + new Week(max: '2010-01'); + } + + public function testAttributes() + { + $metadata = new ClassMetadata(WeekDummy::class); + $loader = new AttributeLoader(); + $this->assertTrue($loader->loadClassMetadata($metadata)); + + [$aConstraint] = $metadata->properties['a']->getConstraints(); + $this->assertNull($aConstraint->min); + $this->assertNull($aConstraint->max); + + [$bConstraint] = $metadata->properties['b']->getConstraints(); + $this->assertSame('2010-W01', $bConstraint->min); + $this->assertSame('2010-W02', $bConstraint->max); + } +} + +class WeekDummy +{ + #[Week] + private string $a; + + #[Week(min: '2010-W01', max: '2010-W02')] + private string $b; +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/WeekValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/WeekValidatorTest.php new file mode 100644 index 0000000000000..08bc3b29b7aa9 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/WeekValidatorTest.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use Symfony\Component\Validator\Constraints\Week; +use Symfony\Component\Validator\Constraints\WeekValidator; +use Symfony\Component\Validator\Exception\UnexpectedValueException; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +use Symfony\Component\Validator\Tests\Constraints\Fixtures\StringableValue; + +class WeekValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): WeekValidator + { + return new WeekValidator(); + } + + /** + * @dataProvider provideWeekNumber + */ + public function testWeekIsValidWeekNumber(string|\Stringable $value, bool $expectedViolation) + { + $constraint = new Week(); + $this->validator->validate($value, $constraint); + + if ($expectedViolation) { + $this->buildViolation('The week "{{ value }}" is not a valid week.') + ->setCode(Week::INVALID_WEEK_NUMBER_ERROR) + ->setParameter('{{ value }}', $value) + ->assertRaised(); + + return; + } + + $this->assertNoViolation(); + } + + public static function provideWeekNumber() + { + yield ['2015-W53', false]; // 2015 has 53 weeks + yield ['2020-W53', false]; // 2020 also has 53 weeks + yield ['2024-W53', true]; // 2024 has 52 weeks + yield [new StringableValue('2024-W53'), true]; + } + + public function testBounds() + { + $constraint = new Week(min: '2015-W10', max: '2016-W25'); + + $this->validator->validate('2015-W10', $constraint); + $this->assertNoViolation(); + + $this->validator->validate('2016-W25', $constraint); + $this->assertNoViolation(); + } + + public function testTooLow() + { + $constraint = new Week(min: '2015-W10'); + + $this->validator->validate('2015-W08', $constraint); + $this->buildViolation('The value should not be before week "{{ min }}".') + ->setInvalidValue('2015-W08') + ->setParameter('{{ min }}', '2015-W10') + ->setCode(Week::TOO_LOW_ERROR) + ->assertRaised(); + } + + public function testTooHigh() + { + $constraint = new Week(max: '2016-W25'); + + $this->validator->validate('2016-W30', $constraint); + $this->buildViolation('The value should not be after week "{{ max }}".') + ->setInvalidValue('2016-W30') + ->setParameter('{{ max }}', '2016-W25') + ->setCode(Week::TOO_HIGH_ERROR) + ->assertRaised(); + } + + public function testWithNewLine() + { + $this->validator->validate("2015-W10\n", new Week()); + + $this->buildViolation('This value does not represent a valid week in the ISO 8601 format.') + ->setCode(Week::INVALID_FORMAT_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider provideInvalidValues + */ + public function testInvalidValues(string $value) + { + $this->validator->validate($value, new Week()); + + $this->buildViolation('This value does not represent a valid week in the ISO 8601 format.') + ->setCode(Week::INVALID_FORMAT_ERROR) + ->assertRaised(); + } + + /** + * @dataProvider provideInvalidTypes + */ + public function testNonStringValues(mixed $value) + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessageMatches('/Expected argument of type "string", ".*" given/'); + + $this->validator->validate($value, new Week()); + } + + public static function provideInvalidValues() + { + yield ['1970-01']; + yield ['1970-W00']; + yield ['1970-W54']; + yield ['1970-W100']; + yield ['1970-W01-01']; + yield ['-W01']; + yield ['24-W01']; + } + + public static function provideInvalidTypes() + { + yield [true]; + yield [false]; + yield [1]; + yield [1.1]; + yield [[]]; + yield [new \stdClass()]; + } +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy