Extract parameter processor into multiple classes

By extracting them into different processors, the code in the rendered
becomes much more straightforward, and the design makes it possible to
create processors easily. This change will allow users to customize that
behavior if they want to.

Signed-off-by: Henrique Moody <henriquemoody@gmail.com>
This commit is contained in:
Henrique Moody 2024-02-22 01:42:28 +01:00
parent 955a554c5e
commit d25557cd72
No known key found for this signature in database
GPG key ID: 221E9281655813A6
17 changed files with 409 additions and 147 deletions

View file

@ -16,8 +16,10 @@ use Respect\Validation\Attributes\ExceptionClass;
use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Exceptions\InvalidClassException;
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\Message\ParameterStringifier;
use Respect\Validation\Message\Stringifier\KeepOriginalStringName;
use Respect\Validation\Message\Parameter\Processor;
use Respect\Validation\Message\Parameter\Raw;
use Respect\Validation\Message\Parameter\Stringify;
use Respect\Validation\Message\Parameter\Trans;
use Respect\Validation\Message\TemplateCollector;
use Respect\Validation\Message\TemplateRenderer;
@ -37,9 +39,9 @@ final class Factory
/**
* @var callable
*/
private $translator = 'strval';
private $translator;
private ParameterStringifier $parameterStringifier;
private Processor $processor;
private TemplateCollector $templateCollector;
@ -47,7 +49,8 @@ final class Factory
public function __construct()
{
$this->parameterStringifier = new KeepOriginalStringName();
$this->translator = static fn (string $message) => $message;
$this->processor = new Raw(new Trans($this->translator, new Stringify()));
$this->templateCollector = new TemplateCollector();
}
@ -72,14 +75,15 @@ final class Factory
{
$clone = clone $this;
$clone->translator = $translator;
$clone->processor = new Raw(new Trans($translator, new Stringify()));
return $clone;
}
public function withParameterStringifier(ParameterStringifier $parameterStringifier): self
public function withParameterProcessor(Processor $processor): self
{
$clone = clone $this;
$clone->parameterStringifier = $parameterStringifier;
$clone->processor = $processor;
return $clone;
}
@ -121,7 +125,7 @@ final class Factory
}
$template = $validatable->getTemplate($input);
$templates = $this->templateCollector->extract($validatable);
$formatter = new TemplateRenderer($this->translator, $this->parameterStringifier);
$formatter = new TemplateRenderer($this->translator, $this->processor);
$attributes = $reflection->getAttributes(ExceptionClass::class);
if (count($attributes) === 0) {

View file

@ -0,0 +1,15 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Message\Parameter;
interface Processor
{
public function process(string $name, mixed $value, ?string $modifier = null): string;
}

View file

@ -0,0 +1,30 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Message\Parameter;
use function is_bool;
use function is_scalar;
final class Raw implements Processor
{
public function __construct(
private readonly Processor $nextProcessor,
) {
}
public function process(string $name, mixed $value, ?string $modifier = null): string
{
if ($modifier === 'raw' && is_scalar($value)) {
return is_bool($value) ? (string) (int) $value : (string) $value;
}
return $this->nextProcessor->process($name, $value, $modifier);
}
}

View file

@ -7,16 +7,14 @@
declare(strict_types=1);
namespace Respect\Validation\Message\Stringifier;
use Respect\Validation\Message\ParameterStringifier;
namespace Respect\Validation\Message\Parameter;
use function is_string;
use function Respect\Stringifier\stringify;
final class KeepOriginalStringName implements ParameterStringifier
final class Stringify implements Processor
{
public function stringify(string $name, mixed $value): string
public function process(string $name, mixed $value, ?string $modifier = null): string
{
if ($name === 'name' && is_string($value)) {
return $value;

View file

@ -0,0 +1,35 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Message\Parameter;
use function call_user_func;
use function is_string;
final class Trans implements Processor
{
/** @var callable */
private $translator;
public function __construct(
callable $translator,
private readonly Processor $nextProcessor,
) {
$this->translator = $translator;
}
public function process(string $name, mixed $value, ?string $modifier = null): string
{
if ($modifier === 'trans' && is_string($value)) {
return call_user_func($this->translator, $value);
}
return $this->nextProcessor->process($name, $value, $modifier);
}
}

View file

@ -1,15 +0,0 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Message;
interface ParameterStringifier
{
public function stringify(string $name, mixed $value): string;
}

View file

@ -10,12 +10,11 @@ declare(strict_types=1);
namespace Respect\Validation\Message;
use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Message\Parameter\Processor;
use Throwable;
use function call_user_func;
use function is_scalar;
use function preg_replace_callback;
use function Respect\Stringifier\stringify;
use function sprintf;
final class TemplateRenderer
@ -25,7 +24,7 @@ final class TemplateRenderer
public function __construct(
callable $translator,
private readonly ParameterStringifier $parameterStringifier
private readonly Processor $processor
) {
$this->translator = $translator;
}
@ -35,25 +34,16 @@ final class TemplateRenderer
*/
public function render(string $template, mixed $input, array $parameters): string
{
$parameters['name'] ??= $this->parameterStringifier->stringify('input', $input);
$parameters['name'] ??= $this->processor->process('input', $input);
return (string) preg_replace_callback(
'/{{(\w+)(\|(trans|raw))?}}/',
function (array $matches) use ($parameters): string {
'/{{(\w+)(\|([^}]+))?}}/',
function (array $matches) use ($parameters) {
if (!isset($parameters[$matches[1]])) {
return $matches[0];
}
$modifier = $matches[3] ?? null;
if ($modifier === 'raw' && is_scalar($parameters[$matches[1]])) {
return (string) $parameters[$matches[1]];
}
if ($modifier === 'trans') {
return $this->translate($parameters[$matches[1]]);
}
return $this->parameterStringifier->stringify($matches[1], $parameters[$matches[1]]);
return $this->processor->process($matches[1], $parameters[$matches[1]], $matches[3] ?? null);
},
$this->translate($template)
);
@ -61,10 +51,6 @@ final class TemplateRenderer
private function translate(mixed $message): string
{
if (!is_scalar($message)) {
throw new ComponentException(sprintf('Cannot translate scalar value "%s"', stringify($message)));
}
try {
return call_user_func($this->translator, (string) $message);
} catch (Throwable $throwable) {

View file

@ -0,0 +1,27 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Test\Message\Parameter;
use Respect\Validation\Message\Parameter\Processor;
use function json_encode;
use function sprintf;
final class TestingProcessor implements Processor
{
public function process(string $name, mixed $value, ?string $modifier = null): string
{
if ($modifier !== null) {
return sprintf('%s(<%s:%s>)', $modifier, $name, $modifier);
}
return sprintf('<%s:%s>', $name, json_encode($value));
}
}

View file

@ -1,23 +0,0 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Test\Message;
use Respect\Validation\Message\ParameterStringifier;
use function json_encode;
use function sprintf;
final class TestingParameterStringifier implements ParameterStringifier
{
public function stringify(string $name, mixed $value): string
{
return sprintf('<%s:%s>', $name, json_encode($value));
}
}

View file

@ -9,7 +9,7 @@ declare(strict_types=1);
namespace Respect\Validation\Test\Stubs;
use Respect\Validation\Message\Stringifier\KeepOriginalStringName;
use Respect\Validation\Message\Parameter\Stringify;
use Respect\Validation\Message\TemplateRenderer;
use Respect\Validation\Rules\AbstractComposite;
use Respect\Validation\Test\Exceptions\CompositeStubException;
@ -33,7 +33,7 @@ final class CompositeSub extends AbstractComposite
params: $extraParameters,
template: Validatable::TEMPLATE_STANDARD,
templates: [],
formatter: new TemplateRenderer(static fn ($value) => $value, new KeepOriginalStringName())
formatter: new TemplateRenderer(static fn ($value) => $value, new Stringify())
);
}
}

View file

@ -10,7 +10,100 @@ declare(strict_types=1);
namespace Respect\Validation\Test;
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
use stdClass;
use function array_merge;
use function tmpfile;
use const PHP_INT_MAX;
use const PHP_INT_MIN;
abstract class TestCase extends PHPUnitTestCase
{
/** @return array<array{mixed}> */
public static function providerForAnyValues(): array
{
return array_merge(
self::providerForStringValues(),
self::providerForNonScalarValues(),
self::providerForIntegerValues(),
self::providerForBooleanValues(),
self::providerForFloatValues(),
);
}
/** @return array<array{scalar}> */
public static function providerForScalarValues(): array
{
return array_merge(
self::providerForStringValues(),
self::providerForIntegerValues(),
self::providerForBooleanValues(),
self::providerForFloatValues(),
);
}
/** @return array<array{mixed}> */
public static function providerForNonScalarValues(): array
{
return [
'closure' => [static fn() => 'foo'],
'array' => [[]],
'object' => [new stdClass()],
'null' => [null],
'resource' => [tmpfile()],
];
}
/** @return array<array{string}> */
public static function providerForStringValues(): array
{
return [
'string' => ['string'],
'empty string' => [''],
'integer string' => ['500'],
'float string' => ['56.8'],
'zero string' => ['0'],
];
}
/** @return array<array{mixed}> */
public static function providerForNonStringValues(): array
{
return array_merge(
self::providerForNonScalarValues(),
self::providerForIntegerValues(),
self::providerForBooleanValues(),
self::providerForFloatValues(),
);
}
/** @return array<array{int}> */
public static function providerForIntegerValues(): array
{
return [
'zero integer' => [0],
'positive integer' => [PHP_INT_MAX],
'negative integer' => [PHP_INT_MIN],
];
}
/** @return array<array{bool}> */
public static function providerForBooleanValues(): array
{
return [
'true' => [true],
'false' => [false],
];
}
/** @return array<array{float}> */
public static function providerForFloatValues(): array
{
return [
'zero float' => [0.0],
'negative float' => [-893.1],
'positive float' => [32.890],
];
}
}

View file

@ -12,7 +12,7 @@ namespace Respect\Validation\Exceptions;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Message\Stringifier\KeepOriginalStringName;
use Respect\Validation\Message\Parameter\Stringify;
use Respect\Validation\Message\TemplateRenderer;
use Respect\Validation\Test\TestCase;
use Respect\Validation\Validatable;
@ -51,7 +51,7 @@ final class NestedValidationExceptionTest extends TestCase
params: [],
template: Validatable::TEMPLATE_STANDARD,
templates: [],
formatter: new TemplateRenderer('strval', new KeepOriginalStringName())
formatter: new TemplateRenderer('strval', new Stringify())
);
}
@ -63,7 +63,7 @@ final class NestedValidationExceptionTest extends TestCase
params: [],
template: Validatable::TEMPLATE_STANDARD,
templates: [],
formatter: new TemplateRenderer('strval', new KeepOriginalStringName())
formatter: new TemplateRenderer('strval', new Stringify())
);
}
}

View file

@ -12,7 +12,7 @@ namespace Respect\Validation\Exceptions;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Message\Stringifier\KeepOriginalStringName;
use Respect\Validation\Message\Parameter\Stringify;
use Respect\Validation\Message\Template;
use Respect\Validation\Message\TemplateRenderer;
use Respect\Validation\Test\TestCase;
@ -108,7 +108,7 @@ final class ValidationExceptionTest extends TestCase
$template = ' This is my new template ';
$expected = trim($template);
$sut = $this->createValidationException(formatter: new TemplateRenderer('trim', new KeepOriginalStringName()));
$sut = $this->createValidationException(formatter: new TemplateRenderer('trim', new Stringify()));
$sut->updateTemplate($template);
self::assertEquals($expected, $sut->getMessage());
@ -166,7 +166,7 @@ final class ValidationExceptionTest extends TestCase
array $params = [],
string $template = Validatable::TEMPLATE_STANDARD,
array $templates = [],
TemplateRenderer $formatter = new TemplateRenderer('strval', new KeepOriginalStringName())
TemplateRenderer $formatter = new TemplateRenderer('strval', new Stringify())
): ValidationException {
return new ValidationException($input, $id, $params, $template, $templates, $formatter);
}

View file

@ -0,0 +1,65 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Message\Parameter;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Test\Message\Parameter\TestingProcessor;
use Respect\Validation\Test\TestCase;
#[CoversClass(Raw::class)]
final class RawTest extends TestCase
{
public const DEFAULT_NAME = 'foo';
#[Test]
#[DataProvider('providerForScalarValues')]
public function itShouldReturnRawValueWhenModifierIsRawAndInputIsScalar(mixed $value): void
{
$raw = new Raw(new TestingProcessor());
self::assertEquals($value, $raw->process(self::DEFAULT_NAME, $value, 'raw'));
}
#[Test]
public function itShouldReturnRawZeroWhenModifierIsRawAndInputIsFalse(): void
{
$raw = new Raw(new TestingProcessor());
self::assertSame('0', $raw->process(self::DEFAULT_NAME, false, 'raw'));
}
#[Test]
#[DataProvider('providerForNonScalarValues')]
public function itShouldUseNextProcessorWhenModifierIsRawButInputIsNonScalar(mixed $value): void
{
$next = new TestingProcessor();
$raw = new Raw($next);
self::assertEquals(
$next->process(self::DEFAULT_NAME, $value, 'raw'),
$raw->process(self::DEFAULT_NAME, $value, 'raw')
);
}
#[Test]
#[DataProvider('providerForScalarValues')]
public function itShouldUseNextProcessorWhenModifierInputIsScalarButModifierIsNotRaw(mixed $value): void
{
$next = new TestingProcessor();
$raw = new Raw($next);
self::assertEquals(
$next->process(self::DEFAULT_NAME, $value, 'something'),
$raw->process(self::DEFAULT_NAME, $value, 'something')
);
}
}

View file

@ -0,0 +1,41 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Message\Parameter;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Test\TestCase;
use function Respect\Stringifier\stringify;
#[CoversClass(Stringify::class)]
final class StringifyTest extends TestCase
{
public const DEFAULT_NAME = 'not_name';
#[Test]
#[DataProvider('providerForStringValues')]
public function itShouldNotStringifyValueWhenNameIsNameAndValueIsString(string $value): void
{
$stringify = new Stringify();
self::assertEquals($value, $stringify->process('name', $value));
}
#[Test]
#[DataProvider('providerForAnyValues')]
public function itShouldReturnStringifiedValue(mixed $value): void
{
$stringify = new Stringify();
self::assertEquals(stringify($value), $stringify->process(self::DEFAULT_NAME, $value, 'trans'));
}
}

View file

@ -0,0 +1,61 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Validation\Message\Parameter;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Test\Message\Parameter\TestingProcessor;
use Respect\Validation\Test\TestCase;
#[CoversClass(Trans::class)]
final class TransTest extends TestCase
{
public const DEFAULT_NAME = 'foo';
#[Test]
#[DataProvider('providerForStringValues')]
public function itShouldReturnTranslatedValueWhenModifierIsTransAndInputIsString(string $value): void
{
$translation = 'translated';
$translator = static fn(string $original) => [$value => $translation][$original] ?? 'Failed to translate';
$trans = new Trans($translator, new TestingProcessor());
self::assertEquals($translation, $trans->process(self::DEFAULT_NAME, $value, 'trans'));
}
#[Test]
#[DataProvider('providerForNonStringValues')]
public function itShouldUseNextProcessorWhenModifierIsTransButInputIsNotString(mixed $value): void
{
$next = new TestingProcessor();
$trans = new Trans(static fn($original) => $original, $next);
self::assertEquals(
$next->process(self::DEFAULT_NAME, $value, 'trans'),
$trans->process(self::DEFAULT_NAME, $value, 'trans')
);
}
#[Test]
#[DataProvider('providerForScalarValues')]
public function itShouldUseNextProcessorWhenModifierInputIsStringButModifierIsNotTrans(mixed $value): void
{
$next = new TestingProcessor();
$trans = new Trans(static fn($original) => $original, $next);
self::assertEquals(
$next->process(self::DEFAULT_NAME, $value, 'something'),
$trans->process(self::DEFAULT_NAME, $value, 'something')
);
}
}

View file

@ -13,9 +13,8 @@ use Exception;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Test\Message\TestingParameterStringifier;
use Respect\Validation\Test\Message\Parameter\TestingProcessor;
use Respect\Validation\Test\TestCase;
use stdClass;
use function sprintf;
@ -25,7 +24,7 @@ final class TemplateRendererTest extends TestCase
#[Test]
public function itShouldRenderMessageWithItsTemplate(): void
{
$renderer = new TemplateRenderer(static fn(string $value) => $value, new TestingParameterStringifier());
$renderer = new TemplateRenderer(static fn(string $value) => $value, new TestingProcessor());
$template = 'This is my template';
@ -35,14 +34,14 @@ final class TemplateRendererTest extends TestCase
#[Test]
public function itShouldReplaceParameters(): void
{
$parameterStringifier = new TestingParameterStringifier();
$parameterStringifier = new TestingProcessor();
$renderer = new TemplateRenderer(static fn(string $value) => $value, $parameterStringifier);
$key = 'foo';
$value = 42;
$expected = 'Will replace ' . $parameterStringifier->stringify($key, $value);
$expected = 'Will replace ' . $parameterStringifier->process($key, $value, null);
$actual = $renderer->render('Will replace {{foo}}', 'input', [$key => $value]);
self::assertSame($expected, $actual);
@ -51,16 +50,17 @@ final class TemplateRendererTest extends TestCase
#[Test]
public function itShouldReplaceNameWithStringifiedInputWhenThereIsNoName(): void
{
$parameterStringifier = new TestingParameterStringifier();
$parameterStringifier = new TestingProcessor();
$renderer = new TemplateRenderer(static fn(string $value) => $value, $parameterStringifier);
$message = 'Will replace {{name}}';
$input = 'input';
$expected = 'Will replace ' . $parameterStringifier->stringify(
$expected = 'Will replace ' . $parameterStringifier->process(
'name',
$parameterStringifier->stringify('input', $input),
$parameterStringifier->process('input', $input, null),
null,
);
$actual = $renderer->render($message, $input, []);
@ -70,13 +70,13 @@ final class TemplateRendererTest extends TestCase
#[Test]
public function itShouldKeepNameWhenDefined(): void
{
$parameterStringifier = new TestingParameterStringifier();
$parameterStringifier = new TestingProcessor();
$renderer = new TemplateRenderer(static fn(string $value) => $value, $parameterStringifier);
$name = 'real name';
$expected = 'Will replace ' . $parameterStringifier->stringify('name', $name);
$expected = 'Will replace ' . $parameterStringifier->process('name', $name, null);
$actual = $renderer->render('Will replace {{name}}', 'input', ['name' => $name]);
self::assertSame($expected, $actual);
@ -85,7 +85,7 @@ final class TemplateRendererTest extends TestCase
#[Test]
public function itShouldKeepUnknownParameters(): void
{
$renderer = new TemplateRenderer(static fn(string $value) => $value, new TestingParameterStringifier());
$renderer = new TemplateRenderer(static fn(string $value) => $value, new TestingProcessor());
$expected = 'Will not replace {{unknown}}';
$actual = $renderer->render($expected, 'input', []);
@ -101,7 +101,7 @@ final class TemplateRendererTest extends TestCase
$renderer = new TemplateRenderer(
static fn(string $value) => $translations[$value],
new TestingParameterStringifier()
new TestingProcessor()
);
$expected = $translations[$template];
@ -120,63 +120,8 @@ final class TemplateRendererTest extends TestCase
$renderer = new TemplateRenderer(
static fn(string $value) => throw new Exception(),
new TestingParameterStringifier()
new TestingProcessor()
);
$renderer->render($template, 'input', []);
}
#[Test]
public function itShouldRenderTranslateParameter(): void
{
$parameterOriginal = 'original';
$parameterTranslated = 'translated';
$template = 'This is my template with {{foo|trans}}';
$translations = [
$parameterOriginal => $parameterTranslated,
$template => 'This is my translated template with {{foo|trans}}',
];
$renderer = new TemplateRenderer(
static fn(string $value) => $translations[$value],
new TestingParameterStringifier()
);
$parameters = ['foo' => $parameterOriginal];
$expected = 'This is my translated template with translated';
$actual = $renderer->render($template, 'input', $parameters);
self::assertSame($expected, $actual);
}
#[Test]
public function itShouldThrowAnExceptionWhenTranslateParameterIsNotScalar(): void
{
$parameterValue = new stdClass();
$template = 'This is my template with {{foo|trans}}';
$renderer = new TemplateRenderer(static fn(string $value) => $value, new TestingParameterStringifier());
$this->expectException(ComponentException::class);
$renderer->render($template, 'input', ['foo' => $parameterValue]);
}
#[Test]
public function itShouldRenderRawParameter(): void
{
$raw = 'raw';
$template = 'This is my template with {{foo|raw}}';
$renderer = new TemplateRenderer(static fn(string $value) => $value, new TestingParameterStringifier());
$expected = 'This is my template with raw';
$actual = $renderer->render($template, 'input', ['foo' => $raw]);
self::assertSame($expected, $actual);
}
}