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

CPS-IT / handlebars / 15461925183

05 Jun 2025 08:11AM UTC coverage: 90.937% (-1.9%) from 92.804%
15461925183

push

github

web-flow
Merge pull request #434 from CPS-IT/feature/typo3-v13

[!!!][FEATURE] Add support for TYPO3 v13.4, drop support for TYPO3 v12.4

69 of 86 new or added lines in 9 files covered. (80.23%)

2 existing lines in 1 file now uncovered.

883 of 971 relevant lines covered (90.94%)

5.52 hits per line

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

98.36
/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
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17

18
namespace Fr\Typo3Handlebars\Renderer;
19

20
use DevTheorem\Handlebars;
21
use Fr\Typo3Handlebars\Cache;
22
use Fr\Typo3Handlebars\Event;
23
use Fr\Typo3Handlebars\Exception;
24
use Psr\EventDispatcher;
25
use Psr\Http\Message;
26
use Psr\Log;
27
use Symfony\Component\DependencyInjection;
28
use TYPO3\CMS\Core;
29
use TYPO3\CMS\Frontend;
30

31
/**
32
 * HandlebarsRenderer
33
 *
34
 * @author Elias Häußler <e.haeussler@familie-redlich.de>
35
 * @license GPL-2.0-or-later
36
 */
37
#[DependencyInjection\Attribute\AsAlias('handlebars.renderer')]
38
#[DependencyInjection\Attribute\Autoconfigure(tags: ['handlebars.renderer'])]
39
class HandlebarsRenderer implements Renderer
40
{
41
    protected ?bool $debugMode = null;
42

43
    public function __construct(
14✔
44
        #[DependencyInjection\Attribute\Autowire('@handlebars.cache')]
45
        protected readonly Cache\Cache $cache,
46
        protected readonly EventDispatcher\EventDispatcherInterface $eventDispatcher,
47
        protected readonly Helper\HelperRegistry $helperRegistry,
48
        protected readonly Log\LoggerInterface $logger,
49
        #[DependencyInjection\Attribute\Autowire('@handlebars.template_resolver')]
50
        protected readonly Template\TemplateResolver $templateResolver,
51
        protected readonly Variables\VariableBag $variableBag,
52
    ) {}
14✔
53

54
    public function render(Template\View\HandlebarsView $view): string
14✔
55
    {
56
        try {
57
            return $this->processRendering($view);
14✔
58
        } catch (Exception\TemplateFileIsInvalid | Exception\TemplateFormatIsNotSupported | Exception\TemplatePathIsNotResolvable | Exception\ViewIsNotProperlyInitialized $exception) {
7✔
59
            $this->logger->critical($exception->getMessage(), ['exception' => $exception]);
4✔
60

61
            return '';
4✔
62
        }
63
    }
64

65
    /**
66
     * @throws Exception\TemplateFileIsInvalid
67
     * @throws Exception\TemplateFormatIsNotSupported
68
     * @throws Exception\TemplatePathIsNotResolvable
69
     * @throws Exception\ViewIsNotProperlyInitialized
70
     */
71
    protected function processRendering(Template\View\HandlebarsView $view): string
14✔
72
    {
73
        $compileResult = $this->compile($view);
14✔
74

75
        // Early return if template is empty
76
        if ($compileResult === null) {
8✔
77
            return '';
1✔
78
        }
79

80
        // Merge variables with default variables
81
        $mergedVariables = array_merge($this->variableBag->get(), $view->getVariables());
7✔
82

83
        // Dispatch before rendering event
84
        $beforeRenderingEvent = new Event\BeforeRenderingEvent($view, $mergedVariables, $this);
7✔
85
        $this->eventDispatcher->dispatch($beforeRenderingEvent);
7✔
86

87
        // Render content
88
        $renderer = Handlebars\Handlebars::template($compileResult);
7✔
89
        $content = $renderer($beforeRenderingEvent->getVariables(), [
7✔
90
            'helpers' => $this->helperRegistry->getAll(),
7✔
91
        ]);
7✔
92

93
        // Dispatch after rendering event
94
        $afterRenderingEvent = new Event\AfterRenderingEvent($view, $content, $this);
6✔
95
        $this->eventDispatcher->dispatch($afterRenderingEvent);
6✔
96

97
        return $afterRenderingEvent->getContent();
6✔
98
    }
99

100
    /**
101
     * Compile given template by Handlebars compiler.
102
     *
103
     * @throws Exception\TemplateFileIsInvalid
104
     * @throws Exception\TemplateFormatIsNotSupported
105
     * @throws Exception\TemplatePathIsNotResolvable
106
     * @throws Exception\ViewIsNotProperlyInitialized
107
     */
108
    protected function compile(Template\View\HandlebarsView $view): ?string
14✔
109
    {
110
        $template = $view->getTemplate($this->templateResolver);
14✔
111

112
        // Early return if template is empty
113
        if (\trim($template) === '') {
10✔
114
            return null;
1✔
115
        }
116

117
        // Disable cache if debugging is enabled or caching is disabled
118
        if ($this->isDebugModeEnabled() || $this->isCachingDisabled()) {
9✔
119
            $cache = new Cache\NullCache();
3✔
120
        } else {
121
            $cache = $this->cache;
6✔
122
        }
123

124
        // Get compile result from cache
125
        $compileResult = $cache->get($template);
9✔
126
        if ($compileResult !== null) {
9✔
127
            return $compileResult;
1✔
128
        }
129

130
        $compileResult = Handlebars\Handlebars::precompile($template, $this->getCompileOptions());
8✔
131

132
        // Write compiled template into cache
133
        $cache->set($template, $compileResult);
6✔
134

135
        return $compileResult;
6✔
136
    }
137

138
    protected function getCompileOptions(): Handlebars\Options
8✔
139
    {
140
        return new Handlebars\Options(
8✔
141
            strict: $this->isDebugModeEnabled(),
8✔
142
            helpers: $this->getHelperStubs(),
8✔
143
            partialResolver: fn(Handlebars\Context $context, string $name) => $this->resolvePartial($name),
8✔
144
        );
8✔
145
    }
146

147
    /**
148
     * Get currently supported helpers as stubs.
149
     *
150
     * Returns an array of available helper stubs to provide a list of available
151
     * helpers for the compiler. This is necessary to enforce the usage of those
152
     * helpers during compile time, whereas the concrete helper callables are
153
     * provided during runtime.
154
     *
155
     * @return array<string, callable>
156
     */
157
    protected function getHelperStubs(): array
8✔
158
    {
159
        return array_fill_keys(array_keys($this->helperRegistry->getAll()), static fn() => '');
8✔
160
    }
161

162
    /**
163
     * Resolve path to given partial using partial resolver.
164
     *
165
     * Tries to resolve the given partial using the {@see $templateResolver}. If
166
     * no partial resolver is registered, `null` is returned. Otherwise, the
167
     * partials' file contents are returned. Returning `null` will be handled as
168
     * "partial not found" by the renderer.
169
     *
170
     * This method is called by {@see Handlebars\Partial::resolve()}.
171
     *
172
     * @param string $name Name of the partial to be resolved
173
     * @return string|null Partial file contents if partial could be resolved, `null` otherwise
174
     * @throws Exception\PartialPathIsNotResolvable
175
     * @throws Exception\TemplateFormatIsNotSupported
176
     */
177
    protected function resolvePartial(string $name): ?string
2✔
178
    {
179
        $partial = @file_get_contents($this->templateResolver->resolvePartialPath($name));
2✔
180

181
        if ($partial === false) {
2✔
182
            return null;
1✔
183
        }
184

185
        return $partial;
1✔
186
    }
187

188
    protected function isCachingDisabled(): bool
7✔
189
    {
190
        $cacheInstruction = $this->getServerRequest()->getAttribute('frontend.cache.instruction');
7✔
191

192
        if ($cacheInstruction instanceof Frontend\Cache\CacheInstruction) {
7✔
193
            return !$cacheInstruction->isCachingAllowed();
7✔
194
        }
195

NEW
196
        return false;
×
197
    }
198

199
    protected function isDebugModeEnabled(): bool
9✔
200
    {
201
        if ($this->debugMode !== null) {
9✔
202
            return $this->debugMode;
7✔
203
        }
204

205
        $typoScript = $this->getServerRequest()->getAttribute('frontend.typoscript');
9✔
206

207
        if ($typoScript instanceof Core\TypoScript\FrontendTypoScript && (bool)($typoScript->getConfigArray()['debug'] ?? false)) {
9✔
208
            return true;
2✔
209
        }
210

211
        return $this->debugMode = (bool)($GLOBALS['TYPO3_CONF_VARS']['FE']['debug'] ?? false);
8✔
212
    }
213

214
    protected function getServerRequest(): Message\ServerRequestInterface
9✔
215
    {
216
        return $GLOBALS['TYPO3_REQUEST'];
9✔
217
    }
218
}
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