Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
Values
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
7 / 7
19
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
2
 plain
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 get
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 add
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 output
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 wrapped
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 wrapAll
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Boiler;
6
7use Celema\Boiler\Contract\Wrapper;
8use Celema\Boiler\Exception\RuntimeException;
9use Celema\Boiler\Proxy\ArrayProxy;
10use Celema\Boiler\Proxy\StringProxy;
11use Stringable;
12
13/**
14 * The values a template's code receives: its render context, wrapped in an
15 * escaped render, and the results of its template methods.
16 *
17 * Kept apart from Context, because templates run in the scope of their
18 * context object, where every method of Context would shadow a registered
19 * template method of the same name.
20 *
21 * @internal
22 */
23final class Values
24{
25    /** @var array<array-key, mixed>|null */
26    private ?array $wrapped;
27
28    /**
29     * @param array<array-key, mixed> $context
30     * @param bool $wrapped whether the context holds its values as a template gets them, like that of a layout or include
31     */
32    public function __construct(
33        private array $context,
34        public readonly Wrapper $wrapper,
35        private readonly bool $autoescape,
36        bool $wrapped,
37    ) {
38        $this->wrapped = $wrapped ? $context : null;
39    }
40
41    /** The original string of a path, name, or key, which a template may pass wrapped. */
42    public static function plain(StringProxy|string $value): string
43    {
44        return $value instanceof StringProxy ? $value->unwrap() : $value;
45    }
46
47    /**
48     * @param ArrayProxy|array<array-key, mixed> $values
49     * @return array<array-key, mixed>
50     */
51    public function get(ArrayProxy|array $values = []): array
52    {
53        if ($values instanceof ArrayProxy) {
54            $values = $values->unwrap();
55        }
56
57        if (!$this->autoescape) {
58            return $values === []
59                ? $this->context
60                : array_merge($this->context, $values);
61        }
62
63        if ($values === []) {
64            return $this->wrapped();
65        }
66
67        return array_merge($this->wrapped(), $this->wrapAll($values));
68    }
69
70    public function add(StringProxy|string $key, mixed $value): mixed
71    {
72        $this->context[self::plain($key)] = $value;
73        $this->wrapped = null;
74
75        return $this->output($value);
76    }
77
78    /** What a template receives for a value, such as the result of a template method. */
79    public function output(mixed $value, bool $safe = false): mixed
80    {
81        if (!$this->autoescape) {
82            return $this->wrapper->unwrap($value);
83        }
84
85        if (!$safe) {
86            return $this->wrapper->wrap($value);
87        }
88
89        if ($value instanceof StringProxy) {
90            return new StringProxy($value->unwrap(), $this->wrapper, safe: true);
91        }
92
93        if (is_string($value) || $value instanceof Stringable) {
94            return new StringProxy((string) $value, $this->wrapper, safe: true);
95        }
96
97        throw new RuntimeException('Safe template methods must return string or Stringable values');
98    }
99
100    /** @return array<array-key, mixed> */
101    private function wrapped(): array
102    {
103        return $this->wrapped ??= $this->wrapAll($this->context);
104    }
105
106    /**
107     * @param array<array-key, mixed> $values
108     * @return array<array-key, mixed>
109     */
110    private function wrapAll(array $values): array
111    {
112        $wrapped = [];
113
114        /** @var mixed $value */
115        foreach ($values as $key => $value) {
116            /** @psalm-suppress MixedAssignment wrapper returns mixed by design */
117            $wrapped[$key] = $this->wrapper->wrap($value);
118        }
119
120        return $wrapped;
121    }
122}