diff --git a/library/Message/StandardFormatter.php b/library/Message/StandardFormatter.php index 68111edb9..7ced9c682 100644 --- a/library/Message/StandardFormatter.php +++ b/library/Message/StandardFormatter.php @@ -106,42 +106,42 @@ public function array(Result $result, array $templates, Translator $translator): { $selectedTemplates = $this->selectTemplates($result, $templates); $deduplicatedChildren = $this->extractDeduplicatedChildren($result); - if (count($deduplicatedChildren) === 0 || $this->isFinalTemplate($result, $selectedTemplates)) { - return [ - $result->getDeepestPath() ?? $result->id => $this->renderer->render( - $this->getTemplated($result->withDeepestPath(), $selectedTemplates), + $messages = [ + 'messages' => [ + $result->id => $this->renderer->render( + $this->getTemplated($result, $selectedTemplates)->withWithoutPath(), $translator ), - ]; - } - - $messages = []; + ], + 'details' => [], + 'children' => [], + ]; foreach ($deduplicatedChildren as $child) { + if ($child->path === null) { + $messages['details'][$child->id] = $this->renderer->render( + $this->getTemplated($child, $selectedTemplates)->withWithoutPath(), + $translator + ); + continue; + } $key = $child->getDeepestPath() ?? $child->id; - $messages[$key] = $this->array( + $messages['children'][$key] = $this->array( $this->resultWithPath($result, $child), $this->selectTemplates($child, $selectedTemplates), $translator ); - if (count($messages[$key]) !== 1) { + if (count($messages['children'][$key]) !== 1) { continue; } - $messages[$key] = current($messages[$key]); + $messages['children'][$key] = current($messages['children'][$key]); } - if (count($messages) > 1) { - $self = [ - '__root__' => $this->renderer->render( - $this->getTemplated($result->withDeepestPath(), $selectedTemplates), - $translator - ), - ]; - - return $self + $messages; + if ($result->path !== null) { + return [$result->getDeepestPath() => array_filter($messages)]; } - return $messages; + return array_filter($messages); } public function resultWithPath(Result $parent, Result $child): Result diff --git a/library/Result.php b/library/Result.php index 1a97219d3..4b546d7f8 100644 --- a/library/Result.php +++ b/library/Result.php @@ -132,6 +132,23 @@ public function withDeepestPath(): self ); } + public function withWithoutPath(): self + { + return new self( + $this->isValid, + $this->input, + $this->rule, + $this->parameters, + $this->template, + $this->mode, + $this->name, + $this->id, + $this->adjacent?->withWithoutPath(), + null, + ...$this->children + ); + } + public function getDeepestPath(): ?string { if ($this->path === null) { diff --git a/tests/feature/Issues/Issue1427Test.php b/tests/feature/Issues/Issue1427Test.php new file mode 100644 index 000000000..846ad0859 --- /dev/null +++ b/tests/feature/Issues/Issue1427Test.php @@ -0,0 +1,93 @@ + + * SPDX-License-Identifier: MIT + */ + +declare(strict_types=1); + +test('https://github.com/Respect/Validation/discussions/1427', expectAll( + fn () => v::each( + v::arrayVal() + ->key('groups', v::each(v::intVal())) + ->key('permissions', v::each(v::boolVal())) + ) + ->assert([ + 16 => [ + 'groups' => [1, 'A', 3, 4, 5], + 'permissions' => [ + 'perm1' => true, + 'perm2' => false, + 'perm3' => 'boom!', + ], + ], + 18 => false, + 24 => ['permissions' => false], + ]), + '`.16.groups.1` must be an integer value', + <<<'FULL_MESSAGE' + - Each item in `[16: ["groups": [1, "A", 3, 4, 5], "permissions": ["perm1": true, "perm2": false, "perm3": "boom!"]], 18: false, ... ]` must be valid + - `.16` must pass the rules + - Each item in `.groups` must be valid + - `.1` must be an integer value + - Each item in `.permissions` must be valid + - `.perm3` must be a boolean value + - `.18` must pass all the rules + - `.18` must be an array value + - `.groups` must be present + - `.permissions` must be present + - `.24` must pass the rules + - `.groups` must be present + - `.permissions` must be iterable + FULL_MESSAGE, + [ + 'messages' => ['each' => 'Each item in `[16: ["groups": [1, "A", 3, 4, 5], "permissions": ["perm1": true, "perm2": false, "perm3": "boom!"]], 18: false, ... ]` must be valid'], + 'children' => [ + 16 => [ + 'messages' => ['allOf' => '`["groups": [1, "A", 3, 4, 5], "permissions": ["perm1": true, "perm2": false, "perm3": "boom!"]]` must pass the rules'], + 'children' => [ + 'groups' => [ + 'messages' => ['each' => 'Each item in `[1, "A", 3, 4, 5]` must be valid'], + 'children' => [ + 1 => [ + 'messages' => ['intVal' => '"A" must be an integer value'], + ], + ], + ], + 'permissions' => [ + 'messages' => ['each' => 'Each item in `["perm1": true, "perm2": false, "perm3": "boom!"]` must be valid'], + 'children' => [ + 'perm3' => [ + 'messages' => ['boolVal' => '"boom!" must be a boolean value'], + ], + ], + ], + ], + ], + 18 => [ + 'messages' => ['allOf' => '`false` must pass all the rules'], + 'details' => ['arrayVal' => '`false` must be an array value'], + 'children' => [ + 'groups' => [ + 'messages' => ['keyExists' => '`false` must be present'], + ], + 'permissions' => [ + 'messages' => ['keyExists' => '`false` must be present'], + ], + ], + ], + 24 => [ + 'messages' => ['allOf' => '`["permissions": false]` must pass the rules'], + 'children' => [ + 'groups' => [ + 'messages' => ['keyExists' => '`["permissions": false]` must be present'], + ], + 'permissions' => [ + 'messages' => ['each' => '`false` must be iterable'], + ], + ], + ], + ], + ] +));