Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify the StandardFormatter #1525

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
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
5 changes: 5 additions & 0 deletions library/Message/Placeholder/Quoted.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ public function __construct(
) {
}

public static function fromPath(int|string $path): self
{
return new self('.' . $path);
}

public function getValue(): string
{
return $this->value;
Expand Down
139 changes: 52 additions & 87 deletions library/Message/StandardFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,15 @@

namespace Respect\Validation\Message;

use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Result;
use Respect\Validation\ResultSet;

use function array_filter;
use function array_key_exists;
use function array_reduce;
use function array_values;
use function count;
use function current;
use function is_array;
use function is_string;
use function Respect\Stringifier\stringify;
use function rtrim;
use function sprintf;
use function str_repeat;
Expand All @@ -35,22 +32,22 @@ public function __construct(
}

/**
* @param array<string, mixed> $templates
* @param array<string|int, mixed> $templates
*/
public function main(Result $result, array $templates, Translator $translator): string
{
$selectedTemplates = $this->selectTemplates($result, $templates);
if (!$this->isFinalTemplate($result, $selectedTemplates)) {
foreach ($this->extractDeduplicatedChildren($result) as $child) {
return $this->main($child, $selectedTemplates, $translator);
$selector = new TemplateSelector($result, $templates);
if (!$selector->hasOnlyItsOwnTemplate()) {
foreach (new ResultSet($result) as $child) {
return $this->main($child, $selector->templates, $translator);
}
}

return $this->renderer->render($this->getTemplated($result, $selectedTemplates), $translator);
return $this->renderer->render($selector->getResult(), $translator);
}

/**
* @param array<string, mixed> $templates
* @param array<string|int, mixed> $templates
*/
public function full(
Result $result,
Expand All @@ -59,29 +56,30 @@ public function full(
int $depth = 0,
Result ...$siblings
): string {
$selectedTemplates = $this->selectTemplates($result, $templates);
$isFinalTemplate = $this->isFinalTemplate($result, $selectedTemplates);

$selector = new TemplateSelector($result, $templates);
$rendered = '';
if ($this->isAlwaysVisible($result, ...$siblings) || $isFinalTemplate) {
if ($this->isAlwaysVisible($result, ...$siblings) || $selector->hasOnlyItsOwnTemplate()) {
$indentation = str_repeat(' ', $depth * 2);
$rendered .= sprintf(
'%s- %s' . PHP_EOL,
$indentation,
$this->renderer->render($this->getTemplated($result, $selectedTemplates), $translator),
$this->renderer->render(
$depth > 0 ? $selector->getResult()->withDeepestPath() : $selector->getResult(),
$translator
),
);
$depth++;
}

if (!$isFinalTemplate) {
$results = $this->extractDeduplicatedChildren($result);
if (!$selector->hasOnlyItsOwnTemplate()) {
$results = new ResultSet($result);
foreach ($results as $child) {
$rendered .= $this->full(
$child,
$selectedTemplates,
$selector->templates,
$translator,
$depth,
...array_filter($results, static fn (Result $sibling) => $sibling !== $child)
...array_filter($results->getArrayCopy(), static fn (Result $sibling) => $sibling !== $child)
);
$rendered .= PHP_EOL;
}
Expand All @@ -91,37 +89,41 @@ public function full(
}

/**
* @param array<string, mixed> $templates
* @param array<string|int, mixed> $templates
*
* @return array<string, mixed>
* @return array<string|int, mixed>
*/
public function array(Result $result, array $templates, Translator $translator): array
{
$selectedTemplates = $this->selectTemplates($result, $templates);
$selector = new TemplateSelector($result, $templates);
$deduplicatedChildren = $this->extractDeduplicatedChildren($result);
if (count($deduplicatedChildren) === 0 || $this->isFinalTemplate($result, $selectedTemplates)) {
if (count($deduplicatedChildren) === 0 || $selector->hasOnlyItsOwnTemplate()) {
return [
$result->id => $this->renderer->render($this->getTemplated($result, $selectedTemplates), $translator),
$result->path ?? $result->id => $this->renderer->render(
$selector->getResult()->withDeepestPath(),
$translator
),
];
}

$messages = [];
foreach ($deduplicatedChildren as $child) {
$messages[$child->id] = $this->array(
$child,
$this->selectTemplates($child, $selectedTemplates),
$key = $child->path ?? $child->id;
$messages[$key] = $this->array(
$this->resultWithPath($result, $child),
$selector->templates,
$translator
);
if (count($messages[$child->id]) !== 1) {
if (count($messages[$key]) !== 1) {
continue;
}

$messages[$child->id] = current($messages[$child->id]);
$messages[$key] = current($messages[$key]);
}

if (count($messages) > 1) {
$self = [
'__root__' => $this->renderer->render($this->getTemplated($result, $selectedTemplates), $translator),
'__root__' => $this->renderer->render($selector->getResult()->withDeepestPath(), $translator),
];

return $self + $messages;
Expand All @@ -130,6 +132,19 @@ public function array(Result $result, array $templates, Translator $translator):
return $messages;
}

public function resultWithPath(Result $parent, Result $child): Result
{
if ($parent->path !== null && $child->path !== null && $child->path !== $parent->path) {
return $child->withPath($parent->path);
}

if ($parent->path !== null && $child->path === null) {
return $child->withPath($parent->path);
}

return $child;
}

private function isAlwaysVisible(Result $result, Result ...$siblings): bool
{
if ($result->isValid) {
Expand Down Expand Up @@ -165,68 +180,18 @@ private function isAlwaysVisible(Result $result, Result ...$siblings): bool
);
}

/** @param array<string, mixed> $templates */
private function getTemplated(Result $result, array $templates): Result
{
if ($result->hasCustomTemplate()) {
return $result;
}

if (!isset($templates[$result->id]) && isset($templates['__root__'])) {
return $result->withTemplate($templates['__root__']);
}

if (!isset($templates[$result->id])) {
return $result;
}

$template = $templates[$result->id];
if (is_string($template)) {
return $result->withTemplate($template);
}

throw new ComponentException(
sprintf('Template for "%s" must be a string, %s given', $result->id, stringify($template))
);
}

/**
* @param array<string, mixed> $templates
*/
private function isFinalTemplate(Result $result, array $templates): bool
{
if (isset($templates[$result->id]) && is_string($templates[$result->id])) {
return true;
}

if (count($templates) !== 1) {
return false;
}

return isset($templates['__root__']) || isset($templates[$result->id]);
}

/**
* @param array<string, mixed> $templates
*
* @return array<string, mixed>
*/
private function selectTemplates(Result $message, array $templates): array
{
if (isset($templates[$message->id]) && is_array($templates[$message->id])) {
return $templates[$message->id];
}

return $templates;
}

/** @return array<Result> */
private function extractDeduplicatedChildren(Result $result): array
{
/** @var array<string, Result> $deduplicatedResults */
$deduplicatedResults = [];
$duplicateCounters = [];
foreach ($result->children as $child) {
if ($child->path !== null) {
$deduplicatedResults[$child->path] = $child->isValid ? null : $child;
continue;
}

$id = $child->id;
if (isset($duplicateCounters[$id])) {
$id .= '.' . ++$duplicateCounters[$id];
Expand All @@ -236,7 +201,7 @@ private function extractDeduplicatedChildren(Result $result): array
$duplicateCounters[$id] = 2;
$id .= '.2';
}
$deduplicatedResults[$id] = $child->isValid ? null : $child->withId($id);
$deduplicatedResults[$id] = $child->isValid ? null : $child->withId((string) $id);
}

return array_values(array_filter($deduplicatedResults));
Expand Down
5 changes: 4 additions & 1 deletion library/Message/StandardRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@ public function __construct(
public function render(Result $result, Translator $translator, ?string $template = null): string
{
$parameters = $result->parameters;
$parameters['name'] ??= $result->name ?? $this->placeholder('input', $result->input, $translator);
$parameters['path'] = $result->path !== null ? Quoted::fromPath($result->path) : null;
$parameters['input'] = $result->input;

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

$rendered = (string) preg_replace_callback(
'/{{(\w+)(\|([^}]+))?}}/',
function (array $matches) use ($parameters, $translator) {
Expand Down
93 changes: 93 additions & 0 deletions library/Message/TemplateSelector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

/*
* Copyright (c) Alexandre Gomes Gaigalas <[email protected]>
* SPDX-License-Identifier: MIT
*/

declare(strict_types=1);

namespace Respect\Validation\Message;

use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Result;

use function count;
use function is_array;
use function is_string;
use function Respect\Stringifier\stringify;
use function sprintf;

final class TemplateSelector
{
/** @var array<string|int, mixed> */
public readonly array $templates;

/** @param array<string|int, mixed> $templates */
public function __construct(
private readonly Result $result,
array $templates,
) {
$this->templates = $this->selectedTemplates($templates);
}

public function getResult(): Result
{
if ($this->result->hasCustomTemplate()) {
return $this->result;
}

foreach ([$this->result->getDeepestPath(), $this->result->name, $this->result->id, '__root__'] as $key) {
if (!isset($this->templates[$key])) {
continue;
}

if (is_string($this->templates[$key])) {
return $this->result->withTemplate($this->templates[$key]);
}

throw new ComponentException(
sprintf('Template for "%s" must be a string, %s given', $key, stringify($this->templates[$key]))
);

Check warning on line 51 in library/Message/TemplateSelector.php

View check run for this annotation

Codecov / codecov/patch

library/Message/TemplateSelector.php#L49-L51

Added lines #L49 - L51 were not covered by tests
}

return $this->result;
}

public function hasOnlyItsOwnTemplate(): bool
{
$keys = [$this->result->getDeepestPath(), $this->result->name, $this->result->id];
foreach ($keys as $key) {
if (isset($this->templates[$key]) && is_string($this->templates[$key])) {
return true;
}
}

if (count($this->templates) !== 1) {
return false;
}

foreach ($keys as $key) {
if (isset($this->templates[$key])) {
return true;

Check warning on line 72 in library/Message/TemplateSelector.php

View check run for this annotation

Codecov / codecov/patch

library/Message/TemplateSelector.php#L72

Added line #L72 was not covered by tests
}
}

return isset($this->templates['__root__']);
}

/**
* @param array<string|int, mixed> $templates
* @return array<string|int, mixed>
*/
private function selectedTemplates(array $templates): array
{
foreach ([$this->result->getDeepestPath(), $this->result->name, $this->result->id] as $key) {
if (isset($templates[$key]) && is_array($templates[$key])) {
return $templates[$key];
}
}

return $templates;
}
}
Loading
Loading