Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
Template
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
6 / 6
11
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 render
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 renderEscaped
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 renderUnescaped
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 method
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderWith
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Boiler;
6
7use Celema\Boiler\Exception\LookupException;
8
9/** @api */
10final class Template
11{
12    /** the resolved file path */
13    public readonly string $path;
14    public readonly Engine $engine;
15    private readonly Methods $methods;
16
17    public function __construct(string $path, ?Engine $engine = null)
18    {
19        if ($engine === null) {
20            if ($path === '') {
21                throw new LookupException('No directory given or empty path');
22            }
23
24            $engine = new Engine(new Resolver(dirname($path)), new Environment(), true);
25        }
26
27        $file = realpath($path);
28
29        // realpath() also resolves directories, which include() cannot load.
30        if ($file === false || !is_file($file)) {
31            throw new LookupException('Template not found: ' . $path);
32        }
33
34        // PHP reports an included file under its resolved path, so error
35        // locations only find the template's lines under that path.
36        $this->path = $file;
37        $this->engine = $engine;
38        // Layered over the engine's, so engine methods stay available, including
39        // ones registered later, unless this template registers the same name.
40        $this->methods = new Methods($engine->methods());
41    }
42
43    /**
44     * @param array<string, mixed> $context
45     * @param list<class-string> $trusted
46     */
47    public function render(array $context = [], array $trusted = []): string
48    {
49        return $this->renderWith($context, $trusted, $this->engine->autoescape);
50    }
51
52    /**
53     * @param array<string, mixed> $context
54     * @param list<class-string> $trusted
55     */
56    public function renderEscaped(array $context = [], array $trusted = []): string
57    {
58        return $this->renderWith($context, $trusted, true);
59    }
60
61    /**
62     * @param array<string, mixed> $context
63     * @param list<class-string> $trusted
64     */
65    public function renderUnescaped(array $context = [], array $trusted = []): string
66    {
67        return $this->renderWith($context, $trusted, false);
68    }
69
70    /** @param non-empty-string $name */
71    public function method(string $name, callable $callable, bool $safe = false): static
72    {
73        $this->methods->add($name, $callable, $safe);
74
75        return $this;
76    }
77
78    /**
79     * @param array<string, mixed> $context
80     * @param list<class-string> $trusted
81     */
82    private function renderWith(array $context, array $trusted, bool $autoescape): string
83    {
84        // Applied here rather than in Engine::render(), so that a template from
85        // Engine::template() renders like Engine::render().
86        $defaults = $this->engine->defaults;
87        $context = $defaults === [] ? $context : array_merge($defaults, $context);
88        $trusted = [...$this->engine->trusted, ...$trusted];
89        $rendering = new Rendering(
90            $this->path,
91            $this->engine,
92            $this->methods,
93            new Sections(),
94            $trusted,
95            $this->engine->wrapper()->withTrusted($trusted),
96            $autoescape,
97        );
98
99        return TemplateFunctions::run(
100            $rendering->wrapper,
101            static fn(): string => $rendering->render($context, wrapped: false),
102        );
103    }
104}