Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
Filters
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
6 / 6
11
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
1
 register
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 builtins
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 assertName
100.00% covered (success)
100.00%
4 / 4
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
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Boiler;
6
7use Celemas\Boiler\Exception\UnexpectedValueException;
8use Celemas\Boiler\Filter\Lower;
9use Celemas\Boiler\Filter\Sanitize;
10use Celemas\Boiler\Filter\StripTags;
11use Celemas\Boiler\Filter\Trim;
12use Celemas\Boiler\Filter\Upper;
13
14/** @api */
15final class Filters implements Contract\RegistersFilters
16{
17    /** @var array<non-empty-string, Contract\Filter> */
18    private array $registry;
19
20    /** @param array<non-empty-string, Contract\Filter> $filters */
21    public function __construct(array $filters = [])
22    {
23        $this->registry = $this->normalize(array_replace($this->builtins(), $filters));
24    }
25
26    #[\Override]
27    public function register(string $name, Contract\Filter $filter): void
28    {
29        self::assertName($name);
30        $this->registry[$name] = $filter;
31    }
32
33    #[\Override]
34    public function get(string $name): Contract\Filter
35    {
36        return $this->registry[$name] ?? throw new UnexpectedValueException("Unknown filter `{$name}`");
37    }
38
39    /** @return array<non-empty-string, Contract\Filter> */
40    private function builtins(): array
41    {
42        $builtins = [
43            'lower' => new Lower(),
44            'stripTags' => new StripTags(),
45            'trim' => new Trim(),
46            'upper' => new Upper(),
47        ];
48
49        if (class_exists(\Symfony\Component\HtmlSanitizer\HtmlSanitizer::class)) {
50            $builtins['sanitize'] = new Sanitize();
51        }
52
53        return $builtins;
54    }
55
56    /** @psalm-assert non-empty-string $name */
57    private static function assertName(string $name): void
58    {
59        if (!preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $name)) {
60            throw new UnexpectedValueException(
61                "Filter name `{$name}` is not a valid PHP method name",
62            );
63        }
64    }
65
66    /**
67     * @param array<array-key, Contract\Filter> $filters
68     * @return array<non-empty-string, Contract\Filter>
69     */
70    private function normalize(array $filters): array
71    {
72        $normalized = [];
73
74        foreach ($filters as $name => $filter) {
75            if (!is_string($name)) {
76                throw new UnexpectedValueException('Filter name must be a string');
77            }
78
79            self::assertName($name);
80
81            if (!$filter instanceof Contract\Filter) {
82                throw new UnexpectedValueException(
83                    "Filter `{$name}` must implement `Celemas\\Boiler\\Contract\\Filter`",
84                );
85            }
86
87            $normalized[$name] = $filter;
88        }
89
90        return $normalized;
91    }
92}