respect-validation/tests/unit/Message/Formatter/FirstResultStringFormatterTest.php
Henrique Moody 82e0722443
Split the Formatter into different formatters
I've noticed that the `StandardFormatter` was quite bloated, which made
it difficult to maintain. Understanding what each method was doing was
quite complicated. Besides, the name "Standard" doesn't mean anything,
because it doesn't say what the implementation does.

I split the `Formatter` into two different interfaces: `StringFormatter`
and `ArrayFormatter`, and I moved some code around:

*  `StandardFormatter::main()` -> `FirstResultStringFormatter`
* `StandardFormatter::full()` -> `NestedListStringFormatter`
* `StandardFormatter::array()` -> `NestedArrayFormatter`

That opens up new ways of handling error messages, potentially
introducing features like `JsonStringFormatter` or `FlatArrayFormatter`
in the future.

While working on this, I removed a significant amount of unnecessary
code, which also improved my overall understanding of those formatters.

I'm not very happy with all the methods in `ValidatorDefaults`, but I
will refactor that later.
2025-12-19 16:20:28 +01:00

116 lines
4.5 KiB
PHP

<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Message\Formatter;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Message\StandardFormatter\ResultCreator;
use Respect\Validation\Message\Translator\DummyTranslator;
use Respect\Validation\Result;
use Respect\Validation\Rule;
use Respect\Validation\Test\Builders\ResultBuilder;
use Respect\Validation\Test\Message\TestingMessageRenderer;
use Respect\Validation\Test\TestCase;
use stdClass;
use function Respect\Stringifier\stringify;
use function sprintf;
#[CoversClass(FirstResultStringFormatter::class)]
final class FirstResultStringFormatterTest extends TestCase
{
use ResultCreator;
/** @param array<string, mixed> $templates */
#[Test]
#[DataProvider('provideForMain')]
public function itShouldFormatMainMessage(Result $result, string $expected, array $templates = []): void
{
$formatter = new FirstResultStringFormatter(
renderer: new TestingMessageRenderer(),
templateResolver: new TemplateResolver(),
);
self::assertSame($expected, $formatter->format($result, $templates, new DummyTranslator()));
}
#[Test]
public function itShouldThrowAnExceptionWhenTryingToFormatAndTemplateIsInvalid(): void
{
$formatter = new FirstResultStringFormatter(
renderer: new TestingMessageRenderer(),
templateResolver: new TemplateResolver(),
);
$result = (new ResultBuilder())->id('foo')->build();
$template = new stdClass();
$this->expectException(ComponentException::class);
$this->expectExceptionMessage(sprintf('Template for "foo" must be a string, %s given', stringify($template)));
$formatter->format($result, ['foo' => $template], new DummyTranslator());
}
/** @return array<string, array{0: Result, 1: string, 2?: array<string, mixed>}> */
public static function provideForMain(): array
{
return [
'without children, without templates' => [
(new ResultBuilder())->build(),
Rule::TEMPLATE_STANDARD,
],
'without children, with templates' => [
(new ResultBuilder())->build(),
'This is a new template',
[(new ResultBuilder())->build()->id => 'This is a new template'],
],
'with children, without templates' => [
(new ResultBuilder())
->id('parent')->id('parent')->template('__parent_original__')
->children(
(new ResultBuilder())->id('1st')->template('__1st_original__')->build(),
(new ResultBuilder())->id('1st')->template('__2nd_original__')->build(),
)
->build(),
'__1st_original__',
],
'with children, with templates' => [
(new ResultBuilder())->id('parent')->template('__parent_original__')
->children(
(new ResultBuilder())->id('1st')->template('__1st_original__')->build(),
(new ResultBuilder())->id('1st')->template('__2nd_original__')->build(),
)
->build(),
'1st custom',
[
'__root__' => 'Parent custom',
'1st' => '1st custom',
'2nd' => '2nd custom',
],
],
'with nested children, without templates' => [
(new ResultBuilder())->id('parent')->template('__parent_original__')
->children(
(new ResultBuilder())->id('1st')->template('__1st_original__')
->children(
(new ResultBuilder())->id('1st_1st')->template('__1st_1st_original__')->build(),
(new ResultBuilder())->id('1st_2nd')->template('__1st_2nd_original__')->build(),
)
->build(),
(new ResultBuilder())->id('1st')->template('__2nd_original__')->build(),
)
->build(),
'__1st_1st_original__',
],
];
}
}