• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CPS-IT / handlebars / 12784490269

15 Jan 2025 08:37AM UTC coverage: 87.817% (-5.9%) from 93.697%
12784490269

Pull #393

github

web-flow
Merge 2b5dcbbdc into 88f260a4f
Pull Request #393: [!!!][FEATURE] Introduce `HandlebarsView` and require it in renderers

50 of 119 new or added lines in 13 files covered. (42.02%)

2 existing lines in 2 files now uncovered.

901 of 1026 relevant lines covered (87.82%)

3.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.75
/Classes/Renderer/HandlebarsRenderer.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the TYPO3 CMS extension "handlebars".
7
 *
8
 * Copyright (C) 2020 Elias Häußler <e.haeussler@familie-redlich.de>
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, either version 2 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22
 */
23

24
namespace Fr\Typo3Handlebars\Renderer;
25

26
use Fr\Typo3Handlebars\Cache;
27
use Fr\Typo3Handlebars\Event;
28
use Fr\Typo3Handlebars\Exception;
29
use LightnCandy\Context;
30
use LightnCandy\LightnCandy;
31
use LightnCandy\Partial;
32
use LightnCandy\Runtime;
33
use Psr\EventDispatcher;
34
use Psr\Log;
35
use Symfony\Component\DependencyInjection;
36
use TYPO3\CMS\Core;
37
use TYPO3\CMS\Frontend;
38

39
/**
40
 * HandlebarsRenderer
41
 *
42
 * @author Elias Häußler <e.haeussler@familie-redlich.de>
43
 * @license GPL-2.0-or-later
44
 */
45
#[DependencyInjection\Attribute\AsAlias('handlebars.renderer')]
46
#[DependencyInjection\Attribute\Autoconfigure(tags: ['handlebars.renderer'])]
47
class HandlebarsRenderer implements Renderer
48
{
49
    protected readonly bool $debugMode;
50

51
    public function __construct(
14✔
52
        #[DependencyInjection\Attribute\Autowire('@handlebars.cache')]
53
        protected readonly Cache\Cache $cache,
54
        protected readonly EventDispatcher\EventDispatcherInterface $eventDispatcher,
55
        protected readonly Helper\HelperRegistry $helperRegistry,
56
        protected readonly Log\LoggerInterface $logger,
57
        #[DependencyInjection\Attribute\Autowire('@handlebars.template_resolver')]
58
        protected readonly Template\TemplateResolver $templateResolver,
59
        protected readonly Variables\VariableBag $variableBag,
60
    ) {
61
        $this->debugMode = $this->isDebugModeEnabled();
14✔
62
    }
63

64
    public function render(Template\View\HandlebarsView $view): string
12✔
65
    {
66
        try {
67
            return $this->processRendering($view);
12✔
68
        } catch (Exception\TemplateCompilationException | Exception\TemplateFileIsInvalid | Exception\TemplatePathIsNotResolvable | Exception\ViewIsNotProperlyInitialized $exception) {
5✔
69
            $this->logger->critical($exception->getMessage(), ['exception' => $exception]);
5✔
70

71
            return '';
5✔
72
        }
73
    }
74

75
    /**
76
     * @throws Exception\TemplateFileIsInvalid
77
     * @throws Exception\TemplatePathIsNotResolvable
78
     * @throws Exception\ViewIsNotProperlyInitialized
79
     */
80
    protected function processRendering(Template\View\HandlebarsView $view): string
12✔
81
    {
82
        $compileResult = $this->compile($view);
12✔
83

84
        // Early return if template is empty
85
        if ($compileResult === null) {
8✔
86
            return '';
1✔
87
        }
88

89
        // Merge variables with default variables
90
        $mergedVariables = array_merge($this->variableBag->get(), $view->getVariables());
7✔
91

92
        // Dispatch before rendering event
93
        $beforeRenderingEvent = new Event\BeforeRenderingEvent($view, $mergedVariables, $this);
7✔
94
        $this->eventDispatcher->dispatch($beforeRenderingEvent);
7✔
95

96
        // Render content
97
        $renderer = $this->prepareCompileResult($compileResult);
7✔
98
        $content = $renderer($beforeRenderingEvent->getVariables(), [
6✔
99
            'debug' => Runtime::DEBUG_TAGS_HTML,
6✔
100
            'helpers' => $this->helperRegistry->getAll(),
6✔
101
        ]);
6✔
102

103
        // Dispatch after rendering event
104
        $afterRenderingEvent = new Event\AfterRenderingEvent($view, $content, $this);
6✔
105
        $this->eventDispatcher->dispatch($afterRenderingEvent);
6✔
106

107
        return $afterRenderingEvent->getContent();
6✔
108
    }
109

110
    /**
111
     * Compile given template by LightnCandy compiler.
112
     *
113
     * @throws Exception\TemplateFileIsInvalid
114
     * @throws Exception\TemplatePathIsNotResolvable
115
     * @throws Exception\ViewIsNotProperlyInitialized
116
     */
117
    protected function compile(Template\View\HandlebarsView $view): ?string
12✔
118
    {
119
        $template = $view->getTemplate($this->templateResolver);
12✔
120

121
        // Early return if template is empty
122
        if (\trim($template) === '') {
9✔
123
            return null;
1✔
124
        }
125

126
        // Disable cache if debugging is enabled or caching is disabled
127
        if ($this->debugMode || $this->isCachingDisabled()) {
8✔
128
            $cache = new Cache\NullCache();
2✔
129
        } else {
130
            $cache = $this->cache;
6✔
131
        }
132

133
        // Get compile result from cache
134
        $compileResult = $cache->get($template);
8✔
135
        if ($compileResult !== null) {
8✔
136
            return $compileResult;
2✔
137
        }
138

139
        $compileResult = LightnCandy::compile($template, $this->getCompileOptions());
6✔
140

141
        // Handle compilation failures
142
        if ($compileResult === false) {
6✔
143
            $errors = LightnCandy::getContext()['error'] ?? [];
1✔
144

145
            throw new Exception\TemplateCompilationException(
1✔
146
                \sprintf(
1✔
147
                    'Error during template compilation: "%s"',
1✔
148
                    implode('", "', \is_array($errors) ? $errors : [$errors])
1✔
149
                ),
1✔
150
                1614620212
1✔
151
            );
1✔
152
        }
153

154
        // Write compiled template into cache
155
        if (!$this->debugMode) {
5✔
156
            $cache->set($template, $compileResult);
4✔
157
        }
158

159
        return $compileResult;
5✔
160
    }
161

162
    /**
163
     * @return array<string, mixed>
164
     */
165
    protected function getCompileOptions(): array
6✔
166
    {
167
        return [
6✔
168
            'flags' => $this->getCompileFlags(),
6✔
169
            'helpers' => $this->getHelperStubs(),
6✔
170
            'partialresolver' => $this->resolvePartial(...),
6✔
171
        ];
6✔
172
    }
173

174
    protected function getCompileFlags(): int
6✔
175
    {
176
        $flags = LightnCandy::FLAG_HANDLEBARS | LightnCandy::FLAG_RUNTIMEPARTIAL | LightnCandy::FLAG_EXTHELPER | LightnCandy::FLAG_ERROR_EXCEPTION;
6✔
177
        if ($this->debugMode) {
6✔
178
            $flags |= LightnCandy::FLAG_RENDER_DEBUG;
1✔
179
        }
180
        return $flags;
6✔
181
    }
182

183
    protected function prepareCompileResult(string $compileResult): callable
7✔
184
    {
185
        // Touch temporary file
186
        $path = Core\Utility\GeneralUtility::tempnam('hbs_');
7✔
187

188
        try {
189
            // Write file and validate write result
190
            /** @var string|null $writeResult */
191
            $writeResult = Core\Utility\GeneralUtility::writeFileToTypo3tempDir($path, '<?php ' . $compileResult);
7✔
192
            if ($writeResult !== null) {
7✔
UNCOV
193
                throw new Exception\TemplateCompilationException(\sprintf('Cannot prepare compiled render function: %s', $writeResult), 1614705397);
×
194
            }
195

196
            // Build callable
197
            $callable = include $path;
7✔
198
        } finally {
199
            // Remove temporary file
200
            Core\Utility\GeneralUtility::unlink_tempfile($path);
7✔
201
        }
202

203
        // Validate callable
204
        if (!\is_callable($callable)) {
7✔
205
            throw new Exception\TemplateCompilationException('Got invalid compile result from compiler.', 1639405571);
1✔
206
        }
207

208
        return $callable;
6✔
209
    }
210

211
    /**
212
     * Get currently supported helpers as stubs.
213
     *
214
     * Returns an array of available helper stubs to provide a list of available
215
     * helpers for the compiler. This is necessary to enforce the usage of those
216
     * helpers during compile time, whereas the concrete helper callables are
217
     * provided during runtime.
218
     *
219
     * @return array<string, true>
220
     */
221
    protected function getHelperStubs(): array
6✔
222
    {
223
        return array_fill_keys(array_keys($this->helperRegistry->getAll()), true);
6✔
224
    }
225

226
    /**
227
     * Resolve path to given partial using partial resolver.
228
     *
229
     * Tries to resolve the given partial using the {@see $templateResolver}. If
230
     * no partial resolver is registered, `null` is returned. Otherwise, the
231
     * partials' file contents are returned. Returning `null` will be handled as
232
     * "partial not found" by the renderer.
233
     *
234
     * This method is called by {@see Partial::resolver()}.
235
     *
236
     * @param array<string, mixed> $context Current context of compiler progress, see {@see Context::create()}
237
     * @param string $name Name of the partial to be resolved
238
     * @return string|null Partial file contents if partial could be resolved, `null` otherwise
239
     * @throws Exception\PartialPathIsNotResolvable
240
     */
241
    public function resolvePartial(array $context, string $name): ?string
3✔
242
    {
243
        return file_get_contents($this->templateResolver->resolvePartialPath($name)) ?: null;
3✔
244
    }
245

246
    protected function isCachingDisabled(): bool
7✔
247
    {
248
        $tsfe = $this->getTypoScriptFrontendController();
7✔
249
        return $tsfe !== null && (bool)$tsfe->no_cache;
7✔
250
    }
251

252
    protected function isDebugModeEnabled(): bool
14✔
253
    {
254
        $tsfe = $this->getTypoScriptFrontendController();
14✔
255
        if ($tsfe !== null && (bool)($tsfe->config['config']['debug'] ?? false)) {
14✔
256
            return true;
1✔
257
        }
258
        return (bool)($GLOBALS['TYPO3_CONF_VARS']['FE']['debug'] ?? false);
14✔
259
    }
260

261
    protected function getTypoScriptFrontendController(): ?Frontend\Controller\TypoScriptFrontendController
14✔
262
    {
263
        return $GLOBALS['TSFE'] ?? null;
14✔
264
    }
265
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc