Skip to content

Commit 3c71b96

Browse files
committed
Replace template rendering code with Respect\StringFormatter
I've moved almost all the code for placeholder replacement and parameter modifiers into an external library called Respect\StringFormatter. This approach allows us to evolve the template capabilities without making major changes to the Validation's code. This commit will introduce another dependency, `respect/string-formatter`, and will upgrade the version of `respect/string-formatter`, which simplifies our internal API greatly. While making this change, I also updated how we generate exceptions. Instead of rendering the full message and the array of messages, we delegate that creation to the `ResultQuery`, which improves performance because we don’t need to render those big messages unless the user actually needs them.
1 parent 9862963 commit 3c71b96

Some content is hidden

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

54 files changed

+407
-1706
lines changed

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"php": ">=8.5",
2424
"php-di/php-di": "^7.1",
2525
"psr/container": "^2.0",
26-
"respect/stringifier": "^2.0.0",
26+
"respect/string-formatter": "^0.1.0",
27+
"respect/stringifier": "^3.0",
2728
"symfony/polyfill-mbstring": "^1.33"
2829
},
2930
"require-dev": {
@@ -46,6 +47,7 @@
4647
"sokil/php-isocodes-db-only": "^4.0",
4748
"squizlabs/php_codesniffer": "^4.0",
4849
"symfony/console": "^7.4",
50+
"symfony/translation": "^8.0",
4951
"symfony/var-exporter": "^6.4 || ^7.0"
5052
},
5153
"suggest": {

docs/messages/placeholder-pipes.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,25 +62,25 @@ v::templated(
6262
// → Le champ "adresse e-mail" est invalide
6363
```
6464

65-
### listOr
65+
### list:or
6666

67-
The `listOr` modifier formats arrays as readable lists using "or":
67+
The `list:or` modifier formats arrays as readable lists using "or":
6868

6969
```php
7070
v::templated(
71-
'Status must be {{haystack|listOr}}',
71+
'Status must be {{haystack|list:or}}',
7272
v::in(['active', 'pending', 'archived']),
7373
)->assert('deleted');
7474
// → Status must be "active", "pending", or "archived"
7575
```
7676

77-
### listAnd
77+
### list:and
7878

79-
The `listAnd` modifier formats arrays as readable lists using "and":
79+
The `list:and` modifier formats arrays as readable lists using "and":
8080

8181
```php
8282
v::templated(
83-
'User must have {{roles|listAnd}} roles to perform this action',
83+
'User must have {{roles|list:and}} roles to perform this action',
8484
v::callback(fn(User $user) => $user->hasRoles(['admin', 'editor'])),
8585
['roles' => ['admin', 'editor']],
8686
)->assert($user);

library/ContainerRegistry.php

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,33 @@
1212

1313
use DI\Container;
1414
use Psr\Container\ContainerInterface;
15+
use Respect\StringFormatter\BypassTranslator;
16+
use Respect\StringFormatter\Modifier;
17+
use Respect\StringFormatter\Modifiers\ListModifier;
18+
use Respect\StringFormatter\Modifiers\QuoteModifier;
19+
use Respect\StringFormatter\Modifiers\RawModifier;
20+
use Respect\StringFormatter\Modifiers\StringifyModifier;
21+
use Respect\StringFormatter\Modifiers\TransModifier;
22+
use Respect\StringFormatter\PlaceholderFormatter;
23+
use Respect\Stringifier\DumpStringifier;
24+
use Respect\Stringifier\Handler;
25+
use Respect\Stringifier\Handlers\CompositeHandler;
26+
use Respect\Stringifier\HandlerStringifier;
1527
use Respect\Stringifier\Quoter;
16-
use Respect\Stringifier\Quoters\StandardQuoter;
28+
use Respect\Stringifier\Quoters\CodeQuoter;
1729
use Respect\Stringifier\Stringifier;
1830
use Respect\Validation\Message\Formatter\FirstResultStringFormatter;
1931
use Respect\Validation\Message\Formatter\NestedArrayFormatter;
2032
use Respect\Validation\Message\Formatter\NestedListStringFormatter;
2133
use Respect\Validation\Message\Formatter\TemplateResolver;
2234
use Respect\Validation\Message\InterpolationRenderer;
23-
use Respect\Validation\Message\Modifier;
24-
use Respect\Validation\Message\Modifier\ListAndModifier;
25-
use Respect\Validation\Message\Modifier\ListOrModifier;
26-
use Respect\Validation\Message\Modifier\QuoteModifier;
27-
use Respect\Validation\Message\Modifier\RawModifier;
28-
use Respect\Validation\Message\Modifier\StringifyModifier;
29-
use Respect\Validation\Message\Modifier\TransModifier;
3035
use Respect\Validation\Message\Renderer;
31-
use Respect\Validation\Message\Translator;
32-
use Respect\Validation\Message\Translator\DummyTranslator;
33-
use Respect\Validation\Message\ValidationStringifier;
36+
use Respect\Validation\Message\Stringifier\Handlers\NameHandler;
37+
use Respect\Validation\Message\Stringifier\Handlers\PathHandler;
38+
use Respect\Validation\Message\Stringifier\Handlers\ResultHandler;
3439
use Respect\Validation\Transformers\Prefix;
3540
use Respect\Validation\Transformers\Transformer;
41+
use Symfony\Contracts\Translation\TranslatorInterface;
3642

3743
use function DI\autowire;
3844
use function DI\create;
@@ -47,9 +53,7 @@ public static function createContainer(): Container
4753
return new Container([
4854
Transformer::class => create(Prefix::class),
4955
TemplateResolver::class => create(TemplateResolver::class),
50-
Quoter::class => create(StandardQuoter::class)->constructor(ValidationStringifier::MAXIMUM_LENGTH),
51-
Stringifier::class => autowire(ValidationStringifier::class),
52-
Translator::class => autowire(DummyTranslator::class),
56+
TranslatorInterface::class => autowire(BypassTranslator::class),
5357
Renderer::class => autowire(InterpolationRenderer::class),
5458
ResultFilter::class => create(OnlyFailedChildrenResultFilter::class),
5559
'respect.validation.formatter.message' => autowire(FirstResultStringFormatter::class),
@@ -61,19 +65,33 @@ public static function createContainer(): Container
6165
$container->get(Transformer::class),
6266
$container->get('respect.validation.rule_factory.namespaces'),
6367
)),
68+
Quoter::class => create(CodeQuoter::class)->constructor(120),
69+
Handler::class => factory(static function (Container $container) {
70+
$handler = CompositeHandler::create();
71+
$handler->prependHandler(new PathHandler($container->get(Quoter::class)));
72+
$handler->prependHandler(new NameHandler());
73+
$handler->prependHandler(new ResultHandler($handler));
74+
75+
return $handler;
76+
}),
77+
PlaceholderFormatter::class => factory(static fn(Container $container) => new PlaceholderFormatter(
78+
[],
79+
$container->get(Modifier::class),
80+
)),
81+
Stringifier::class => factory(static fn(Container $container) => new HandlerStringifier(
82+
$container->get(Handler::class),
83+
new DumpStringifier(),
84+
)),
6485
Modifier::class => factory(static fn(Container $container) => new TransModifier(
65-
$container->get(Translator::class),
66-
new ListOrModifier(
67-
$container->get(Translator::class),
68-
new ListAndModifier(
69-
$container->get(Translator::class),
70-
new QuoteModifier(
71-
new RawModifier(
72-
new StringifyModifier($container->get(Stringifier::class)),
73-
),
86+
new ListModifier(
87+
new QuoteModifier(
88+
new RawModifier(
89+
new StringifyModifier($container->get(Stringifier::class)),
7490
),
7591
),
92+
$container->get(TranslatorInterface::class),
7693
),
94+
$container->get(TranslatorInterface::class),
7795
)),
7896
ValidatorBuilder::class => factory(static fn(Container $container) => new ValidatorBuilder(
7997
$container->get(ValidatorFactory::class),

library/Exceptions/ValidationException.php

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,18 @@
1818
namespace Respect\Validation\Exceptions;
1919

2020
use InvalidArgumentException;
21+
use Respect\Validation\ResultQuery;
2122

2223
use function array_shift;
2324
use function in_array;
2425
use function realpath;
2526

2627
final class ValidationException extends InvalidArgumentException implements Exception
2728
{
28-
/**
29-
* @param array<string|int, mixed> $messages
30-
* @param array<string> $ignoredBacktracePaths
31-
*/
3229
public function __construct(
3330
string $message,
34-
private readonly string $fullMessage,
35-
private readonly array $messages,
36-
array $ignoredBacktracePaths = [],
31+
private readonly ResultQuery $resultQuery,
32+
string ...$ignoredBacktracePaths,
3733
) {
3834
$this->overwriteFileAndLine($ignoredBacktracePaths);
3935

@@ -42,13 +38,13 @@ public function __construct(
4238

4339
public function getFullMessage(): string
4440
{
45-
return $this->fullMessage;
41+
return $this->resultQuery->getFullMessage();
4642
}
4743

4844
/** @return array<string|int, mixed> */
4945
public function getMessages(): array
5046
{
51-
return $this->messages;
47+
return $this->resultQuery->getMessages();
5248
}
5349

5450
/** @param array<string> $ignoredBacktracePaths */

library/Message/InterpolationRenderer.php

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,38 +10,32 @@
1010

1111
namespace Respect\Validation\Message;
1212

13+
use Respect\StringFormatter\PlaceholderFormatter;
1314
use Respect\Validation\Message\Formatter\TemplateResolver;
14-
use Respect\Validation\Message\Placeholder\Subject;
1515
use Respect\Validation\Result;
16-
17-
use function array_key_exists;
18-
use function array_pad;
19-
use function assert;
20-
use function is_string;
21-
use function preg_replace_callback;
16+
use Symfony\Contracts\Translation\TranslatorInterface;
2217

2318
final readonly class InterpolationRenderer implements Renderer
2419
{
2520
public function __construct(
26-
private Translator $translator,
27-
private Modifier $modifier,
21+
private TranslatorInterface $translator,
22+
private PlaceholderFormatter $formatter,
2823
private TemplateResolver $templateResolver,
2924
) {
3025
}
3126

3227
/** @param array<string|int, mixed> $templates */
3328
public function render(Result $result, array $templates): string
3429
{
35-
$parameters = ['path' => $result->path, 'input' => $result->input, 'subject' => Subject::fromResult($result)];
30+
$parameters = ['path' => $result->path, 'input' => $result->input, 'subject' => $result];
3631
$parameters += $result->parameters;
3732

3833
$givenTemplate = $this->templateResolver->getGivenTemplate($result, $templates);
3934
$ruleTemplate = $this->templateResolver->getValidatorTemplate($result);
4035

41-
$rendered = (string) preg_replace_callback(
42-
'/{{(\w+)(\|([^}]+))?}}/',
43-
fn(array $matches) => $this->processPlaceholder($parameters, $matches),
44-
$this->translator->translate($givenTemplate ?? $ruleTemplate),
36+
$rendered = $this->formatter->formatUsing(
37+
$this->translator->trans($givenTemplate ?? $ruleTemplate),
38+
$parameters,
4539
);
4640

4741
if (!$result->hasCustomTemplate() && $givenTemplate === null && $result->adjacent !== null) {
@@ -50,20 +44,4 @@ public function render(Result $result, array $templates): string
5044

5145
return $rendered;
5246
}
53-
54-
/**
55-
* @param array<string, mixed> $parameters
56-
* @param array<int, string|null> $matches
57-
*/
58-
private function processPlaceholder(array $parameters, array $matches): string
59-
{
60-
[$placeholder, $name, , $pipe] = array_pad($matches, 4, null);
61-
assert(is_string($placeholder));
62-
assert(is_string($name));
63-
if (!array_key_exists($name, $parameters)) {
64-
return $placeholder;
65-
}
66-
67-
return $this->modifier->modify($parameters[$name], $pipe);
68-
}
6947
}

library/Message/Modifier.php

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

library/Message/Modifier/ListAndModifier.php

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

library/Message/Modifier/ListOrModifier.php

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

library/Message/Modifier/QuoteModifier.php

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

0 commit comments

Comments
 (0)