Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
Escapers
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
7 / 7
12
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 register
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 builtins
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 assertName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 normalize
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 unknown
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Boiler;
6
7use Celemas\Boiler\Exception\UnexpectedValueException;
8
9/** @api */
10final class Escapers implements Contract\RegistersEscapers
11{
12    private const string HTML = 'html';
13
14    public readonly string $default;
15
16    /** @var array<non-empty-string, Contract\Escaper> */
17    private array $registry;
18
19    /** @param array<non-empty-string, Contract\Escaper> $escapers */
20    public function __construct(
21        array $escapers = [],
22        string $default = self::HTML,
23    ) {
24        $this->registry = $this->normalize(array_replace($this->builtins(), $escapers));
25        self::assertName($default);
26
27        if (!isset($this->registry[$default])) {
28            throw self::unknown($default);
29        }
30
31        $this->default = $default;
32    }
33
34    #[\Override]
35    public function get(string $name): Contract\Escaper
36    {
37        return $this->registry[$name] ?? throw self::unknown($name);
38    }
39
40    #[\Override]
41    public function register(string $name, Contract\Escaper $escaper): void
42    {
43        self::assertName($name);
44        $this->registry[$name] = $escaper;
45    }
46
47    /** @return array<non-empty-string, Contract\Escaper> */
48    private function builtins(): array
49    {
50        return [
51            self::HTML => new Escaper\Html(),
52        ];
53    }
54
55    /** @psalm-assert non-empty-string $name */
56    private static function assertName(string $name): void
57    {
58        if ($name === '') {
59            throw new UnexpectedValueException('Escaper name must be a non-empty string');
60        }
61    }
62
63    /**
64     * @param array<array-key, Contract\Escaper> $escapers
65     * @return array<non-empty-string, Contract\Escaper>
66     */
67    private function normalize(array $escapers): array
68    {
69        $normalized = [];
70
71        foreach ($escapers as $name => $escaper) {
72            if (!is_string($name)) {
73                throw new UnexpectedValueException('Escaper name must be a non-empty string');
74            }
75
76            self::assertName($name);
77
78            if (!$escaper instanceof Contract\Escaper) {
79                throw new UnexpectedValueException(
80                    "Escaper `{$name}` must implement `Celemas\\Boiler\\Contract\\Escaper`",
81                );
82            }
83
84            $normalized[$name] = $escaper;
85        }
86
87        return $normalized;
88    }
89
90    private static function unknown(string $name): UnexpectedValueException
91    {
92        return new UnexpectedValueException("Unknown escaper `{$name}`");
93    }
94}