Skip to content

Commit 137c74c

Browse files
committed
Change how we trace the path of results
Currently, we’re using scalar values to trace paths. The problem with that approach is that we can’t create a reliable hierarchy with them, as we can’t know for sure when a path is the same for different rules. By using an object, we can easily compare and create a parent-child relationship with it. While making these changes, I deemed it necessary to also create objects to handle Name and Id, which makes the code simpler and more robust. By having Name and Path, we can create specific stringifiers that allow us to customise how we render those values. I didn’t manage to make those changes atomically, which is why this commit makes so many changes. I found myself moving back and forth, and making all those changes at once was the best solution I found.
1 parent c0b8baa commit 137c74c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+440
-256
lines changed

library/Message/Formatter/FirstResultStringFormatter.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
final readonly class FirstResultStringFormatter implements StringFormatter
1818
{
19-
use PathProcessor;
20-
2119
public function __construct(
2220
private Renderer $renderer,
2321
private TemplateResolver $templateResolver,
@@ -31,7 +29,7 @@ public function format(Result $result, array $templates, Translator $translator)
3129
if (!$this->templateResolver->hasMatch($result, $matchedTemplates)) {
3230
foreach ($result->children as $child) {
3331
return $this->format(
34-
$this->overwritePath($result, $child),
32+
$child,
3533
$matchedTemplates,
3634
$translator,
3735
);

library/Message/Formatter/NestedArrayFormatter.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919

2020
final readonly class NestedArrayFormatter implements ArrayFormatter
2121
{
22-
use PathProcessor;
23-
2422
public function __construct(
2523
private Renderer $renderer,
2624
private TemplateResolver $templateResolver,
@@ -37,18 +35,18 @@ public function format(Result $result, array $templates, Translator $translator)
3735
$matchedTemplates = $this->templateResolver->selectMatches($result, $templates);
3836
if (count($result->children) === 0 || $this->templateResolver->hasMatch($result, $matchedTemplates)) {
3937
return [
40-
$result->getDeepestPath() ?? $result->id => $this->renderer->render(
41-
$this->templateResolver->resolve($result->withDeepestPath(), $matchedTemplates),
38+
$result->path->value ?? $result->id->value => $this->renderer->render(
39+
$this->templateResolver->resolve($result->withoutParentPath(), $matchedTemplates),
4240
$translator,
4341
),
4442
];
4543
}
4644

4745
$messages = [];
4846
foreach ($result->children as $child) {
49-
$key = $child->getDeepestPath() ?? $child->id ?? 0;
47+
$key = $child->path->value ?? $child->id->value;
5048
$messages[$key] = $this->format(
51-
$this->overwritePath($result, $child),
49+
$child->withoutParentPath()->withoutName(),
5250
$this->templateResolver->selectMatches($child, $matchedTemplates),
5351
$translator,
5452
);
@@ -62,7 +60,7 @@ public function format(Result $result, array $templates, Translator $translator)
6260
if (count($messages) > 1) {
6361
$self = [
6462
'__root__' => $this->renderer->render(
65-
$this->templateResolver->resolve($result->withDeepestPath(), $matchedTemplates),
63+
$this->templateResolver->resolve($result->withoutParentPath(), $matchedTemplates),
6664
$translator,
6765
),
6866
];

library/Message/Formatter/NestedListStringFormatter.php

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
use Respect\Validation\Result;
1616

1717
use function array_filter;
18-
use function array_map;
1918
use function array_reduce;
2019
use function count;
2120
use function rtrim;
@@ -26,8 +25,6 @@
2625

2726
final readonly class NestedListStringFormatter implements StringFormatter
2827
{
29-
use PathProcessor;
30-
3128
public function __construct(
3229
private Renderer $renderer,
3330
private TemplateResolver $templateResolver,
@@ -51,14 +48,16 @@ private function formatRecursively(
5148
$matchedTemplates = $this->templateResolver->selectMatches($result, $templates);
5249

5350
$formatted = '';
51+
$displayedName = null;
5452
if ($this->isVisible($result, ...$siblings)) {
5553
$indentation = str_repeat(' ', $depth * 2);
54+
$displayedName = $result->name;
5655
$formatted .= sprintf(
5756
'%s- %s' . PHP_EOL,
5857
$indentation,
5958
$this->renderer->render(
6059
$this->templateResolver->resolve(
61-
$depth > 0 ? $result->withDeepestPath() : $result,
60+
$result->withoutParentPath(),
6261
$matchedTemplates,
6362
),
6463
$translator,
@@ -68,17 +67,13 @@ private function formatRecursively(
6867
}
6968

7069
if (!$this->templateResolver->hasMatch($result, $matchedTemplates)) {
71-
$children = array_map(
72-
fn(Result $child) => $this->overwritePath($result, $child),
73-
$result->children,
74-
);
75-
foreach ($children as $child) {
70+
foreach ($result->children as $child) {
7671
$formatted .= $this->formatRecursively(
77-
$child,
72+
$displayedName === $child->name ? $child->withoutName() : $child,
7873
$matchedTemplates,
7974
$translator,
8075
$depth,
81-
...array_filter($children, static fn(Result $sibling) => $sibling !== $child),
76+
...array_filter($result->children, static fn(Result $sibling) => $sibling !== $child),
8277
);
8378
$formatted .= PHP_EOL;
8479
}

library/Message/Formatter/PathProcessor.php

Lines changed: 0 additions & 28 deletions
This file was deleted.

library/Message/Formatter/TemplateResolver.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ public function selectMatches(Result $result, array $templates): array
8080
/** @return non-empty-array<string|int> */
8181
private function getKeys(Result $result): array
8282
{
83-
return array_filter([$result->path, $result->name, $result->id], static fn($key) => $key !== null);
83+
return array_filter(
84+
[$result->path?->value, $result->name?->value, $result->id->value],
85+
static fn($key) => $key !== null,
86+
);
8487
}
8588
}

library/Message/InterpolationRenderer.php

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
use ReflectionClass;
1313
use Respect\Stringifier\Stringifier;
1414
use Respect\Validation\Message\Placeholder\Listed;
15+
use Respect\Validation\Message\Placeholder\Name;
1516
use Respect\Validation\Message\Placeholder\Quoted;
1617
use Respect\Validation\Result;
1718
use Respect\Validation\Rule;
1819

20+
use function array_key_exists;
1921
use function is_array;
2022
use function is_bool;
2123
use function is_scalar;
@@ -35,12 +37,8 @@ public function __construct(
3537

3638
public function render(Result $result, Translator $translator): string
3739
{
38-
$parameters = $result->parameters;
39-
$parameters['path'] = $result->path !== null ? Quoted::fromPath($result->path) : null;
40-
$parameters['input'] = $result->input;
41-
42-
$builtName = $result->name ?? $parameters['path'] ?? $this->placeholder('input', $result->input, $translator);
43-
$parameters['name'] ??= $builtName;
40+
$parameters = ['path' => $result->path, 'input' => $result->input, 'name' => $this->getName($result)];
41+
$parameters += $result->parameters;
4442

4543
$rendered = (string) preg_replace_callback(
4644
'/{{(\w+)(\|([^}]+))?}}/',
@@ -100,10 +98,6 @@ private function placeholder(
10098
return $translator->translate($value);
10199
}
102100

103-
if ($name === 'name' && is_string($value)) {
104-
return $value;
105-
}
106-
107101
return $this->stringifier->stringify($value, 0) ?? print_r($value, true);
108102
}
109103

@@ -127,4 +121,25 @@ private function getTemplateMessage(Result $result): string
127121

128122
return $result->template;
129123
}
124+
125+
private function getName(Result $result): Name
126+
{
127+
if (array_key_exists('name', $result->parameters) && is_string($result->parameters['name'])) {
128+
return new Name($result->parameters['name']);
129+
}
130+
131+
if (array_key_exists('name', $result->parameters)) {
132+
return new Name((string) $this->stringifier->stringify($result->parameters['name'], 0));
133+
}
134+
135+
if ($result->name !== null) {
136+
return $result->path ? $result->name->withPath($result->path) : $result->name;
137+
}
138+
139+
if ($result->path?->value !== null) {
140+
return new Name((string) $this->stringifier->stringify($result->path, 0));
141+
}
142+
143+
return new Name((string) $this->stringifier->stringify($result->input, 0));
144+
}
130145
}

library/Message/Placeholder/Id.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
use Respect\Validation\Rule;
13+
14+
use function lcfirst;
15+
use function strrchr;
16+
use function substr;
17+
use function ucfirst;
18+
19+
final readonly class Id
20+
{
21+
public function __construct(
22+
public string $value,
23+
) {
24+
}
25+
26+
public static function fromRule(Rule $rule): self
27+
{
28+
return new self(lcfirst(substr((string) strrchr($rule::class, '\\'), 1)));
29+
}
30+
31+
public function withPrefix(string $prefix): self
32+
{
33+
return new self($prefix . ucfirst($this->value));
34+
}
35+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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 readonly class Name
13+
{
14+
public function __construct(
15+
public string $value,
16+
public Path|null $path = null,
17+
) {
18+
}
19+
20+
public function withPath(Path $path): Name
21+
{
22+
return new self($this->value, $path);
23+
}
24+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
public int|string $value,
16+
public Path|null $parent = null,
17+
) {
18+
}
19+
20+
public function isOrphan(): bool
21+
{
22+
return $this->parent === null;
23+
}
24+
25+
public function withParent(self $parent): self
26+
{
27+
if ($parent === $this->parent) {
28+
return $this;
29+
}
30+
31+
$this->parent = $parent;
32+
33+
return $this;
34+
}
35+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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\Stringifier;
13+
use Respect\Validation\Message\Placeholder\Name;
14+
15+
use function sprintf;
16+
17+
final readonly class NameStringifier implements Stringifier
18+
{
19+
public function __construct(
20+
private Stringifier $stringifier,
21+
) {
22+
}
23+
24+
public function stringify(mixed $raw, int $depth): string|null
25+
{
26+
if (!$raw instanceof Name) {
27+
return null;
28+
}
29+
30+
if ($raw->path === null || $raw->path->isOrphan()) {
31+
return $raw->value;
32+
}
33+
34+
return sprintf(
35+
'%s (<- %s)',
36+
$this->stringifier->stringify($raw->path, $depth),
37+
$raw->value,
38+
);
39+
}
40+
}

0 commit comments

Comments
 (0)