Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
28 / 28
28.30% covered (danger)
28.30%
15 / 53
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
MessageFormatter
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
28 / 28
28.30% covered (danger)
28.30%
15 / 53
100.00% covered (success)
100.00%
6 / 6
152.05
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 format
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 template
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
17 / 17
17.39% covered (danger)
17.39%
8 / 46
100.00% covered (success)
100.00%
1 / 1
54.66
 render
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
4
 placeholders
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 stringify
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Sire;
6
7use Stringable;
8
9/** @api */
10final readonly class MessageFormatter
11{
12    /** @param array<string, string> $messages */
13    public function __construct(
14        private array $messages,
15    ) {}
16
17    /**
18     * Templates use `{label}`, `{field}`, `{value}`, and the failure or rule
19     * arguments: named ones by their name, positional ones as `{argN}`.
20     *
21     * @param array<array-key, mixed> $args
22     * @param array<string, string> $messages
23     */
24    public function format(
25        Failure $failure,
26        string $label,
27        string $field,
28        mixed $pristine,
29        ?string $defaultKey = null,
30        string $fallback = '{label} is invalid',
31        array $args = [],
32        array $messages = [],
33    ): string {
34        $template = $this->template($failure, $defaultKey, $fallback, $messages);
35        $args = $failure->args === [] ? $args : $failure->args;
36
37        return self::render($template, $label, $field, $pristine, $args);
38    }
39
40    /** @param array<string, string> $messages */
41    private function template(
42        Failure $failure,
43        ?string $defaultKey,
44        string $fallback,
45        array $messages,
46    ): string {
47        if ($failure->key !== '' && array_key_exists($failure->key, $messages)) {
48            return $messages[$failure->key];
49        }
50
51        if ($defaultKey !== null && array_key_exists($defaultKey, $messages)) {
52            return $messages[$defaultKey];
53        }
54
55        if ($failure->key !== '' && array_key_exists($failure->key, $this->messages)) {
56            return $this->messages[$failure->key];
57        }
58
59        if ($defaultKey !== null && array_key_exists($defaultKey, $this->messages)) {
60            return $this->messages[$defaultKey];
61        }
62
63        return $failure->fallback ?? $fallback;
64    }
65
66    /** @param array<array-key, mixed> $args */
67    private static function render(
68        string $template,
69        string $label,
70        string $field,
71        mixed $pristine,
72        array $args,
73    ): string {
74        $values = self::placeholders(
75            $label,
76            $field,
77            self::stringify($pristine),
78            array_map(self::stringify(...), $args),
79        );
80        $rendered = preg_replace_callback(
81            '/{{|}}|{([^{}]+)}/',
82            static function (array $matches) use ($values): string {
83                $token = $matches[0];
84
85                return match ($token) {
86                    '{{' => '{',
87                    '}}' => '}',
88                    default => $values[$matches[1] ?? ''] ?? $token,
89                };
90            },
91            $template,
92        );
93
94        return $rendered ?? $template;
95    }
96
97    /**
98     * Label, field, and value win over arguments of the same name.
99     *
100     * @param array<array-key, string> $args
101     * @return array<string, string>
102     */
103    private static function placeholders(
104        string $label,
105        string $field,
106        string $value,
107        array $args,
108    ): array {
109        return [
110            ...Arguments::keyed($args),
111            'label' => $label,
112            'field' => $field,
113            'value' => $value,
114        ];
115    }
116
117    private static function stringify(mixed $value): string
118    {
119        return $value instanceof Stringable ? (string) $value : print_r($value, true);
120    }
121}