Skip to content

Commit 22cc869

Browse files
committed
Trace paths instead of IDs
1 parent 0e8ac18 commit 22cc869

File tree

13 files changed

+240
-69
lines changed

13 files changed

+240
-69
lines changed

library/FailedResultIterator.php

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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 Countable;
13+
use Iterator;
14+
use RecursiveIterator;
15+
16+
use function array_filter;
17+
use function array_key_exists;
18+
use function array_map;
19+
use function array_values;
20+
use function count;
21+
use function current;
22+
use function key;
23+
use function next;
24+
use function reset;
25+
26+
/**
27+
* @implements Iterator<int, Result>
28+
* @implements RecursiveIterator<int, Result>
29+
*/
30+
final class FailedResultIterator implements Iterator, Countable, RecursiveIterator
31+
{
32+
/**
33+
* @var array<int, Result>
34+
*/
35+
private array $children;
36+
37+
public function __construct(
38+
private readonly Result $result,
39+
) {
40+
$this->children = $this->extractDeduplicatedChildren();
41+
}
42+
43+
public function current(): Result|false
44+
{
45+
return current($this->children);
46+
}
47+
48+
/**
49+
* @return array<int, Result>
50+
*/
51+
public function getArrayCopy(): array
52+
{
53+
return $this->children;
54+
}
55+
56+
public function next(): void
57+
{
58+
next($this->children);
59+
}
60+
61+
public function key(): ?int
62+
{
63+
return key($this->children);
64+
}
65+
66+
public function valid(): bool
67+
{
68+
return key($this->children) !== null;
69+
}
70+
71+
public function rewind(): void
72+
{
73+
reset($this->children);
74+
}
75+
76+
public function count(): int
77+
{
78+
return count($this->children);
79+
}
80+
81+
public function hasChildren(): bool
82+
{
83+
return $this->result->children !== [];
84+
}
85+
86+
/**
87+
* @return null|RecursiveIterator<int, Result>
88+
*/
89+
public function getChildren(): ?RecursiveIterator
90+
{
91+
if (!$this->hasChildren()) {
92+
return null;
93+
}
94+
95+
return new self($this->result);
96+
}
97+
98+
/**
99+
* @return array<int, Result>
100+
*/
101+
private function extractDeduplicatedChildren(): array
102+
{
103+
/** @var array<string, Result> $deduplicatedResults */
104+
$deduplicatedResults = [];
105+
$duplicateCounters = [];
106+
foreach ($this->result->children as $child) {
107+
if ($child->path !== null) {
108+
$deduplicatedResults[$child->path] = $child->hasPassed ? null : $child;
109+
continue;
110+
}
111+
112+
$id = $child->id;
113+
if (isset($duplicateCounters[$id])) {
114+
$id .= '.' . ++$duplicateCounters[$id];
115+
} elseif (array_key_exists($id, $deduplicatedResults)) {
116+
$deduplicatedResults[$id . '.1'] = $deduplicatedResults[$id]?->withId($id . '.1');
117+
unset($deduplicatedResults[$id]);
118+
$duplicateCounters[$id] = 2;
119+
$id .= '.2';
120+
}
121+
122+
$deduplicatedResults[$id] = $child->hasPassed ? null : $child->withId($id);
123+
}
124+
125+
return array_map(
126+
function (Result $child): Result {
127+
if ($this->result->path !== null && $child->path !== null && $child->path !== $this->result->path) {
128+
return $child->withPath($this->result->path);
129+
}
130+
131+
if ($this->result->path !== null && $child->path === null) {
132+
return $child->withPath($this->result->path);
133+
}
134+
135+
return $child;
136+
},
137+
array_values(array_filter($deduplicatedResults))
138+
);
139+
}
140+
}

library/Message/Placeholder/Path.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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\Message\Placeholder;
11+
12+
final class Path
13+
{
14+
public function __construct(
15+
private readonly int|string $value
16+
) {
17+
}
18+
19+
public function getValue(): int|string
20+
{
21+
return $this->value;
22+
}
23+
}

library/Message/StandardFormatter.php

Lines changed: 6 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,11 @@
1010
namespace Respect\Validation\Message;
1111

1212
use Respect\Validation\Exceptions\ComponentException;
13+
use Respect\Validation\FailedResultIterator;
1314
use Respect\Validation\Result;
1415

1516
use function array_filter;
16-
use function array_key_exists;
17-
use function array_map;
1817
use function array_reduce;
19-
use function array_values;
2018
use function count;
2119
use function current;
2220
use function is_array;
@@ -42,8 +40,8 @@ public function main(Result $result, array $templates, Translator $translator):
4240
{
4341
$selectedTemplates = $this->selectTemplates($result, $templates);
4442
if (!$this->isFinalTemplate($result, $selectedTemplates)) {
45-
foreach ($this->extractDeduplicatedChildren($result) as $child) {
46-
return $this->main($this->resultWithPath($result, $child), $selectedTemplates, $translator);
43+
foreach (new FailedResultIterator($result) as $child) {
44+
return $this->main($child, $selectedTemplates, $translator);
4745
}
4846
}
4947

@@ -78,17 +76,14 @@ public function full(
7876
}
7977

8078
if (!$isFinalTemplate) {
81-
$results = array_map(
82-
fn(Result $child) => $this->resultWithPath($result, $child),
83-
$this->extractDeduplicatedChildren($result)
84-
);
79+
$results = new FailedResultIterator($result);
8580
foreach ($results as $child) {
8681
$rendered .= $this->full(
8782
$child,
8883
$selectedTemplates,
8984
$translator,
9085
$depth,
91-
...array_filter($results, static fn (Result $sibling) => $sibling !== $child)
86+
...array_filter($results->getArrayCopy(), static fn (Result $sibling) => $sibling !== $child)
9287
);
9388
$rendered .= PHP_EOL;
9489
}
@@ -105,7 +100,7 @@ public function full(
105100
public function array(Result $result, array $templates, Translator $translator): array
106101
{
107102
$selectedTemplates = $this->selectTemplates($result, $templates);
108-
$deduplicatedChildren = $this->extractDeduplicatedChildren($result);
103+
$deduplicatedChildren = new FailedResultIterator($result);
109104
if (count($deduplicatedChildren) === 0 || $this->isFinalTemplate($result, $selectedTemplates)) {
110105
return [
111106
$result->getDeepestPath() ?? $result->id => $this->renderer->render(
@@ -256,32 +251,4 @@ private function selectTemplates(Result $result, array $templates): array
256251

257252
return $templates;
258253
}
259-
260-
/** @return array<Result> */
261-
private function extractDeduplicatedChildren(Result $result): array
262-
{
263-
/** @var array<string, Result> $deduplicatedResults */
264-
$deduplicatedResults = [];
265-
$duplicateCounters = [];
266-
foreach ($result->children as $child) {
267-
$id = $child->getDeepestPath() ?? $child->id;
268-
if (isset($duplicateCounters[$id])) {
269-
$id .= '.' . ++$duplicateCounters[$id];
270-
} elseif (array_key_exists($id, $deduplicatedResults)) {
271-
$deduplicatedResults[$id . '.1'] = $deduplicatedResults[$id]?->withId($id . '.1');
272-
unset($deduplicatedResults[$id]);
273-
$duplicateCounters[$id] = 2;
274-
$id .= '.2';
275-
}
276-
277-
if ($child->path === null) {
278-
$deduplicatedResults[$id] = $child->hasPassed ? null : $child->withId($id);
279-
continue;
280-
}
281-
282-
$deduplicatedResults[$id] = $child->hasPassed ? null : $child;
283-
}
284-
285-
return array_values(array_filter($deduplicatedResults));
286-
}
287254
}

library/Message/StandardRenderer.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use ReflectionClass;
1313
use Respect\Stringifier\Stringifier;
1414
use Respect\Validation\Message\Placeholder\Listed;
15+
use Respect\Validation\Message\Placeholder\Path;
1516
use Respect\Validation\Message\Placeholder\Quoted;
1617
use Respect\Validation\Result;
1718
use Respect\Validation\Rule;
@@ -36,7 +37,7 @@ public function __construct(
3637
public function render(Result $result, Translator $translator, ?string $template = null): string
3738
{
3839
$parameters = $result->parameters;
39-
$parameters['path'] = $result->path !== null ? Quoted::fromPath($result->path) : null;
40+
$parameters['path'] = $result->path !== null ? new Path($result->path) : null;
4041
$parameters['input'] = $result->input;
4142

4243
$builtName = $result->name ?? $parameters['path'] ?? $this->placeholder('input', $result->input, $translator);

library/Message/StandardStringifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use Respect\Stringifier\Stringifiers\StringableObjectStringifier;
3333
use Respect\Stringifier\Stringifiers\ThrowableObjectStringifier;
3434
use Respect\Validation\Message\Stringifier\ListedStringifier;
35+
use Respect\Validation\Message\Stringifier\PathStringifier;
3536
use Respect\Validation\Message\Stringifier\QuotedStringifier;
3637

3738
final class StandardStringifier implements Stringifier
@@ -88,6 +89,7 @@ private function createStringifier(Quoter $quoter): Stringifier
8889
$stringifier->prependStringifier(new ThrowableObjectStringifier($jsonEncodableStringifier, $quoter));
8990
$stringifier->prependStringifier(new DateTimeStringifier($quoter, DateTimeInterface::ATOM));
9091
$stringifier->prependStringifier(new IteratorObjectStringifier($stringifier, $quoter));
92+
$stringifier->prependStringifier(new PathStringifier($quoter));
9193
$stringifier->prependStringifier(new QuotedStringifier($quoter));
9294
$stringifier->prependStringifier(new ListedStringifier($stringifier));
9395

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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\Message\Stringifier;
11+
12+
use Respect\Stringifier\Quoter;
13+
use Respect\Stringifier\Stringifier;
14+
use Respect\Validation\Message\Placeholder\Path;
15+
16+
final class PathStringifier implements Stringifier
17+
{
18+
public function __construct(
19+
private readonly Quoter $quoter
20+
) {
21+
}
22+
23+
public function stringify(mixed $raw, int $depth): ?string
24+
{
25+
if (!$raw instanceof Path) {
26+
return null;
27+
}
28+
29+
return $this->quoter->quote('.' . $raw->getValue(), $depth);
30+
}
31+
}

library/Rules/Cnh.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use function is_scalar;
1717
use function mb_strlen;
18+
use function preg_match;
1819
use function preg_replace;
1920

2021
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
@@ -34,7 +35,11 @@ public function isValid(mixed $input): bool
3435
$input = (string) preg_replace('{\D}', '', (string) $input);
3536

3637
// Validate length and invalid numbers
37-
if (mb_strlen($input) != 11 || ((int) $input === 0)) {
38+
if (mb_strlen($input) != 11) {
39+
return false;
40+
}
41+
42+
if (preg_match('/^(\d)\1{10}/', $input) > 0) {
3843
return false;
3944
}
4045

library/Transformers/Deprecated/KeyValueRule.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
namespace Respect\Validation\Transformers\Deprecated;
1111

12-
use Respect\Validation\Message\Placeholder\Quoted;
12+
use Respect\Validation\Message\Placeholder\Path;
1313
use Respect\Validation\Rules\AlwaysInvalid;
1414
use Respect\Validation\Rules\Key;
1515
use Respect\Validation\Rules\KeyExists;
@@ -56,7 +56,7 @@ static function ($input) use ($comparedKey, $ruleName, $baseKey) {
5656
return new Templated(
5757
new AlwaysInvalid(),
5858
'{{baseKey}} must be valid to validate {{comparedKey}}',
59-
['comparedKey' => Quoted::fromPath($comparedKey), 'baseKey' => Quoted::fromPath($baseKey)]
59+
['comparedKey' => new Path($comparedKey), 'baseKey' => new Path($baseKey)],
6060
);
6161
}
6262
}

tests/feature/Issues/Issue1289Test.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,7 @@
5656
[
5757
0 => [
5858
'__root__' => '`.0` must pass the rules',
59-
'default' => [
60-
'__root__' => '`.default` must pass one of the rules',
61-
'stringType' => '`.default` must be a string',
62-
'boolType' => '`.default` must be a boolean',
63-
],
59+
'default' => '`.default` must be a boolean',
6460
'description' => '`.description` must be a string value',
6561
],
6662
],

tests/feature/Issues/Issue1376Test.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,7 @@
2828
'__root__' => '`stdClass { +$author="foo" }` must pass all the rules',
2929
'title' => '`.title` must be present',
3030
'description' => '`.description` must be present',
31-
'author' => [
32-
'__root__' => '`.author` must pass all the rules',
33-
'intType' => '`.author` must be an integer',
34-
'lengthBetween' => 'The length of `.author` must be between 1 and 2',
35-
],
31+
'author' => 'The length of `.author` must be between 1 and 2',
3632
'user' => '`.user` must be present',
3733
],
3834
));

tests/feature/Rules/AttributesTest.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,7 @@
5252
[
5353
'__root__' => '`Respect\Validation\Test\Stubs\WithAttributes { +$name="" +$birthdate="not a date" +$email="not an email" +$phone ... }` must pass the rules',
5454
'name' => '`.name` must not be empty',
55-
'birthdate' => [
56-
'__root__' => '`.birthdate` must pass all the rules',
57-
'date' => '`.birthdate` must be a valid date in the format "2005-12-30"',
58-
'dateTimeDiffLessThanOrEqual' => 'For comparison with now, `.birthdate` must be a valid datetime',
59-
],
55+
'birthdate' => 'For comparison with now, `.birthdate` must be a valid datetime',
6056
'email' => '`.email` must be a valid email address or must be null',
6157
'phone' => '`.phone` must be a valid telephone number or must be null',
6258
],

0 commit comments

Comments
 (0)