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

CPS-IT / handlebars / 16874800904

11 Aug 2025 08:26AM UTC coverage: 91.444% (+0.04%) from 91.407%
16874800904

push

github

web-flow
Merge pull request #458 from CPS-IT/feature/remove-if-empty

7 of 7 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

855 of 935 relevant lines covered (91.44%)

5.77 hits per line

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

95.41
/Classes/Frontend/ContentObject/HandlebarsTemplateContentObject.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 CPSIT\Typo3Handlebars\Frontend\ContentObject;
19

20
use CPSIT\Typo3Handlebars\Exception;
21
use CPSIT\Typo3Handlebars\Renderer;
22
use Symfony\Component\DependencyInjection;
23
use TYPO3\CMS\Core;
24
use TYPO3\CMS\Frontend;
25

26
/**
27
 * HandlebarsTemplateContentObject
28
 *
29
 * @author Elias Häußler <e.haeussler@familie-redlich.de>
30
 * @license GPL-2.0-or-later
31
 */
32
#[DependencyInjection\Attribute\AutoconfigureTag('frontend.contentobject', ['identifier' => 'HANDLEBARSTEMPLATE'])]
33
final class HandlebarsTemplateContentObject extends Frontend\ContentObject\AbstractContentObject
34
{
35
    public function __construct(
20✔
36
        private readonly Frontend\ContentObject\ContentDataProcessor $contentDataProcessor,
37
        private readonly Renderer\Template\Path\ContentObjectPathProvider $pathProvider,
38
        private readonly Renderer\Renderer $renderer,
39
        private readonly Core\TypoScript\TypoScriptService $typoScriptService,
40
    ) {}
20✔
41

42
    /**
43
     * @param array<string, mixed> $conf
44
     */
45
    public function render($conf = []): string
20✔
46
    {
47
        /* @phpstan-ignore function.alreadyNarrowedType */
48
        if (!\is_array($conf)) {
20✔
49
            $conf = [];
×
50
        }
51

52
        // Create handlebars view
53
        $view = $this->createView($conf);
20✔
54

55
        // Resolve template paths
56
        /** @var array<string, mixed> $templatePaths */
57
        $templatePaths = $this->typoScriptService->convertTypoScriptArrayToPlainArray(
20✔
58
            array_intersect_key(
20✔
59
                $conf,
20✔
60
                [
20✔
61
                    'partialRootPath' => true,
20✔
62
                    'partialRootPaths.' => true,
20✔
63
                    'templateRootPath' => true,
20✔
64
                    'templateRootPaths.' => true,
20✔
65
                ],
20✔
66
            ),
20✔
67
        );
20✔
68

69
        // Populate template paths for availability in subsequent renderings
70
        $this->pathProvider->push($templatePaths);
20✔
71

72
        $view->assignMultiple($this->resolveVariables($conf));
20✔
73

74
        $this->renderPageAssetsIntoPageRenderer($conf);
20✔
75

76
        try {
77
            $content = $this->renderer->render($view);
20✔
78
        } finally {
79
            // Remove current content object rendering from path provider stack
80
            $this->pathProvider->pop();
20✔
81
        }
82

83
        if (isset($conf['stdWrap.'])) {
15✔
84
            return $this->cObj?->stdWrap($content, $conf['stdWrap.']) ?? $content;
1✔
85
        }
86

87
        return $content;
14✔
88
    }
89

90
    /**
91
     * @param array<string, mixed> $config
92
     */
93
    private function createView(array $config): Renderer\Template\View\HandlebarsView
20✔
94
    {
95
        $format = $this->cObj?->stdWrapValue('format', $config, null);
20✔
96
        $view = new Renderer\Template\View\HandlebarsView();
20✔
97

98
        if (is_string($format)) {
20✔
99
            $view->setFormat($format);
2✔
100
        }
101

102
        if (isset($config['templateName']) || isset($config['templateName.'])) {
20✔
103
            return $view->setTemplatePath(
2✔
104
                (string)$this->cObj?->stdWrapValue('templateName', $config),
2✔
105
            );
2✔
106
        }
107

108
        if (isset($config['template']) || isset($config['template.'])) {
18✔
109
            return $view->setTemplateSource(
14✔
110
                (string)$this->cObj?->stdWrapValue('template', $config),
14✔
111
            );
14✔
112
        }
113

114
        if (isset($config['file']) || isset($config['file.'])) {
4✔
115
            return $view->setTemplatePath(
2✔
116
                (string)$this->cObj?->stdWrapValue('file', $config),
2✔
117
            );
2✔
118
        }
119

120
        return $view;
2✔
121
    }
122

123
    /**
124
     * @param array<string, mixed> $config
125
     * @return array<string, mixed>
126
     */
127
    private function resolveVariables(array $config): array
20✔
128
    {
129
        // Process content object variables and simple variables
130
        if (\is_array($config['variables.'] ?? null)) {
20✔
131
            $variables = $this->processVariables($config['variables.']);
5✔
132
        } else {
133
            $variables = $this->getContentObjectVariables($config);
15✔
134
        }
135

136
        // Process variables with configured data processors
137
        if ($this->cObj !== null) {
20✔
138
            $variables = $this->contentDataProcessor->process($this->cObj, $config, $variables);
20✔
139
        }
140

141
        // Make settings available as variables
142
        if (isset($config['settings.'])) {
20✔
143
            $variables['settings'] = $this->typoScriptService->convertTypoScriptArrayToPlainArray($config['settings.']);
1✔
144
        }
145

146
        return $variables;
20✔
147
    }
148

149
    /**
150
     * @param array<string, mixed> $variables
151
     * @return array<string, mixed>
152
     */
153
    private function processVariables(array $variables): array
5✔
154
    {
155
        $contentObjectRenderer = $this->getContentObjectRenderer();
5✔
156
        $variablesToProcess = [];
5✔
157
        $simpleVariables = [];
5✔
158

159
        foreach ($variables as $name => $value) {
5✔
160
            if (isset($variablesToProcess[$name])) {
5✔
161
                continue;
3✔
162
            }
163

164
            // Use sanitized variable name for simple variables
165
            $sanitizedName = \rtrim($name, '.');
5✔
166

167
            // Apply variable as simple variable if it's a complex structure (such as objects)
168
            if (!is_string($value) && !\is_array($value)) {
5✔
169
                $simpleVariables[$sanitizedName] = $value;
×
170

171
                continue;
×
172
            }
173

174
            // Register variable for further processing if an appropriate content object is available
175
            // or if variable is a reference to another variable (will be resolved later)
176
            if (is_string($value) &&
5✔
177
                ($contentObjectRenderer->getContentObject($value) !== null || str_starts_with($value, '<'))
5✔
178
            ) {
179
                $cObjConfName = $name . '.';
4✔
180
                $variablesToProcess[$name] = $value;
4✔
181

182
                if (isset($variables[$cObjConfName])) {
4✔
183
                    $variablesToProcess[$cObjConfName] = $variables[$cObjConfName];
3✔
184
                }
185

186
                continue;
4✔
187
            }
188

189
            // Apply variable as simple variable if it's a simple construct
190
            // (including arrays, which will be processed recursively as they may contain content objects)
191
            if (\is_array($value)) {
3✔
192
                $simpleVariables[$sanitizedName] = $this->processVariables($value);
3✔
193

194
                unset($simpleVariables[$sanitizedName]['data']);
3✔
195
                unset($simpleVariables[$sanitizedName]['current']);
3✔
196
            } else {
197
                $simpleVariables[$sanitizedName] = $value;
1✔
198
            }
199
        }
200

201
        // Process content object variables
202
        $processedVariables = $this->getContentObjectVariables(['variables.' => $variablesToProcess]);
5✔
203

204
        // Merged processed content object variables with simple variables
205
        Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($processedVariables, $simpleVariables);
5✔
206

207
        return $processedVariables;
5✔
208
    }
209

210
    /**
211
     * @param array<string, mixed> $conf
212
     * @return array<string, mixed>
213
     * @throws Exception\ReservedVariableCannotBeUsed
214
     * @see https://github.com/TYPO3/typo3/blob/v13.4.13/typo3/sysext/frontend/Classes/ContentObject/FluidTemplateContentObject.php#L228
215
     */
216
    private function getContentObjectVariables(array $conf): array
20✔
217
    {
218
        if ($this->cObj === null) {
20✔
219
            return [];
×
220
        }
221

222
        $variables = [];
20✔
223
        $reservedVariables = ['data', 'current'];
20✔
224
        $variablesToProcess = (array)($conf['variables.'] ?? []);
20✔
225

226
        foreach ($variablesToProcess as $variableName => $cObjType) {
20✔
227
            if (is_array($cObjType)) {
4✔
228
                continue;
3✔
229
            }
230

231
            if (in_array($variableName, $reservedVariables, true)) {
4✔
UNCOV
232
                throw new Exception\ReservedVariableCannotBeUsed($variableName);
×
233
            }
234

235
            $cObjConf = $variablesToProcess[$variableName . '.'] ?? [];
4✔
236

237
            // Check if empty value should *not* be applied after processing
238
            $removeIfEmpty = (int)($cObjConf['removeIfEmpty'] ?? 0) === 1;
4✔
239
            unset($cObjConf['removeIfEmpty']);
4✔
240

241
            // Process value
242
            $value = $this->cObj->cObjGetSingle($cObjType, $cObjConf, 'variables.' . $variableName);
4✔
243

244
            // Apply value if not empty or no *empty toggle* is set
245
            if (!$removeIfEmpty || trim($value) !== '') {
4✔
246
                $variables[$variableName] = $value;
3✔
247
            }
248
        }
249

250
        $variables['data'] = $this->cObj->data;
20✔
251
        $variables['current'] = $this->cObj->data[$this->cObj->currentValKey] ?? null;
20✔
252

253
        return $variables;
20✔
254
    }
255

256
    /**
257
     * @param array<string, mixed> $config
258
     */
259
    private function renderPageAssetsIntoPageRenderer(array $config): void
20✔
260
    {
261
        if (is_string($config['headerAssets'] ?? null) && is_array($config['headerAssets.'] ?? null)) {
20✔
262
            $headerAssets = $this->cObj?->cObjGetSingle($config['headerAssets'], $config['headerAssets.']) ?? '';
1✔
263
        } else {
264
            $headerAssets = '';
20✔
265
        }
266

267
        if (is_string($config['footerAssets'] ?? null) && is_array($config['footerAssets.'] ?? null)) {
20✔
268
            $footerAssets = $this->cObj?->cObjGetSingle($config['footerAssets'], $config['footerAssets.']) ?? '';
1✔
269
        } else {
270
            $footerAssets = '';
20✔
271
        }
272

273
        if (\trim($headerAssets) !== '') {
20✔
274
            $this->getPageRenderer()->addHeaderData($headerAssets);
1✔
275
        }
276

277
        if (\trim($footerAssets) !== '') {
20✔
278
            $this->getPageRenderer()->addFooterData($footerAssets);
1✔
279
        }
280
    }
281
}
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