respect-validation/library/Rules/DateTimeDiff.php
Henrique Moody 94ddfcd0bd
Create named constructor to create Result
The constructor of `Result` has many arguments, but that's not the
primary reason why I'm making this change. I want to change the
constructor, and it will become more complicated, so having this named
constructor will be useful in the next refactoring.

With this change, I also made the `id` mandatory. That made the
constructor look neater and most to promote almost all properties to the
constructor.

Another change was removing the `fromAdjacent` method, which was quite
confusing. I created the `asAdjacentOf` method, which is a bit clearer.
If anything, it makes all static methods named constructors. It will be
a bit more verbose, but more intuitive.
2025-12-26 22:34:43 +01:00

146 lines
4.8 KiB
PHP

<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Rules;
use Attribute;
use DateTimeImmutable;
use DateTimeInterface;
use Respect\Validation\Exceptions\InvalidRuleConstructorException;
use Respect\Validation\Helpers\CanValidateDateTime;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Rule;
use Throwable;
use function in_array;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Template(
'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}} and',
'The number of {{type|trans}} between {{now}} and',
self::TEMPLATE_CUSTOMIZED,
)]
#[Template(
'For comparison with {{now|raw}}, {{subject}} must be a valid datetime',
'For comparison with {{now|raw}}, {{subject}} must not be a valid datetime',
self::TEMPLATE_NOT_A_DATE,
)]
#[Template(
'For comparison with {{now|raw}}, {{subject}} must be a valid datetime in the format {{sample|raw}}',
'For comparison with {{now|raw}}, {{subject}} must not be a valid datetime in the format {{sample|raw}}',
self::TEMPLATE_WRONG_FORMAT,
)]
final readonly class DateTimeDiff implements Rule
{
use CanValidateDateTime;
public const string TEMPLATE_CUSTOMIZED = '__customized__';
public const string TEMPLATE_NOT_A_DATE = '__not_a_date__';
public const string TEMPLATE_WRONG_FORMAT = '__wrong_format__';
/** @param "years"|"months"|"days"|"hours"|"minutes"|"seconds"|"microseconds" $type */
public function __construct(
private string $type,
private Rule $rule,
private string|null $format = null,
private DateTimeImmutable|null $now = null,
) {
$availableTypes = ['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'microseconds'];
if (!in_array($this->type, $availableTypes, true)) {
throw new InvalidRuleConstructorException(
'"%s" is not a valid type of age (Available: %s)',
$this->type,
$availableTypes,
);
}
}
public function evaluate(mixed $input): Result
{
$now = $this->now ?? new DateTimeImmutable();
$compareTo = $this->createDateTimeObject($input);
if ($compareTo === null) {
$template = $this->format === null ? self::TEMPLATE_NOT_A_DATE : self::TEMPLATE_WRONG_FORMAT;
$parameters = ['sample' => $now->format($this->format ?? 'c'), 'now' => $this->nowParameter($now)];
return Result::failed($input, $this, $parameters, $template)
->withId($this->rule->evaluate($input)->id->withPrefix('dateTimeDiff'));
}
$nowPlaceholder = $this->nowParameter($now);
$result = $this->rule->evaluate($this->comparisonValue($now, $compareTo));
return $result->asAdjacentOf(
Result::of(
$result->hasPassed,
$input,
$this,
['type' => $this->type, 'now' => $nowPlaceholder],
$nowPlaceholder === 'now' ? self::TEMPLATE_STANDARD : self::TEMPLATE_CUSTOMIZED,
),
'dateTimeDiff',
);
}
private function comparisonValue(DateTimeInterface $now, DateTimeInterface $compareTo): int|float
{
return match ($this->type) {
'years' => $compareTo->diff($now)->y,
'months' => $compareTo->diff($now)->m,
'days' => $compareTo->diff($now)->d,
'hours' => $compareTo->diff($now)->h,
'minutes' => $compareTo->diff($now)->i,
'seconds' => $compareTo->diff($now)->s,
'microseconds' => $compareTo->diff($now)->f,
};
}
private function nowParameter(DateTimeInterface $now): string
{
if ($this->format === null && $this->now === null) {
return 'now';
}
if ($this->format === null) {
return $now->format('Y-m-d H:i:s.u');
}
return $now->format($this->format);
}
private function createDateTimeObject(mixed $input): DateTimeInterface|null
{
if ($input instanceof DateTimeInterface) {
return $input;
}
if ($this->format === null) {
try {
return new DateTimeImmutable((string) $input);
} catch (Throwable) {
return null;
}
}
$format = $this->getExceptionalFormats()[$this->format] ?? $this->format;
$dateTime = DateTimeImmutable::createFromFormat($format, (string) $input);
if ($dateTime === false) {
return null;
}
return $dateTime;
}
}