Skip to content

Commit 4079ab5

Browse files
[Validator] Add Week constraint
1 parent 5938c51 commit 4079ab5

File tree

5 files changed

+392
-0
lines changed

5 files changed

+392
-0
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CHANGELOG
1010
* Add `errorPath` to Unique constraint
1111
* Add the `format` option to the `Ulid` constraint to allow accepting different ULID formats
1212
* Add the `WordCount` constraint
13+
* Add the `Week` constraint
1314

1415
7.1
1516
---
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Attribute\HasNamedArguments;
15+
use Symfony\Component\Validator\Constraint;
16+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
17+
18+
/**
19+
* @author Alexandre Daubois <[email protected]>
20+
*/
21+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
22+
final class Week extends Constraint
23+
{
24+
public const INVALID_FORMAT_ERROR = '19012dd1-01c8-4ce8-959f-72ad22684f5f';
25+
public const INVALID_WEEK_NUMBER_ERROR = 'd67ebfc9-45fe-4e4c-a038-5eaa56895ea3';
26+
public const TOO_LOW_ERROR = '9b506423-77a3-4749-aa34-c822a08be978';
27+
public const TOO_HIGH_ERROR = '85156377-d1e6-42cd-8f6e-dc43c2ecb72b';
28+
29+
protected const ERROR_NAMES = [
30+
self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR',
31+
self::INVALID_WEEK_NUMBER_ERROR => 'INVALID_WEEK_NUMBER_ERROR',
32+
self::TOO_LOW_ERROR => 'TOO_LOW_ERROR',
33+
self::TOO_HIGH_ERROR => 'TOO_HIGH_ERROR',
34+
];
35+
36+
#[HasNamedArguments]
37+
public function __construct(
38+
public ?string $min = null,
39+
public ?string $max = null,
40+
public string $invalidFormatMessage = 'This value does not represent a valid week in the ISO 8601 format.',
41+
public string $invalidWeekNumberMessage = 'The week "{{ value }}" is not a valid week.',
42+
public string $tooLowMessage = 'The value should not be before week "{{ min }}".',
43+
public string $tooHighMessage = 'The value should not be after week "{{ max }}".',
44+
?array $groups = null,
45+
mixed $payload = null,
46+
) {
47+
parent::__construct(null, $groups, $payload);
48+
49+
if (null !== $min && !preg_match('/^\d{4}-W(0[1-9]|[1-4][0-9]|5[0-3])$/', $min)) {
50+
throw new ConstraintDefinitionException(\sprintf('The "%s" constraint requires the min week to be in the ISO 8601 format if set.', __CLASS__));
51+
}
52+
53+
if (null !== $max && !preg_match('/^\d{4}-W(0[1-9]|[1-4][0-9]|5[0-3])$/', $max)) {
54+
throw new ConstraintDefinitionException(\sprintf('The "%s" constraint requires the max week to be in the ISO 8601 format if set.', __CLASS__));
55+
}
56+
57+
if (null !== $min && null !== $max) {
58+
[$minYear, $minWeekNumber] = \explode('-W', $min, 2);
59+
[$maxYear, $maxWeekNumber] = \explode('-W', $max, 2);
60+
61+
if ($minYear > $maxYear || ($minYear === $maxYear && $minWeekNumber > $maxWeekNumber)) {
62+
throw new ConstraintDefinitionException(\sprintf('The "%s" constraint requires the min week to be less than or equal to the max week.', __CLASS__));
63+
}
64+
}
65+
}
66+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\ConstraintValidator;
16+
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
17+
use Symfony\Component\Validator\Exception\UnexpectedValueException;
18+
19+
/**
20+
* @author Alexandre Daubois <[email protected]>
21+
*/
22+
final class WeekValidator extends ConstraintValidator
23+
{
24+
public function validate(mixed $value, Constraint $constraint): void
25+
{
26+
if (!$constraint instanceof Week) {
27+
throw new UnexpectedTypeException($constraint, Week::class);
28+
}
29+
30+
if (null === $value) {
31+
return;
32+
}
33+
34+
if (!\is_string($value) && !$value instanceof \Stringable) {
35+
throw new UnexpectedValueException($value, 'string');
36+
}
37+
38+
if (!preg_match('/^\d{4}-W(0[1-9]|[1-4][0-9]|5[0-3])$/D', $value)) {
39+
$this->context->buildViolation($constraint->invalidFormatMessage)
40+
->setCode(Week::INVALID_FORMAT_ERROR)
41+
->addViolation();
42+
43+
return;
44+
}
45+
46+
[$year, $weekNumber] = \explode('-W', $value, 2);
47+
$weeksInYear = (int) \date('W', \mktime(0, 0, 0, 12, 28, $year));
48+
49+
if ($weekNumber > $weeksInYear) {
50+
$this->context->buildViolation($constraint->invalidWeekNumberMessage)
51+
->setCode(Week::INVALID_WEEK_NUMBER_ERROR)
52+
->setParameter('{{ value }}', $value)
53+
->addViolation();
54+
55+
return;
56+
}
57+
58+
if ($constraint->min) {
59+
[$minYear, $minWeekNumber] = \explode('-W', $constraint->min, 2);
60+
if ($year < $minYear || ($year === $minYear && $weekNumber < $minWeekNumber)) {
61+
$this->context->buildViolation($constraint->tooLowMessage)
62+
->setCode(Week::TOO_LOW_ERROR)
63+
->setInvalidValue($value)
64+
->setParameter('{{ min }}', $constraint->min)
65+
->addViolation();
66+
67+
return;
68+
}
69+
}
70+
71+
if ($constraint->max) {
72+
[$maxYear, $maxWeekNumber] = \explode('-W', $constraint->max, 2);
73+
if ($year > $maxYear || ($year === $maxYear && $weekNumber > $maxWeekNumber)) {
74+
$this->context->buildViolation($constraint->tooHighMessage)
75+
->setCode(Week::TOO_HIGH_ERROR)
76+
->setInvalidValue($value)
77+
->setParameter('{{ max }}', $constraint->max)
78+
->addViolation();
79+
}
80+
}
81+
}
82+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Constraints;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Validator\Constraints\Week;
16+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
17+
use Symfony\Component\Validator\Mapping\ClassMetadata;
18+
use Symfony\Component\Validator\Mapping\Loader\AttributeLoader;
19+
20+
class WeekTest extends TestCase
21+
{
22+
public function testWithoutArgument()
23+
{
24+
$week = new Week();
25+
26+
$this->assertNull($week->min);
27+
$this->assertNull($week->max);
28+
}
29+
30+
public function testConstructor()
31+
{
32+
$week = new Week(min: '2010-W01', max: '2010-W02');
33+
34+
$this->assertSame('2010-W01', $week->min);
35+
$this->assertSame('2010-W02', $week->max);
36+
}
37+
38+
public function testMinYearIsAfterMaxYear()
39+
{
40+
$this->expectException(ConstraintDefinitionException::class);
41+
$this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Week" constraint requires the min week to be less than or equal to the max week.');
42+
43+
new Week(min: '2011-W01', max: '2010-W02');
44+
}
45+
46+
public function testMinWeekIsAfterMaxWeek()
47+
{
48+
$this->expectException(ConstraintDefinitionException::class);
49+
$this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Week" constraint requires the min week to be less than or equal to the max week.');
50+
51+
new Week(min: '2010-W02', max: '2010-W01');
52+
}
53+
54+
public function testMinAndMaxWeeksAreTheSame()
55+
{
56+
$week = new Week(min: '2010-W01', max: '2010-W01');
57+
58+
$this->assertSame('2010-W01', $week->min);
59+
$this->assertSame('2010-W01', $week->max);
60+
}
61+
62+
public function testMinIsBadlyFormatted()
63+
{
64+
$this->expectException(ConstraintDefinitionException::class);
65+
$this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Week" constraint requires the min week to be in the ISO 8601 format if set.');
66+
67+
new Week(min: '2010-01');
68+
}
69+
70+
public function testMaxIsBadlyFormatted()
71+
{
72+
$this->expectException(ConstraintDefinitionException::class);
73+
$this->expectExceptionMessage('The "Symfony\Component\Validator\Constraints\Week" constraint requires the max week to be in the ISO 8601 format if set.');
74+
75+
new Week(max: '2010-01');
76+
}
77+
78+
public function testAttributes()
79+
{
80+
$metadata = new ClassMetadata(WeekDummy::class);
81+
$loader = new AttributeLoader();
82+
$this->assertTrue($loader->loadClassMetadata($metadata));
83+
84+
[$aConstraint] = $metadata->properties['a']->getConstraints();
85+
$this->assertNull($aConstraint->min);
86+
$this->assertNull($aConstraint->max);
87+
88+
[$bConstraint] = $metadata->properties['b']->getConstraints();
89+
$this->assertSame('2010-W01', $bConstraint->min);
90+
$this->assertSame('2010-W02', $bConstraint->max);
91+
}
92+
}
93+
94+
class WeekDummy
95+
{
96+
#[Week]
97+
private string $a;
98+
99+
#[Week(min: '2010-W01', max: '2010-W02')]
100+
private string $b;
101+
}

0 commit comments

Comments
 (0)