Skip to content

Commit 16c2a75

Browse files
committed
Enable validating inputs without throwing exceptions
Currently, the only way to handle when a validation fails is by `assert()`, which either throws an exception or doesn't. That means that every time a user wants to handle the results, they must use try-catch blocks, which may add some overhead. This commit introduces the `ResultQuery` class that wraps a `Result` object, providing an alternative to exception-based validation. This allows users to handle results directly without try-catch blocks. I’m taking a risky move here using the old method `validate()`, but I can’t think of a better name for this method.
1 parent 5b00d69 commit 16c2a75

File tree

9 files changed

+934
-23
lines changed

9 files changed

+934
-23
lines changed

library/Exceptions/ValidationException.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
final class ValidationException extends InvalidArgumentException implements Exception
1919
{
2020
/**
21-
* @param array<string, mixed> $messages
22-
* @param array<string> $ignoredBacktracePaths
21+
* @param array<string|int, mixed> $messages
22+
* @param array<string> $ignoredBacktracePaths
2323
*/
2424
public function __construct(
2525
string $message,
@@ -37,7 +37,7 @@ public function getFullMessage(): string
3737
return $this->fullMessage;
3838
}
3939

40-
/** @return array<string, mixed> */
40+
/** @return array<string|int, mixed> */
4141
public function getMessages(): array
4242
{
4343
return $this->messages;

library/Message/ArrayFormatter.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
interface ArrayFormatter
1515
{
1616
/**
17-
* @param array<string, mixed> $templates
17+
* @param array<string|int, mixed> $templates
1818
*
19-
* @return array<string, mixed>
19+
* @return array<string|int, mixed>
2020
*/
2121
public function format(Result $result, Renderer $renderer, array $templates): array;
2222
}

library/Message/StringFormatter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@
1313

1414
interface StringFormatter
1515
{
16-
/** @param array<string, mixed> $templates */
16+
/** @param array<string|int, mixed> $templates */
1717
public function format(Result $result, Renderer $renderer, array $templates): string;
1818
}

library/ResultQuery.php

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* Copyright (c) Alexandre Gomes Gaigalas <[email protected]>
7+
* SPDX-License-Identifier: MIT
8+
*/
9+
10+
namespace Respect\Validation;
11+
12+
use Respect\Validation\Message\ArrayFormatter;
13+
use Respect\Validation\Message\Renderer;
14+
use Respect\Validation\Message\StringFormatter;
15+
use Stringable;
16+
17+
use function array_shift;
18+
use function explode;
19+
use function implode;
20+
use function is_string;
21+
22+
final readonly class ResultQuery implements Stringable
23+
{
24+
/** @param array<string|int, mixed> $templates */
25+
public function __construct(
26+
private Result $result,
27+
private Renderer $renderer,
28+
private StringFormatter $messageFormatter,
29+
private StringFormatter $fullMessageFormatter,
30+
private ArrayFormatter $messagesFormatter,
31+
private array $templates,
32+
) {
33+
}
34+
35+
public function findById(string $id): self|null
36+
{
37+
if ($this->result->id->value === $id) {
38+
return $this;
39+
}
40+
41+
foreach ($this->result->children as $child) {
42+
$resultQuery = clone ($this, ['result' => $child]);
43+
if ($child->id->value === $id) {
44+
return $resultQuery;
45+
}
46+
47+
return $resultQuery->findById($id);
48+
}
49+
50+
return null;
51+
}
52+
53+
public function findByName(string $name): self|null
54+
{
55+
if ($this->result->name?->value === $name) {
56+
return $this;
57+
}
58+
59+
foreach ($this->result->children as $child) {
60+
$resultQuery = clone ($this, ['result' => $child]);
61+
if ($child->name?->value === $name) {
62+
return $resultQuery;
63+
}
64+
65+
return $resultQuery->findByName($name);
66+
}
67+
68+
return null;
69+
}
70+
71+
public function findByPath(string|int $path): self|null
72+
{
73+
if ($this->result->path?->value === $path) {
74+
return $this;
75+
}
76+
77+
$paths = is_string($path) ? explode('.', $path) : [$path];
78+
$currentPath = array_shift($paths);
79+
80+
foreach ($this->result->children as $child) {
81+
if ($child->path?->value !== $currentPath) {
82+
continue;
83+
}
84+
85+
$resultQuery = clone ($this, ['result' => $child]);
86+
if ($paths === []) {
87+
return $resultQuery;
88+
}
89+
90+
return $resultQuery->findByPath(is_string($path) ? implode('.', $paths) : $path);
91+
}
92+
93+
return null;
94+
}
95+
96+
public function isValid(): bool
97+
{
98+
return $this->result->hasPassed;
99+
}
100+
101+
public function toMessage(): string
102+
{
103+
if ($this->result->hasPassed) {
104+
return '';
105+
}
106+
107+
return $this->messageFormatter->format($this->result, $this->renderer, $this->templates);
108+
}
109+
110+
public function toFullMessage(): string
111+
{
112+
if ($this->result->hasPassed) {
113+
return '';
114+
}
115+
116+
return $this->fullMessageFormatter->format($this->result, $this->renderer, $this->templates);
117+
}
118+
119+
/** @return array<string|int, mixed> */
120+
public function toArrayMessages(): array
121+
{
122+
if ($this->result->hasPassed) {
123+
return [];
124+
}
125+
126+
return $this->messagesFormatter->format($this->result, $this->renderer, $this->templates);
127+
}
128+
129+
public function __toString(): string
130+
{
131+
return $this->toMessage();
132+
}
133+
}

library/Validator.php

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,18 @@ public function evaluate(mixed $input): Result
6060
return $this->getRule()->evaluate($input);
6161
}
6262

63+
/** @param array<string|int, mixed>|string|null $template */
64+
public function validate(mixed $input, array|string|null $template = null): ResultQuery
65+
{
66+
return $this->toResultQuery($this->evaluate($input), $template);
67+
}
68+
6369
public function isValid(mixed $input): bool
6470
{
6571
return $this->evaluate($input)->hasPassed;
6672
}
6773

68-
/** @param array<string, mixed>|callable(ValidationException): Throwable|string|Throwable|null $template */
74+
/** @param array<string|int, mixed>|callable(ValidationException): Throwable|string|Throwable|null $template */
6975
public function assert(mixed $input, array|string|Throwable|callable|null $template = null): void
7076
{
7177
$result = $this->evaluate($input);
@@ -77,29 +83,20 @@ public function assert(mixed $input, array|string|Throwable|callable|null $templ
7783
throw $template;
7884
}
7985

80-
$failedResult = $this->resultFilter->filter($result);
81-
82-
$templates = $this->templates;
83-
if (is_array($template)) {
84-
$templates = $template;
85-
} elseif (is_string($template)) {
86-
$failedResult = $failedResult->withTemplate($template);
87-
} elseif ($this->getTemplate() != null) {
88-
$failedResult = $failedResult->withTemplate($this->getTemplate());
89-
}
86+
$resultQuery = $this->toResultQuery($result, is_callable($template) ? null : $template);
9087

9188
$exception = new ValidationException(
92-
$this->mainMessageFormatter->format($failedResult, $this->renderer, $templates),
93-
$this->fullMessageFormatter->format($failedResult, $this->renderer, $templates),
94-
$this->messagesFormatter->format($failedResult, $this->renderer, $templates),
89+
$resultQuery->toMessage(),
90+
$resultQuery->toFullMessage(),
91+
$resultQuery->toArrayMessages(),
9592
$this->ignoredBacktracePaths,
9693
);
9794

98-
if (!is_callable($template)) {
99-
throw $exception;
95+
if (is_callable($template)) {
96+
throw $template($exception);
10097
}
10198

102-
throw $template($exception);
99+
throw $exception;
103100
}
104101

105102
/** @param array<string|int, mixed> $templates */
@@ -161,6 +158,25 @@ public function setTemplate(string $template): static
161158
return $this;
162159
}
163160

161+
/** @param array<string|int, mixed>|string|null $template */
162+
private function toResultQuery(Result $result, array|string|null $template): ResultQuery
163+
{
164+
if (is_string($template)) {
165+
$result = $result->withTemplate($template);
166+
} elseif ($this->getTemplate() != null) {
167+
$result = $result->withTemplate($this->getTemplate());
168+
}
169+
170+
return new ResultQuery(
171+
$this->resultFilter->filter($result),
172+
$this->renderer,
173+
$this->mainMessageFormatter,
174+
$this->fullMessageFormatter,
175+
$this->messagesFormatter,
176+
is_array($template) ? $template : $this->templates,
177+
);
178+
}
179+
164180
/** @param mixed[] $arguments */
165181
public static function __callStatic(string $ruleName, array $arguments): self
166182
{
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* Copyright (c) Alexandre Gomes Gaigalas <[email protected]>
5+
* SPDX-License-Identifier: MIT
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Respect\Validation\Test\Message;
11+
12+
use Respect\Validation\Message\ArrayFormatter;
13+
use Respect\Validation\Message\Renderer;
14+
use Respect\Validation\Result;
15+
16+
final class TestingArrayFormatter implements ArrayFormatter
17+
{
18+
/**
19+
* @param array<string|int, mixed> $templates
20+
*
21+
* @return array<string|int, mixed>
22+
*/
23+
public function format(Result $result, Renderer $renderer, array $templates): array
24+
{
25+
return [$result->id->value => $renderer->render($result, $templates)];
26+
}
27+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* Copyright (c) Alexandre Gomes Gaigalas <[email protected]>
5+
* SPDX-License-Identifier: MIT
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Respect\Validation\Test\Message;
11+
12+
use Respect\Validation\Message\Renderer;
13+
use Respect\Validation\Message\StringFormatter;
14+
use Respect\Validation\Result;
15+
16+
final class TestingStringFormatter implements StringFormatter
17+
{
18+
public function __construct(
19+
private readonly string $prefix = '',
20+
) {
21+
}
22+
23+
/** @param array<string|int, mixed> $templates */
24+
public function format(Result $result, Renderer $renderer, array $templates): string
25+
{
26+
return $this->prefix . $renderer->render($result, $templates);
27+
}
28+
}

0 commit comments

Comments
 (0)