diff --git a/docs/rules/DateTimeDiff.md b/docs/rules/DateTimeDiff.md index 035552f3..622100c3 100644 --- a/docs/rules/DateTimeDiff.md +++ b/docs/rules/DateTimeDiff.md @@ -2,6 +2,7 @@ - `DateTimeDiff(string $type, Rule $rule)` - `DateTimeDiff(string $type, Rule $rule, string $format)` +- `DateTimeDiff(string $type, Rule $rule, string $format, DateTimeImmutable $now)` Validates the difference of date/time against a specific rule. @@ -30,20 +31,62 @@ The supported types are: ## Templates -`DateTimeDiff::TEMPLATE_STANDARD` +The first two templates serve as message suffixes: -| Mode | Template | -|------------|--------------------------------------------------------------| -| `default` | The number of {{type|raw}} between {{now|raw}} and | -| `inverted` | The number of {{type|raw}} between {{now|raw}} and | +```php +v::dateTimeDiff('years', v::equals(2))->assert('1 year ago') +// The number of years between now and 1 year ago must be equal to 2 + +v::not(v::dateTimeDiff('years', v::lessThan(8)))->assert('7 year ago') +// The number of years between now and 7 year ago must not be less than 8 +``` + +### `DateTimeDiff::TEMPLATE_STANDARD` + +Used when `$format` and `$now` are not defined. + +| Mode | Template | +|------------|---------------------------------------------------| +| `default` | The number of {{type|trans}} between now and | +| `inverted` | The number of {{type|trans}} between now and | + +### `DateTimeDiff::TEMPLATE_CUSTOMIZED` + +Used when `$format` or `$now` are defined. + +| Mode | Template | +|------------|----------------------------------------------------------------| +| `default` | The number of {{type|trans}} between {{now|raw}} and | +| `inverted` | The number of {{type|trans}} between {{now|raw}} and | + +### `DateTimeDiff::TEMPLATE_WRONG_FORMAT` + +Used when the input cannot be parsed with the given format. + +| Mode | Template | +|------------|---------------------------------------------------------------------------------------------------------------| +| `default` | For comparison with {{now|raw}}, {{name}} must be a valid datetime in the format {{sample|raw}} | +| `inverted` | For comparison with {{now|raw}}, {{name}} must not be a valid datetime in the format {{sample|raw}} | ## Template placeholders | Placeholder | Description | |-------------|------------------------------------------------------------------| | `name` | The validated input or the custom validator name (if specified). | -| `now` | | -| `type` | | +| `now` | The date and time that is considered as now. | +| `sample` | A sample of the datetime. | +| `type` | The type of interval (years, months, etc.). | + +## Caveats + +When using custom templates, the key must be `dateTimeDiff` + name of the rule you passed, for example: + +```php +v::dateTimeDiff('years', v::equals(2))->assert('1 year ago', [ + 'dateTimeDiffEquals' => 'Please enter a date that is 2 years ago' +]); +// Please enter a date that is 2 years ago. +``` ## Categorization diff --git a/library/Helpers/CanExtractRules.php b/library/Helpers/CanExtractRules.php deleted file mode 100644 index 592ad810..00000000 --- a/library/Helpers/CanExtractRules.php +++ /dev/null @@ -1,55 +0,0 @@ - - * SPDX-License-Identifier: MIT - */ - -declare(strict_types=1); - -namespace Respect\Validation\Helpers; - -use Respect\Validation\Rule; -use Respect\Validation\Rules\Core\Composite; -use Respect\Validation\Rules\Not; -use Respect\Validation\Validator; -use Throwable; - -use function count; - -trait CanExtractRules -{ - private function extractSiblingSuitableRule(Rule $rule, Throwable $throwable): Rule - { - $this->assertSingleRule($rule, $throwable); - - if ($rule instanceof Validator) { - return $rule->getRules()[0]; - } - - return $rule; - } - - private function assertSingleRule(Rule $rule, Throwable $throwable): void - { - if ($rule instanceof Not) { - $this->assertSingleRule($rule->getRule(), $throwable); - - return; - } - - if ($rule instanceof Validator) { - if (count($rule->getRules()) !== 1) { - throw $throwable; - } - - $this->assertSingleRule($rule->getRules()[0], $throwable); - - return; - } - - if ($rule instanceof Composite) { - throw $throwable; - } - } -} diff --git a/library/Rules/DateTimeDiff.php b/library/Rules/DateTimeDiff.php index 92ca336b..fe0a0833 100644 --- a/library/Rules/DateTimeDiff.php +++ b/library/Rules/DateTimeDiff.php @@ -12,30 +12,42 @@ namespace Respect\Validation\Rules; use DateTimeImmutable; use DateTimeInterface; use Respect\Validation\Exceptions\InvalidRuleConstructorException; -use Respect\Validation\Helpers\CanExtractRules; use Respect\Validation\Helpers\CanValidateDateTime; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Rule; use Respect\Validation\Rules\Core\Standard; +use function array_map; use function in_array; +use function ucfirst; #[Template( - 'The number of {{type|raw}} between {{now|raw}} and', - 'The number of {{type|raw}} between {{now|raw}} and', + 'The number of {{type|trans}} between now and', + 'The number of {{type|trans}} between now and', + self::TEMPLATE_STANDARD +)] +#[Template( + 'The number of {{type|trans}} between {{now|raw}} and', + 'The number of {{type|trans}} between {{now|raw}} and', + self::TEMPLATE_CUSTOMIZED +)] +#[Template( + 'For comparison with {{now|raw}}, {{name}} must be a valid datetime in the format {{sample|raw}}', + 'For comparison with {{now|raw}}, {{name}} must not be a valid datetime in the format {{sample|raw}}', + self::TEMPLATE_WRONG_FORMAT )] final class DateTimeDiff extends Standard { use CanValidateDateTime; - use CanExtractRules; - private readonly Rule $rule; + public const TEMPLATE_CUSTOMIZED = '__customized__'; + public const TEMPLATE_WRONG_FORMAT = '__wrong_format__'; /** @param "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds" $type */ public function __construct( private readonly string $type, - Rule $rule, + private readonly Rule $rule, private readonly ?string $format = null, private readonly ?DateTimeImmutable $now = null, ) { @@ -47,27 +59,44 @@ final class DateTimeDiff extends Standard $availableTypes ); } - $this->rule = $this->extractSiblingSuitableRule( - $rule, - new InvalidRuleConstructorException('DateTimeDiff must contain exactly one rule') - ); } public function evaluate(mixed $input): Result { + $now = $this->now ?? new DateTimeImmutable(); $compareTo = $this->createDateTimeObject($input); if ($compareTo === null) { - return Result::failed($input, $this); + $parameters = ['sample' => $now->format($this->format ?? 'c'), 'now' => $this->nowParameter($now)]; + + return Result::failed($input, $this, $parameters, self::TEMPLATE_WRONG_FORMAT) + ->withId('dateTimeDiff' . ucfirst($this->rule->evaluate($input)->id)); } - $now = $this->now ?? new DateTimeImmutable(); - $nextSibling = $this->rule - ->evaluate($this->comparisonValue($now, $compareTo)) - ->withNameIfMissing($input instanceof DateTimeInterface ? $input->format('c') : $input); + return $this->enrichResult( + $this->nowParameter($now), + $input, + $this->rule->evaluate($this->comparisonValue($now, $compareTo)) + ); + } - $parameters = ['type' => $this->type, 'now' => $this->nowParameter($now)]; + private function enrichResult(string $now, mixed $input, Result $result): Result + { + $name = $input instanceof DateTimeInterface ? $input->format('c') : $input; - return (new Result($nextSibling->isValid, $input, $this, $parameters))->withNextSibling($nextSibling); + if (!$result->isSiblingCompatible()) { + return $result + ->withNameIfMissing($name) + ->withChildren( + ...array_map(fn(Result $child) => $this->enrichResult($now, $input, $child), $result->children) + ); + } + + $parameters = ['type' => $this->type, 'now' => $now]; + $template = $now === 'now' ? self::TEMPLATE_STANDARD : self::TEMPLATE_CUSTOMIZED; + + return (new Result($result->isValid, $input, $this, $parameters, $template, id: $result->id)) + ->withPrefixedId('dateTimeDiff') + ->withNextSibling($result->withNameIfMissing($name)); } private function comparisonValue(DateTimeInterface $now, DateTimeInterface $compareTo): int|float diff --git a/tests/integration/rules/dateTimeDiff.phpt b/tests/integration/rules/dateTimeDiff.phpt index 9feb80f8..eea2a60f 100644 --- a/tests/integration/rules/dateTimeDiff.phpt +++ b/tests/integration/rules/dateTimeDiff.phpt @@ -13,9 +13,33 @@ run([ 'With $type = "minutes"' => [v::dateTimeDiff('minutes', v::equals(6)), '5 minutes ago'], 'With $type = "microseconds"' => [v::dateTimeDiff('microseconds', v::equals(7)), '6 microseconds ago'], 'With custom $format' => [v::dateTimeDiff('years', v::lessThan(8), 'd/m/Y'), '09/12/1988'], + 'With input in incorrect $format' => [v::dateTimeDiff('years', v::equals(2), 'Y-m-d'), '1 year ago'], 'With custom $now' => [v::dateTimeDiff('years', v::lessThan(9), null, new DateTimeImmutable()), '09/12/1988'], + 'With custom template' => [v::dateTimeDiff('years', v::equals(2)->setTemplate('Custom template')), '1 year ago'], 'Wrapped by "not"' => [v::not(v::dateTimeDiff('years', v::lessThan(8))), '7 year ago'], 'Wrapping "not"' => [v::dateTimeDiff('years', v::not(v::lessThan(9))), '8 year ago'], + 'Wrapped with custom template' => [ + v::dateTimeDiff('years', v::equals(2)->setTemplate('Wrapped with custom template')), + '1 year ago', + ], + 'Wrapper with custom template' => [ + v::dateTimeDiff('years', v::equals(2))->setTemplate('Wrapper with custom template'), + '1 year ago', + ], + 'Not a sibling compatible' => [ + v::dateTimeDiff('years', v::primeNumber()->between(2, 5)), + '1 year ago', + ], + 'Not a sibling compatible with templates' => [ + v::dateTimeDiff('years', v::primeNumber()->between(2, 5)), + '1 year ago', + [ + 'dateTimeDiff' => [ + 'primeNumber' => 'Interval must be a valid prime number', + 'between' => 'Interval must be between 2 and 5', + ], + ], + ], ]); ?> --EXPECTF-- @@ -24,7 +48,7 @@ With $type = "years" The number of years between now and 1 year ago must be equal to 2 - The number of years between now and 1 year ago must be equal to 2 [ - 'dateTimeDiff' => 'The number of years between now and 1 year ago must be equal to 2', + 'dateTimeDiffEquals' => 'The number of years between now and 1 year ago must be equal to 2', ] With $type = "months" @@ -32,7 +56,7 @@ With $type = "months" The number of months between now and 2 months ago must be equal to 3 - The number of months between now and 2 months ago must be equal to 3 [ - 'dateTimeDiff' => 'The number of months between now and 2 months ago must be equal to 3', + 'dateTimeDiffEquals' => 'The number of months between now and 2 months ago must be equal to 3', ] With $type = "days" @@ -40,7 +64,7 @@ With $type = "days" The number of days between now and 3 days ago must be equal to 4 - The number of days between now and 3 days ago must be equal to 4 [ - 'dateTimeDiff' => 'The number of days between now and 3 days ago must be equal to 4', + 'dateTimeDiffEquals' => 'The number of days between now and 3 days ago must be equal to 4', ] With $type = "hours" @@ -48,7 +72,7 @@ With $type = "hours" The number of hours between now and 4 hours ago must be equal to 5 - The number of hours between now and 4 hours ago must be equal to 5 [ - 'dateTimeDiff' => 'The number of hours between now and 4 hours ago must be equal to 5', + 'dateTimeDiffEquals' => 'The number of hours between now and 4 hours ago must be equal to 5', ] With $type = "minutes" @@ -56,7 +80,7 @@ With $type = "minutes" The number of minutes between now and 5 minutes ago must be equal to 6 - The number of minutes between now and 5 minutes ago must be equal to 6 [ - 'dateTimeDiff' => 'The number of minutes between now and 5 minutes ago must be equal to 6', + 'dateTimeDiffEquals' => 'The number of minutes between now and 5 minutes ago must be equal to 6', ] With $type = "microseconds" @@ -64,7 +88,7 @@ With $type = "microseconds" The number of microseconds between now and 6 microseconds ago must be equal to 7 - The number of microseconds between now and 6 microseconds ago must be equal to 7 [ - 'dateTimeDiff' => 'The number of microseconds between now and 6 microseconds ago must be equal to 7', + 'dateTimeDiffEquals' => 'The number of microseconds between now and 6 microseconds ago must be equal to 7', ] With custom $format @@ -72,7 +96,15 @@ With custom $format The number of years between %d/%d/%d and 09/12/1988 must be less than 8 - The number of years between %d/%d/%d and 09/12/1988 must be less than 8 [ - 'dateTimeDiff' => 'The number of years between %d/%d/%d and 09/12/1988 must be less than 8', + 'dateTimeDiffLessThan' => 'The number of years between %d/%d/%d and 09/12/1988 must be less than 8', +] + +With input in incorrect $format +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +For comparison with %d-%d-%d, "1 year ago" must be a valid datetime in the format 2024-12-09 +- For comparison with %d-%d-%d, "1 year ago" must be a valid datetime in the format 2024-12-09 +[ + 'dateTimeDiffEquals' => 'For comparison with %d-%d-%d, "1 year ago" must be a valid datetime in the format 2024-12-09', ] With custom $now @@ -80,7 +112,15 @@ With custom $now The number of years between %d-%d-%d %d:%d:%d.%d and 09/12/1988 must be less than 9 - The number of years between %d-%d-%d %d:%d:%d.%d and 09/12/1988 must be less than 9 [ - 'dateTimeDiff' => 'The number of years between %d-%d-%d %d:%d:%d.%d and 09/12/1988 must be less than 9', + 'dateTimeDiffLessThan' => 'The number of years between %d-%d-%d %d:%d:%d.%d and 09/12/1988 must be less than 9', +] + +With custom template +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +Custom template +- Custom template +[ + 'equals' => 'Custom template', ] Wrapped by "not" @@ -88,7 +128,7 @@ Wrapped by "not" The number of years between now and 7 year ago must not be less than 8 - The number of years between now and 7 year ago must not be less than 8 [ - 'notDateTimeDiff' => 'The number of years between now and 7 year ago must not be less than 8', + 'notDateTimeDiffLessThan' => 'The number of years between now and 7 year ago must not be less than 8', ] Wrapping "not" @@ -96,5 +136,45 @@ Wrapping "not" The number of years between now and 8 year ago must not be less than 9 - The number of years between now and 8 year ago must not be less than 9 [ - 'dateTimeDiff' => 'The number of years between now and 8 year ago must not be less than 9', + 'dateTimeDiffNotLessThan' => 'The number of years between now and 8 year ago must not be less than 9', +] + +Wrapped with custom template +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +Wrapped with custom template +- Wrapped with custom template +[ + 'equals' => 'Wrapped with custom template', +] + +Wrapper with custom template +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +Wrapper with custom template +- Wrapper with custom template +[ + 'dateTimeDiffEquals' => 'Wrapper with custom template', +] + +Not a sibling compatible +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +The number of years between now and 1 year ago must be a prime number +- All of the required rules must pass for 1 year ago + - The number of years between now and 1 year ago must be a prime number + - The number of years between now and 1 year ago must be between 2 and 5 +[ + '__root__' => 'All of the required rules must pass for 1 year ago', + 'dateTimeDiffPrimeNumber' => 'The number of years between now and 1 year ago must be a prime number', + 'dateTimeDiffBetween' => 'The number of years between now and 1 year ago must be between 2 and 5', +] + +Not a sibling compatible with templates +⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺ +The number of years between now and 1 year ago must be a prime number +- All of the required rules must pass for 1 year ago + - The number of years between now and 1 year ago must be a prime number + - The number of years between now and 1 year ago must be between 2 and 5 +[ + '__root__' => 'All of the required rules must pass for 1 year ago', + 'dateTimeDiffPrimeNumber' => 'The number of years between now and 1 year ago must be a prime number', + 'dateTimeDiffBetween' => 'The number of years between now and 1 year ago must be between 2 and 5', ] diff --git a/tests/integration/translator.phpt b/tests/integration/translator.phpt index a31d7b1f..e7506840 100644 --- a/tests/integration/translator.phpt +++ b/tests/integration/translator.phpt @@ -15,12 +15,17 @@ ValidatorDefaults::setTranslator(new ArrayTranslator([ '{{name}} must be a valid telephone number for country {{countryName|trans}}' => '{{name}} deve ser um número de telefone válido para o país {{countryName|trans}}', 'United States' => 'Estados Unidos', + 'years' => 'anos', + 'The number of {{type|trans}} between now and' => 'O número de {{type|trans}} entre agora e', + '{{name}} must be equal to {{compareTo}}' => '{{name}} deve ser igual a {{compareTo}}', ])); exceptionFullMessage(static fn() => Validator::stringType()->lengthBetween(2, 15)->phone('US')->assert(0)); +exceptionMessage(static fn() => v::dateTimeDiff('years', v::equals(2))->assert('1972-02-09')); ?> --EXPECT-- - Todas as regras requeridas devem passar para 0 - 0 must be a string - O comprimento de 0 deve possuir de 2 a 15 caracteres - - 0 deve ser um número de telefone válido para o país Estados Unidos \ No newline at end of file + - 0 deve ser um número de telefone válido para o país Estados Unidos +O número de anos entre agora e 1972-02-09 deve ser igual a 2 diff --git a/tests/unit/Rules/DateTimeDiffTest.php b/tests/unit/Rules/DateTimeDiffTest.php index a8a29256..c920cd28 100644 --- a/tests/unit/Rules/DateTimeDiffTest.php +++ b/tests/unit/Rules/DateTimeDiffTest.php @@ -11,14 +11,11 @@ namespace Respect\Validation\Rules; use DateTimeImmutable; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use Respect\Validation\Exceptions\InvalidRuleConstructorException; -use Respect\Validation\Rule; use Respect\Validation\Test\Rules\Stub; use Respect\Validation\Test\RuleTestCase; -use Respect\Validation\Validator; use function array_map; use function iterator_to_array; @@ -37,46 +34,6 @@ final class DateTimeDiffTest extends RuleTestCase new DateTimeDiff('invalid', Stub::daze()); } - #[Test] - #[DataProvider('providerForSiblingSuitableRules')] - public function isShouldAcceptRulesThatCanBeAddedAsNextSibling(Rule $rule): void - { - $this->expectNotToPerformAssertions(); - - new DateTimeDiff('years', $rule); - } - - #[Test] - #[DataProvider('providerForSiblingUnsuitableRules')] - public function isShouldNotAcceptRulesThatCanBeAddedAsNextSibling(Rule $rule): void - { - $this->expectException(InvalidRuleConstructorException::class); - $this->expectExceptionMessage('DateTimeDiff must contain exactly one rule'); - - new DateTimeDiff('years', $rule); - } - - /** @return array */ - public static function providerForSiblingSuitableRules(): array - { - return [ - 'single' => [Stub::daze()], - 'single in validator' => [Validator::create(Stub::daze())], - 'single wrapped by "Not"' => [new Not(Stub::daze())], - 'validator wrapping not, wrapping single' => [Validator::create(new Not(Stub::daze()))], - 'not wrapping validator, wrapping single' => [new Not(Validator::create(Stub::daze()))], - ]; - } - - /** @return array */ - public static function providerForSiblingUnsuitableRules(): array - { - return [ - 'double wrapped by validator' => [Validator::create(Stub::daze(), Stub::daze())], - 'double wrapped by validator, wrapped by "Not"' => [new Not(Validator::create(Stub::daze(), Stub::daze()))], - ]; - } - /** @return array */ public static function providerForValidInput(): array {