Skip to content
Draft
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 composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
},
"require": {
"php": ">=8.5",
"lcobucci/clock": "^3.6",
"psr/container": "^2.0",
"respect/config": "^3.0",
"respect/fluent": "^2.0",
Expand Down
114 changes: 113 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,34 @@ use Respect\Validation\ContainerRegistry;

ContainerRegistry::setContainer($yourPsr11Container);
```

## Clock

Validators that need to know what time it is take a [PSR-20](https://www.php-fig.org/psr/psr-20/) clock. The
`respect.validation.clock` definition names the class to use, and by default that is a system clock, which reads the
current time every time it is asked, just as PHP does on its own:

```php
'respect.validation.clock' => SystemClock::class,
```

Naming `FrozenClock` instead gives each validation chain a clock of its own, held still at the moment the validation
starts and taken again on every run, so that every validator of a chain agrees on what "now" is:

```php
use Lcobucci\Clock\FrozenClock;
use Respect\Validation\ContainerRegistry;

ContainerRegistry::setContainer(
ContainerRegistry::createContainer([
'respect.validation.clock' => FrozenClock::class,
])
);
```

A chain takes its clock when it is built, so configure the container before building any validator. To validate
against a moment of your choosing, give the clock to the validator itself:

```php
v::with(new DateTimeDiff('years', v::greaterThan(18), null, null, new FrozenClock($moment)))->assert($birthDate);
```
5 changes: 5 additions & 0 deletions docs/validators/DateTimeDiff.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ v::dateTimeDiff('months', v::between(1, 18))->assert('5 months ago');
// Validation passes successfully
```

A value such as `"7 years ago"` is resolved when the validation runs, a moment after the time it is compared against
was read, so it falls just short of seven years and counts as six. Freezing the [clock](../configuration.md#clock)
makes both the comparison and the value use the very same moment, and such a value then counts as the seven years it
names.

The supported types are:

- `years`
Expand Down
2 changes: 1 addition & 1 deletion src-dev/Commands/LintMixinCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
scanner: $scanner,
methodBuilder: new MethodBuilder(
excludedTypePrefixes: ['Sokil', 'Egulias', 'Ramsey', 'libphonenumber'],
excludedTypeNames: ['Respect\\Parameter\\Resolver'],
excludedTypeNames: ['Respect\\Parameter\\Resolver', 'Psr\\Clock\\ClockInterface'],
),
interfaces: [
new InterfaceConfig(
Expand Down
1 change: 1 addition & 0 deletions src-dev/Markdown/Linters/ValidatorHeaderLinter.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ private function getParameter(ReflectionParameter $reflection): array|null
if (
str_starts_with($type->getName(), 'Sokil')
|| str_starts_with($type->getName(), 'Egulias')
|| str_starts_with($type->getName(), 'Psr\\Clock')
|| $type->getName() === 'finfo'
) {
return null;
Expand Down
5 changes: 5 additions & 0 deletions src/AutowiringLookup.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public function withNamespace(string $namespace): static
return clone ($this, ['lookup' => $this->lookup->withNamespace($namespace)]);
}

public function withResolver(Resolver $parameterResolver): static
{
return clone ($this, ['parameterResolver' => $parameterResolver]);
}

/** @param array<int|string, mixed> $arguments */
public function create(string $name, array $arguments = []): object
{
Expand Down
2 changes: 2 additions & 0 deletions src/ContainerRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Respect\Validation;

use Lcobucci\Clock\SystemClock;
use libphonenumber\PhoneNumberUtil;
use Psr\Container\ContainerInterface;
use Ramsey\Uuid\UuidFactory;
Expand Down Expand Up @@ -78,6 +79,7 @@ public static function createContainer(array $definitions = []): Container
'respect.validation.formatter.full_message' => new Autowire(NestedListStringFormatter::class),
'respect.validation.formatter.messages' => new Autowire(NestedArrayFormatter::class),
'respect.validation.ignored_backtrace_paths' => [__DIR__ . '/ValidatorBuilder.php'],
'respect.validation.clock' => SystemClock::class,
'respect.validation.rule_factory.namespaces' => ['Respect\\Validation\\Validators'],
Resolver::class => static fn(Container $container) => new ContainerResolver($container),
ValidatorFactory::class => static function (Container $container) {
Expand Down
12 changes: 11 additions & 1 deletion src/FluentValidatorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Respect\Fluent\Exceptions\CouldNotCreate;
use Respect\Fluent\Exceptions\CouldNotResolve;
use Respect\Fluent\FluentFactory;
use Respect\Parameter\Resolver;
use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Exceptions\InvalidClassException;

Expand All @@ -27,7 +28,7 @@ public function __construct(
) {
}

/** @param array<int, mixed> $arguments */
/** @param array<int|string, mixed> $arguments */
public function create(string $ruleName, array $arguments = []): Validator
{
try {
Expand All @@ -51,4 +52,13 @@ public function withNamespace(string $rulesNamespace): self
{
return new self($this->factory->withNamespace(trim($rulesNamespace, '\\')));
}

public function withResolver(Resolver $resolver): self
{
if (!$this->factory instanceof AutowiringLookup) {
return $this;
}

return new self($this->factory->withResolver($resolver));
}
}
14 changes: 6 additions & 8 deletions src/Helpers/CanCompareValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
namespace Respect\Validation\Helpers;

use Countable;
use DateTimeImmutable;
use DateTimeInterface;
use Throwable;
use Lcobucci\Clock\SystemClock;
use Psr\Clock\ClockInterface;

use function is_numeric;
use function is_scalar;
Expand All @@ -24,7 +24,9 @@

trait CanCompareValues
{
private function toComparable(mixed $value): mixed
use CanResolveDateTime;

private function toComparable(mixed $value, ClockInterface|null $clock = null): mixed
{
if ($value instanceof Countable) {
return $value->count();
Expand All @@ -38,11 +40,7 @@ private function toComparable(mixed $value): mixed
return $value;
}

try {
return new DateTimeImmutable($value);
} catch (Throwable) {
return $value;
}
return $this->resolveDateTime($value, $clock ?? SystemClock::fromSystemTimezone()) ?? $value;
}

private function isAbleToCompareValues(mixed $left, mixed $right): bool
Expand Down
Loading
Loading