Skip to content

[Validator] Add Week constraint #57908

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/Validator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand Down
66 changes: 66 additions & 0 deletions src/Symfony/Component/Validator/Constraints/Week.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?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\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;

/**
* @author Alexandre Daubois <alex.daubois@gmail.com>
*/
#[\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__));
}
}
}
}
82 changes: 82 additions & 0 deletions src/Symfony/Component/Validator/Constraints/WeekValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?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\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;

/**
* @author Alexandre Daubois <alex.daubois@gmail.com>
*/
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();
}
}
}
}
101 changes: 101 additions & 0 deletions src/Symfony/Component/Validator/Tests/Constraints/WeekTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?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\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;
}
Loading