Skip to content
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

add DateRule (#602) #646

Merged
merged 12 commits into from
Jan 26, 2024
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 2.0.0 under development

- New #646: Add `DateTime` rule (@pamparam83)
- New #615: Add the `Each::PARAMETER_EACH_KEY` validation context parameter that available during `Each` rule handling
and containing the current key (@dood-)
- Enh #648: Raise the minimum version of PHP to 8.1 (@pamparam83)
Expand Down
158 changes: 158 additions & 0 deletions src/Rule/DateTime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Validator\Rule;

use Closure;
use Attribute;
use InvalidArgumentException;
use Yiisoft\Validator\WhenInterface;
use Yiisoft\Validator\DumpedRuleInterface;
use Yiisoft\Validator\SkipOnErrorInterface;
use Yiisoft\Validator\SkipOnEmptyInterface;
use Yiisoft\Validator\Rule\Trait\WhenTrait;
use Yiisoft\Validator\RuleHandlerInterface;
use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait;
use Yiisoft\Validator\Rule\Trait\SkipOnErrorTrait;

/**
* Defines validation options to check that the value is a date.
*
* An example for simple that can be used to validate the date:
* ```php
* use Yiisoft\Validator\Rule\DateTime;
*
* $rules = [
* 'date' => [
* new DateTime(format: 'Y-m-d'),
* ],
* ];
* ```
* In the example above, the PHP attributes equivalent will be:
*
* ```php
* use Yiisoft\Validator\Validator;
* use Yiisoft\Validator\Rule\DateTime;
*
* final class User
* {
* public function __construct(
* #[DateTime(format: 'Y-m-d')]
* public string $date,
* ){
* }
* }
*
* $user = new User( date: '2022-01-01' );
*
* $validator = (new Validator())->validate($user);
*
* ```
*
* @see DateTimeHandler
*
* @psalm-import-type WhenType from WhenInterface
*/
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
final class DateTime implements DumpedRuleInterface, SkipOnErrorInterface, WhenInterface, SkipOnEmptyInterface
{
use SkipOnEmptyTrait;
use SkipOnErrorTrait;
use WhenTrait;

/**
* @link https://www.php.net/manual/en/datetimeimmutable.createfromformat.php
* @psalm-var non-empty-string
* @var string The allowed date formats.
*/
private string $format;

/**
* @param string $format The format of the date. See {@see $format}
* @param string $message A message used when the value is not valid.
* You may use the following placeholders in the message:
* - `{attribute}`: the translated label of the attribute being validated.
* - `{value}`: the value of the attribute being validated.
* @param string $incorrectInputMessage A message used when the input is incorrect.
* You may use the following placeholders in the message:
* - `{attribute}`: the translated label of the attribute being validated.
* - `{type}`: the type of the value being validated.
* @param bool|callable|null $skipOnEmpty Whether to skip this rule if the value validated is empty. See {@see SkipOnEmptyInterface}.
* @param bool $skipOnError Whether to skip this rule if any of the previous rules gave an error. See {@see SkipOnErrorInterface}.
* @param Closure|null $when A callable to define a condition for applying the rule. See {@see WhenInterface}.
* @psalm-param WhenType $when
*/
public function __construct(
string $format = 'Y-m-d',
private string $incorrectInputMessage = 'The {attribute} must be a date.',
private string $message = 'The {attribute} is not a valid date.',
private mixed $skipOnEmpty = null,
private bool $skipOnError = false,
private ?Closure $when = null,
) {
if ($format === '') {
throw new InvalidArgumentException('Format can\'t be empty.');
}

$this->format = $format;
}

/**
* The date format.
*
* @return string The format. See {@see $format}
* @psalm-return non-empty-string
*
* @see $format
*/

public function getFormat(): string
{
return $this->format;
}

/**
* Get a message used when the input is incorrect.
*
* @return string A message used when the input is incorrect.
* @see $incorrectInputMessage
*/
public function getIncorrectInputMessage(): string
{
return $this->incorrectInputMessage;
}

public function getOptions(): array
{
return [
'format' => $this->format,
'incorrectInputMessage' => [
'template' => $this->incorrectInputMessage,
'parameters' => [],
],
'message' => [
'template' => $this->message,
'parameters' => [],
],
'skipOnEmpty' => $this->getSkipOnEmptyOption(),
'skipOnError' => $this->skipOnError,
];
}

public function getMessage(): string
{
return $this->message;
}

public function getName(): string
{
return self::class;
}

public function getHandler(): string|RuleHandlerInterface
{
return DateTimeHandler::class;
}

}
46 changes: 46 additions & 0 deletions src/Rule/DateTimeHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Validator\Rule;

use Yiisoft\Validator\Result;
use Yiisoft\Validator\ValidationContext;
use Yiisoft\Validator\RuleHandlerInterface;
use Yiisoft\Validator\Exception\UnexpectedRuleException;

use function is_string;

/**
* Validates that the value is a valid date.
*/
final class DateTimeHandler implements RuleHandlerInterface
{
public function validate(mixed $value, object $rule, ValidationContext $context): Result
{
if (!$rule instanceof DateTime) {
throw new UnexpectedRuleException(DateTime::class, $rule);
}

$result = new Result();

if ((!is_string($value) && !is_int($value) && !is_float($value)) || empty($value)) {
return $result->addError($rule->getIncorrectInputMessage(), [
'attribute' => $context->getTranslatedAttribute(),
'type' => get_debug_type($value),
]);
}
\DateTime::createFromFormat($rule->getFormat(), (string)$value);

// Before PHP 8.2 may return array instead of false (see https://github.com/php/php-src/issues/9431).
$errors = \DateTime::getLastErrors() ?: [ 'error_count' => 0, 'warning_count' => 0 ];

Check warning on line 36 in src/Rule/DateTimeHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": --- Original +++ New @@ @@ } \DateTime::createFromFormat($rule->getFormat(), (string) $value); // Before PHP 8.2 may return array instead of false (see https://github.com/php/php-src/issues/9431). - $errors = \DateTime::getLastErrors() ?: ['error_count' => 0, 'warning_count' => 0]; + $errors = \DateTime::getLastErrors() ?: ['error_count' => -1, 'warning_count' => 0]; if ($errors['error_count'] !== 0 || $errors['warning_count'] !== 0) { $result->addError($rule->getMessage(), ['attribute' => $context->getTranslatedAttribute(), 'value' => $value]); }

Check warning on line 36 in src/Rule/DateTimeHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": --- Original +++ New @@ @@ } \DateTime::createFromFormat($rule->getFormat(), (string) $value); // Before PHP 8.2 may return array instead of false (see https://github.com/php/php-src/issues/9431). - $errors = \DateTime::getLastErrors() ?: ['error_count' => 0, 'warning_count' => 0]; + $errors = \DateTime::getLastErrors() ?: ['error_count' => 1, 'warning_count' => 0]; if ($errors['error_count'] !== 0 || $errors['warning_count'] !== 0) { $result->addError($rule->getMessage(), ['attribute' => $context->getTranslatedAttribute(), 'value' => $value]); }

Check warning on line 36 in src/Rule/DateTimeHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": --- Original +++ New @@ @@ } \DateTime::createFromFormat($rule->getFormat(), (string) $value); // Before PHP 8.2 may return array instead of false (see https://github.com/php/php-src/issues/9431). - $errors = \DateTime::getLastErrors() ?: ['error_count' => 0, 'warning_count' => 0]; + $errors = \DateTime::getLastErrors() ?: ['error_count' => 0, 'warning_count' => -1]; if ($errors['error_count'] !== 0 || $errors['warning_count'] !== 0) { $result->addError($rule->getMessage(), ['attribute' => $context->getTranslatedAttribute(), 'value' => $value]); }

Check warning on line 36 in src/Rule/DateTimeHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.1-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": --- Original +++ New @@ @@ } \DateTime::createFromFormat($rule->getFormat(), (string) $value); // Before PHP 8.2 may return array instead of false (see https://github.com/php/php-src/issues/9431). - $errors = \DateTime::getLastErrors() ?: ['error_count' => 0, 'warning_count' => 0]; + $errors = \DateTime::getLastErrors() ?: ['error_count' => 0, 'warning_count' => 1]; if ($errors['error_count'] !== 0 || $errors['warning_count'] !== 0) { $result->addError($rule->getMessage(), ['attribute' => $context->getTranslatedAttribute(), 'value' => $value]); }
if($errors['error_count'] !== 0 || $errors['warning_count'] !== 0){
$result->addError($rule->getMessage(), [
'attribute' => $context->getTranslatedAttribute(),
'value' => $value,
]);
}

return $result;
}
}
145 changes: 145 additions & 0 deletions tests/Rule/DateTimeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Validator\Tests\Rule;

use InvalidArgumentException;
use Yiisoft\Validator\Rule\DateTime;
use Yiisoft\Validator\Rule\DateTimeHandler;
use Yiisoft\Validator\Tests\Rule\Base\RuleTestCase;
use Yiisoft\Validator\Tests\Rule\Base\WhenTestTrait;
use Yiisoft\Validator\Tests\Rule\Base\SkipOnErrorTestTrait;
use Yiisoft\Validator\Tests\Rule\Base\RuleWithOptionsTestTrait;
use Yiisoft\Validator\Tests\Rule\Base\DifferentRuleInHandlerTestTrait;

final class DateTimeTest extends RuleTestCase
{
use DifferentRuleInHandlerTestTrait;
use RuleWithOptionsTestTrait;
use SkipOnErrorTestTrait;
use WhenTestTrait;

public function dataInvalidConfiguration(): array
{
return [
[['format' => ''], 'Format can\'t be empty.'],
];
}

/**
* @dataProvider dataInvalidConfiguration
*/
public function testinvalidConfiguration(array $arguments, string $expectedExceptionMessage): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage($expectedExceptionMessage);
new DateTime(...$arguments);
}

public function dataValidationPassed(): array
{
return [
['2020-01-01', [new DateTime(format: 'Y-m-d')]],
['2020-01-01 10:10:10', [new DateTime(format: 'Y-m-d H:i:s')]],
['10.02.2023', [new DateTime(format: 'd.m.Y')]],
['10/02/2023', [new DateTime(format: 'd/m/Y')]],
['April 30, 2023, 5:16 pm', [new DateTime(format: 'F j, Y, g:i a')]],
['', [new DateTime(format: 'd-m-Y', skipOnEmpty: true)]],
['125636000', [new DateTime(format: 'U')]],
[125636000, [new DateTime(format: 'U')]],
[123456.344, [new DateTime(format: 'U.u')]],
];
}

public function dataValidationFailed(): array
{
return [
'incorrect input, is boolean' => [
true,
[new DateTime(incorrectInputMessage: 'Custom incorrect input message.')],
['' => ['Custom incorrect input message.']],
],
[
'2023-02-20ee',
[new DateTime(format: 'Y-m-dee',)],
['' => ['The is not a valid date.']],
],
[
'2024-02-20',
[new DateTime(format: 'H:i',)],
['' => ['The is not a valid date.']],
],
[
'2023-02-30',
[new DateTime(format: 'Y-m-d', message: 'Attribute - {attribute}, value - {value}.')],
['' => ['Attribute - , value - 2023-02-30.']],
],
'custom incorrect input message with parameters, attribute set' => [
['attribute' => []],
['attribute' => [new DateTime(incorrectInputMessage: 'Attribute - {attribute}, type - {type}.')]],
['attribute' => ['Attribute - attribute, type - array.']],
],
'incorrect input, is not date' => [
'datetime',
[new DateTime(message: 'Attribute - {attribute}, value - {value}.')],
['' => ['Attribute - , value - datetime.']],
],
'empty string and custom message' => [
'',
[new DateTime()],
['' => ['The must be a date.']],
],
[
null,
[new DateTime()],
['' => ['The must be a date.']],
],
];
}

public function dataOptions(): array
{
return [
[
new DateTime(),
[
'format' => 'Y-m-d',
'incorrectInputMessage' => [
'template' => 'The {attribute} must be a date.',
'parameters' => [],
],
'message' => [
'template' => 'The {attribute} is not a valid date.',
'parameters' => [],
],
'skipOnEmpty' => false,
'skipOnError' => false,
],
],
];
}

public function testSkipOnError(): void
{
$this->testSkipOnErrorInternal(new DateTime(), new DateTime(skipOnError: true));
}

public function testWhen(): void
{
$when = static fn(mixed $value): bool => $value !== null;
$this->testWhenInternal(new DateTime(), new DateTime(when: $when));
}

protected function getDifferentRuleInHandlerItems(): array
{
return [DateTime::class, DateTimeHandler::class];
}

public function testGetName(): void
{
$rule = new DateTime();
$this->assertSame(DateTime::class, $rule->getName());
}

}
Loading