Passed
Push — master ( 417605...635760 )
by Divine Niiquaye
11:55
created

TwigRender::withCache()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.072

Importance

Changes 0
Metric Value
cc 3
eloc 4
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 8
ccs 4
cts 5
cp 0.8
crap 3.072
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Biurad opensource projects.
7
 *
8
 * PHP version 7.2 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Biurad\UI\Renders;
19
20
use Biurad\UI\Exceptions\LoaderException;
21
use Biurad\UI\Interfaces\CacheInterface as RenderCacheInterface;
22
use Biurad\UI\Interfaces\RenderInterface;
23
use Biurad\UI\Template;
24
use Twig;
25
use Twig\Cache\FilesystemCache;
26
use Twig\Extension\ExtensionInterface;
27
use Twig\Loader\ChainLoader;
28
use Twig\Loader\LoaderInterface;
29
use Twig\NodeVisitor\NodeVisitorInterface;
30
use Twig\RuntimeLoader\RuntimeLoaderInterface;
31
32
/**
33
 * Render for Twig templating.
34
 *
35
 * @author Divine Niiquaye Ibok <[email protected]>
36
 */
37
final class TwigRender extends AbstractRender implements RenderCacheInterface
38
{
39
    protected const EXTENSIONS = ['twig'];
40
41
    /** @var Twig\Environment */
42
    protected $environment;
43
44
    /**
45
     * TwigEngine constructor.
46
     *
47
     * @param string[] $extensions
48
     */
49 4
    public function __construct(Twig\Environment $environment = null, array $extensions = self::EXTENSIONS)
50
    {
51 4
        $this->environment = $environment ?? new Twig\Environment(new ArrayLoader([], true));
52 4
        $this->extensions = $extensions;
53
    }
54
55
    /**
56
     * {@inheritdoc}
57
     */
58 4
    public function withCache(?string $cacheDir): void
59
    {
60 4
        if (null !== $cacheDir) {
61 3
            if (false !== $this->environment->getCache()) {
62
                throw new LoaderException('The Twig render has an existing cache implementation which must be removed.');
63
            }
64
65 3
            $this->environment->setCache(new FilesystemCache($cacheDir));
66
        }
67
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72 4
    public function withLoader(Template $loader): RenderInterface
73
    {
74 4
        $this->environment->addFunction(
75 4
            new Twig\TwigFunction(
76
                'template',
77 4
                static function (string $template, array $parameters = []) use ($loader): string {
78 4
                    return $loader->render($template, $parameters);
79
                },
80 4
                ['is_safe' => ['all']]
81
            )
82
        );
83
84 4
        return parent::withLoader($loader);
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90 4
    public function render(string $template, array $parameters): string
91
    {
92 4
        if (\file_exists($template)) {
93 4
            if (false !== $cache = $this->environment->getCache()) {
94 3
                $mainClass = $this->environment->getTemplateClass($template);
95
96 3
                if (\file_exists($cacheKey = $cache->generateKey($template, $mainClass))) {
97 3
                    if (!$this->environment->isAutoReload() || $this->environment->isTemplateFresh($template, $cache->getTimestamp($template))) {
98 3
                        $cache->load($cacheKey);
99
                    }
100
101 4
                    return $this->environment->load($template)->render($parameters);
102
                }
103
            }
104
        } else {
105
            $source = self::loadHtml($template) ?? $template;
106
            $template = \substr(\md5($template), 0, 7);
107
        }
108
109 2
        $this->addLoader(new ArrayLoader([$template => $source ?? \file_get_contents($template)]));
110
111 2
        return $this->environment->render($template, $parameters);
112
    }
113
114
    public function setCharset(string $charset): void
115
    {
116
        $this->environment->setCharset($charset);
117
    }
118
119 2
    public function addLoader(LoaderInterface $loader): void
120
    {
121 2
        $templateLoader = $this->environment->getLoader();
122
123 2
        if ($templateLoader instanceof ChainLoader) {
124
            $templateLoader->addLoader($loader);
125
126
            return;
127
        }
128
129 2
        if ($loader instanceof ChainLoader) {
130
            $loader->addLoader($templateLoader);
131 2
        } elseif (!$templateLoader instanceof ArrayLoader || !$templateLoader->isEmpty()) {
132
            $loader = new ChainLoader([$loader, $templateLoader]);
133
        }
134
135 2
        $this->environment->setLoader($loader);
136
    }
137
138
    public function addRuntimeLoader(RuntimeLoaderInterface $loader): void
139
    {
140
        $this->environment->addRuntimeLoader($loader);
141
    }
142
143
    public function addExtension(ExtensionInterface $extension): void
144
    {
145
        $this->environment->addExtension($extension);
146
    }
147
148
    /**
149
     * @param ExtensionInterface[] $extensions An array of extensions
150
     */
151
    public function setExtensions(array $extensions): void
152
    {
153
        $this->environment->setExtensions($extensions);
154
    }
155
156
    public function addNodeVisitor(NodeVisitorInterface $visitor): void
157
    {
158
        $this->environment->addNodeVisitor($visitor);
159
    }
160
161
    public function addFilter(Twig\TwigFilter $filter): void
162
    {
163
        $this->environment->addFilter($filter);
164
    }
165
166
    public function registerUndefinedFilterCallback(callable $callable): void
167
    {
168
        $this->environment->registerUndefinedFilterCallback($callable);
169
    }
170
171
    public function addFunction(Twig\TwigFunction $function): void
172
    {
173
        $this->environment->addFunction($function);
174
    }
175
}
176
177
/**
178
 * Twig Template loader.
179
 */
180
class ArrayLoader implements Twig\Loader\LoaderInterface
181
{
182
    /** @var bool */
183
    private $ignoreKey;
184
185
    /** @var array<string,string> */
186
    private $templates = [];
187
188
    /**
189
     * @param array $templates An array of templates (keys are the names, and values are the source code)
190
     */
191 4
    public function __construct(array $templates = [], bool $ignoreKey = false)
192
    {
193 4
        $this->ignoreKey = $ignoreKey;
194 4
        $this->templates = $templates;
195
    }
196
197 2
    public function isEmpty(): bool
198
    {
199 2
        return empty($this->templates);
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205
    public function setTemplate(string $name, string $template): void
206
    {
207
        $this->templates[$name] = $template;
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213 1
    public function getSourceContext(string $name): Twig\Source
214
    {
215 1
        if (!isset($this->templates[$name])) {
216
            throw new Twig\Error\LoaderError(sprintf('Template "%s" is not defined.', $name));
217
        }
218
219 1
        return new Twig\Source($this->templates[$name], $name);
220
    }
221
222
    public function exists(string $name): bool
223
    {
224
        return isset($this->templates[$name]);
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230 4
    public function getCacheKey(string $name): string
231
    {
232 4
        if (!isset($this->templates[$name])) {
233 3
            if (\file_exists($name)) {
234 3
                return $name;
235
            }
236
237
            if (!$this->ignoreKey) {
238
                return $name . ':' . $this->templates[$name];
239
            }
240
241
            throw new Twig\Error\LoaderError(\sprintf('Template "%s" is not defined.', $name));
242
        }
243
244 2
        return \file_exists($name) ? $name : $name . ':' . $this->templates[$name];
245
    }
246
247
    public function isFresh(string $name, int $time): bool
248
    {
249
        if (\file_exists($name)) {
250
            return \filemtime($name) < $time;
251
        }
252
253
        if (!isset($this->templates[$name])) {
254
            throw new Twig\Error\LoaderError(sprintf('Template "%s" is not defined.', $name));
255
        }
256
257
        return true;
258
    }
259
}
260