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

CPS-IT / handlebars / 18873543528

28 Oct 2025 11:39AM UTC coverage: 90.854% (-0.06%) from 90.918%
18873543528

Pull #484

github

web-flow
Merge 74526dbb2 into 0eb845663
Pull Request #484: [FEATURE] Extend `removeIf` support to simple variables

13 of 14 new or added lines in 1 file covered. (92.86%)

1 existing line in 1 file now uncovered.

894 of 984 relevant lines covered (90.85%)

5.77 hits per line

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

95.04
/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(
22✔
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
    ) {}
22✔
41

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

52
        // Create rendering context
53
        $context = $this->createContext($conf);
22✔
54

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

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

72
        $context->assignMultiple($this->resolveVariables($conf));
22✔
73

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

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

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

87
        return $content;
16✔
88
    }
89

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

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

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

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

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

120
        return $context;
2✔
121
    }
122

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

136
        // Add current context variables
137
        $variables['data'] = $this->cObj->data ?? [];
22✔
138
        $variables['current'] = $this->cObj?->data[$this->cObj->currentValKey] ?? null;
22✔
139

140
        // Process variables with configured data processors
141
        if ($this->cObj !== null) {
22✔
142
            $variables = $this->contentDataProcessor->process($this->cObj, $config, $variables);
22✔
143
        }
144

145
        // Convert flat variables (foo.bar.baz = xxx) to its multidimensional array representation (foo { bar { baz = xxx }}})
146
        if ((int)($config['unflattenVariableNames'] ?? 0) === 1) {
22✔
147
            $variables = Core\Utility\ArrayUtility::unflatten($variables);
1✔
148
        }
149

150
        // Make settings available as variables
151
        if (isset($config['settings.'])) {
22✔
152
            $variables['settings'] = $this->typoScriptService->convertTypoScriptArrayToPlainArray($config['settings.']);
1✔
153
        }
154

155
        return $variables;
22✔
156
    }
157

158
    /**
159
     * @param array<string, mixed> $variables
160
     * @return array<string, mixed>
161
     */
162
    private function processVariables(array $variables): array
7✔
163
    {
164
        $contentObjectRenderer = $this->getContentObjectRenderer();
7✔
165
        $variablesToProcess = [];
7✔
166
        $simpleVariables = [];
7✔
167

168
        foreach ($variables as $name => $value) {
7✔
169
            if (isset($variablesToProcess[$name])) {
7✔
170
                continue;
3✔
171
            }
172

173
            // Use sanitized variable name for simple variables
174
            $sanitizedName = \rtrim($name, '.');
7✔
175

176
            // Apply variable as simple variable if it's a complex structure (such as objects)
177
            if (!is_string($value) && !\is_array($value)) {
7✔
178
                $simpleVariables[$sanitizedName] = $value;
×
179

180
                continue;
×
181
            }
182

183
            // Register variable for further processing if an appropriate content object is available
184
            // or if variable is a reference to another variable (will be resolved later)
185
            if (is_string($value) &&
7✔
186
                ($contentObjectRenderer->getContentObject($value) !== null || str_starts_with($value, '<'))
7✔
187
            ) {
188
                $cObjConfName = $name . '.';
4✔
189
                $variablesToProcess[$name] = $value;
4✔
190

191
                if (isset($variables[$cObjConfName])) {
4✔
192
                    $variablesToProcess[$cObjConfName] = $variables[$cObjConfName];
3✔
193
                }
194

195
                continue;
4✔
196
            }
197

198
            // Apply variable as simple variable if it's a simple construct
199
            // (including arrays, which will be processed recursively as they may contain content objects)
200
            if (!\is_array($value)) {
5✔
201
                $simpleVariables[$sanitizedName] = $value;
2✔
202
            } elseif (!$this->shouldRemoveVariable($value)) {
4✔
203
                $simpleVariables[$sanitizedName] = $this->processVariables($value);
3✔
204
            }
205
        }
206

207
        // Return only simple variables if no variables need to be processed
208
        if ($variablesToProcess === []) {
7✔
209
            return $simpleVariables;
5✔
210
        }
211

212
        // Process content object variables
213
        $processedVariables = $this->getContentObjectVariables(['variables.' => $variablesToProcess]);
4✔
214

215
        // Merged processed content object variables with simple variables
216
        Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($processedVariables, $simpleVariables);
4✔
217

218
        return $processedVariables;
4✔
219
    }
220

221
    /**
222
     * @param array<string, mixed> $conf
223
     * @return array<string, mixed>
224
     * @throws Exception\ReservedVariableCannotBeUsed
225
     * @see https://github.com/TYPO3/typo3/blob/v13.4.13/typo3/sysext/frontend/Classes/ContentObject/FluidTemplateContentObject.php#L228
226
     */
227
    private function getContentObjectVariables(array $conf): array
19✔
228
    {
229
        if ($this->cObj === null) {
19✔
230
            return [];
×
231
        }
232

233
        $variables = [];
19✔
234
        $reservedVariables = ['data', 'current'];
19✔
235
        $variablesToProcess = (array)($conf['variables.'] ?? []);
19✔
236

237
        foreach ($variablesToProcess as $variableName => $cObjType) {
19✔
238
            if (is_array($cObjType)) {
4✔
239
                continue;
3✔
240
            }
241

242
            if (in_array($variableName, $reservedVariables, true)) {
4✔
243
                throw new Exception\ReservedVariableCannotBeUsed($variableName);
×
244
            }
245

246
            $cObjConf = $variablesToProcess[$variableName . '.'] ?? [];
4✔
247

248
            // Process value
249
            $value = $this->cObj->cObjGetSingle($cObjType, $cObjConf, 'variables.' . $variableName);
4✔
250

251
            // Check if value should *not* be applied after processing
252
            $removeVariable = $this->shouldRemoveVariable($cObjConf, $value);
4✔
253

254
            // Apply value if not empty or no *empty toggle* is set
255
            if (!$removeVariable || trim($value) !== '') {
4✔
256
                $variables[$variableName] = $value;
3✔
257
            }
258
        }
259

260
        return $variables;
19✔
261
    }
262

263
    /**
264
     * @param array<string, mixed> $configuration
265
     */
266
    private function shouldRemoveVariable(array $configuration, ?string $value = null): bool
6✔
267
    {
268
        if ($this->cObj === null) {
6✔
NEW
UNCOV
269
            return false;
×
270
        }
271

272
        $removeCondition = $configuration['removeIf.'] ?? null;
6✔
273

274
        // Early return on missing or insufficient remove condition
275
        if (!\is_array($removeCondition)) {
6✔
276
            return false;
5✔
277
        }
278

279
        // Use processed value as current value
280
        $currentValue = $this->cObj->getCurrentVal();
2✔
281
        $this->cObj->setCurrentVal($value);
2✔
282

283
        try {
284
            return $this->cObj->checkIf($removeCondition);
2✔
285
        } finally {
286
            // Restore original current value
287
            $this->cObj->setCurrentVal($currentValue);
2✔
288
        }
289
    }
290

291
    /**
292
     * @param array<string, mixed> $config
293
     */
294
    private function renderPageAssetsIntoPageRenderer(array $config): void
22✔
295
    {
296
        if (is_string($config['headerAssets'] ?? null) && is_array($config['headerAssets.'] ?? null)) {
22✔
297
            $headerAssets = $this->cObj?->cObjGetSingle($config['headerAssets'], $config['headerAssets.']) ?? '';
1✔
298
        } else {
299
            $headerAssets = '';
22✔
300
        }
301

302
        if (is_string($config['footerAssets'] ?? null) && is_array($config['footerAssets.'] ?? null)) {
22✔
303
            $footerAssets = $this->cObj?->cObjGetSingle($config['footerAssets'], $config['footerAssets.']) ?? '';
1✔
304
        } else {
305
            $footerAssets = '';
22✔
306
        }
307

308
        if (\trim($headerAssets) !== '') {
22✔
309
            $this->getPageRenderer()->addHeaderData($headerAssets);
1✔
310
        }
311

312
        if (\trim($footerAssets) !== '') {
22✔
313
            $this->getPageRenderer()->addFooterData($footerAssets);
1✔
314
        }
315
    }
316
}
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