Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
46 / 46
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
Wrapper
100.00% covered (success)
100.00%
46 / 46
100.00% covered (success)
100.00%
8 / 8
28
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 withTrusted
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 wrap
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
12
 unwrap
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 escape
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 filter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 filters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isTrusted
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Boiler;
6
7use Celema\Boiler\Escaper\Html;
8use Celema\Boiler\Exception\UnexpectedValueException;
9use Celema\Boiler\Proxy\ArrayProxy;
10use Celema\Boiler\Proxy\IteratorProxy;
11use Celema\Boiler\Proxy\ObjectProxy;
12use Celema\Boiler\Proxy\Proxy;
13use Celema\Boiler\Proxy\StringProxy;
14use Override;
15use Traversable;
16
17/** @api */
18final class Wrapper implements Contract\Wrapper
19{
20    private readonly Contract\Escapers $escapers;
21    private ?Contract\Filters $filters;
22    private readonly Contract\Escaper $defaultEscaper;
23    private readonly bool $isBuiltinEscaper;
24
25    /** @var list<class-string> */
26    private array $trusted = [];
27
28    public function __construct(
29        ?Contract\Escapers $escapers = null,
30        ?Contract\Filters $filters = null,
31    ) {
32        $this->escapers = $escapers ?? new Escapers();
33        $this->defaultEscaper = $this->escapers->get($this->escapers->default);
34        $this->isBuiltinEscaper = $this->defaultEscaper instanceof Html;
35        $this->filters = $filters;
36    }
37
38    #[Override]
39    public function withTrusted(array $trusted): static
40    {
41        if ($trusted === $this->trusted) {
42            return $this;
43        }
44
45        $clone = clone $this;
46        $clone->trusted = $trusted;
47
48        return $clone;
49    }
50
51    #[Override]
52    public function wrap(mixed $value): mixed
53    {
54        if (is_string($value)) {
55            return new StringProxy($value, $this);
56        }
57
58        if (is_array($value)) {
59            return new ArrayProxy($value, $this);
60        }
61
62        if (
63            $value === null
64                || is_int($value)
65                || is_float($value)
66                || is_bool($value)
67                || is_resource($value)
68        ) {
69            return $value;
70        }
71
72        if ($value instanceof Proxy) {
73            return $value;
74        }
75
76        // Trust applies at any depth: an object of a trusted class is left
77        // alone whether it sits in the context, in an array, or behind an
78        // iterator. Checked before Traversable so trusted collections stay
79        // unwrapped too.
80        if ($this->isTrusted($value)) {
81            return $value;
82        }
83
84        if ($value instanceof Traversable) {
85            return new IteratorProxy($value, $this);
86        }
87
88        if (is_object($value)) {
89            return new ObjectProxy($value, $this);
90        }
91
92        throw new UnexpectedValueException('Unsupported template value type');
93    }
94
95    #[Override]
96    public function unwrap(mixed $value): mixed
97    {
98        if ($value instanceof Proxy) {
99            return $value->unwrap();
100        }
101
102        if (!is_array($value)) {
103            return $value;
104        }
105
106        return array_map($this->unwrap(...), $value);
107    }
108
109    #[Override]
110    public function escape(
111        string $value,
112        ?string $escaper = null,
113    ): string {
114        if ($escaper === null) {
115            return $this->isBuiltinEscaper
116                ? htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
117                : $this->defaultEscaper->escape($value);
118        }
119
120        return $this->escapers->get($escaper)->escape($value);
121    }
122
123    #[Override]
124    public function filter(string $name): Contract\Filter
125    {
126        return $this->filters()->get($name);
127    }
128
129    private function filters(): Contract\Filters
130    {
131        return $this->filters ??= new Filters();
132    }
133
134    private function isTrusted(mixed $value): bool
135    {
136        if ($this->trusted === [] || !is_object($value)) {
137            return false;
138        }
139
140        foreach ($this->trusted as $class) {
141            if ($value instanceof $class) {
142                return true;
143            }
144        }
145
146        return false;
147    }
148}