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

CPS-IT / handlebars-forms / 35988456624

24 Sep 2026 10:39AM UTC coverage: 0.509% (-0.002%) from 0.511%
35988456624

push

github

web-flow
Merge pull request #92 from CPS-IT/fix/no-string

[BUGFIX] Pass through already resolved non-string values while processing

0 of 3 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

7 of 1374 relevant lines covered (0.51%)

0.03 hits per line

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

0.0
/Classes/DataProcessing/ProcessFormProcessor.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the TYPO3 CMS extension "handlebars_forms".
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\Typo3HandlebarsForms\DataProcessing;
19

20
use CPSIT\Typo3HandlebarsForms\ContentObject;
21
use CPSIT\Typo3HandlebarsForms\Domain;
22
use CPSIT\Typo3HandlebarsForms\Utility;
23
use Psr\Http\Message;
24
use Psr\Log;
25
use Symfony\Component\DependencyInjection;
26
use TYPO3\CMS\Fluid;
27
use TYPO3\CMS\Form;
28
use TYPO3\CMS\Frontend;
29
use TYPO3Fluid\Fluid as FluidStandalone;
30

31
/**
32
 * ProcessFormProcessor
33
 *
34
 * @author Elias Häußler <e.haeussler@familie-redlich.de>
35
 * @license GPL-2.0-or-later
36
 */
37
#[DependencyInjection\Attribute\AutoconfigureTag('data.processor', ['identifier' => 'process-form'])]
38
final readonly class ProcessFormProcessor implements Frontend\ContentObject\DataProcessorInterface
39
{
40
    private const CONTENT_PLACEHOLDER = '###FORM_CONTENT###';
41

42
    public function __construct(
×
43
        private Log\LoggerInterface $logger,
44
        private Fluid\Core\Rendering\RenderingContextFactory $renderingContextFactory,
45
        private Domain\ViewModel\Builder\FormViewModelBuilder $formViewModelBuilder,
46
        private ContentObject\Context\ValueCollector $valueCollector,
47
        private ContentObject\Context\ContextStack $contextStack,
48
    ) {}
×
49

50
    /**
51
     * @param array<string|int, mixed> $contentObjectConfiguration
52
     * @param array<string|int, mixed> $processorConfiguration
53
     * @param array<string|int, mixed> $processedData
54
     * @return array<string|int, mixed>
55
     */
56
    public function process(
×
57
        Frontend\ContentObject\ContentObjectRenderer $cObj,
58
        array $contentObjectConfiguration,
59
        array $processorConfiguration,
60
        array $processedData,
61
    ): array {
62
        $formRuntime = $contentObjectConfiguration['variables.']['form'] ?? null;
×
63

64
        if (!($formRuntime instanceof Form\Domain\Runtime\FormRuntime)) {
×
65
            $this->logger->error(
×
66
                'Form runtime is not available when trying to process form with plugin uid "{uid}".',
×
67
                ['uid' => $cObj->data['uid'] ?? '(unknown)'],
×
68
            );
×
69

70
            return $processedData;
×
71
        }
72

73
        // Create and prepare Fluid rendering context
74
        $renderingContext = $this->renderingContextFactory->create();
×
75
        $renderingContext->setAttribute(Message\ServerRequestInterface::class, $formRuntime->getRequest());
×
76
        $renderingContext->getViewHelperVariableContainer()->addOrUpdate(
×
77
            Form\ViewHelpers\RenderRenderableViewHelper::class,
×
78
            'formRuntime',
×
79
            $formRuntime,
×
80
        );
×
81

82
        // Render form and process renderables as part of the form's renderChildrenClosure.
83
        // Since the final rendered form content (which especially contains all relevant hidden fields)
84
        // is not yet available when processing renderables, we temporarily pass a content placeholder
85
        // for all configured CONTENT values and replace them with the real content value later.
86
        $this->formViewModelBuilder->build(
×
87
            $formRuntime,
×
88
            $renderingContext,
×
89
            function (FluidStandalone\Core\ViewHelper\TagBuilder $tagBuilder) use (
×
90
                $cObj,
×
91
                $formRuntime,
×
92
                &$processedData,
×
93
                $processorConfiguration,
×
94
                $renderingContext,
×
95
                &$tag,
×
96
            ) {
×
97
                $tag = $tagBuilder;
×
98
                $tag->setContent(self::CONTENT_PLACEHOLDER);
×
99

100
                $viewModel = new Domain\ViewModel\StandaloneTagViewModel($formRuntime, $tag);
×
101
                $processedData = $this->processRenderable(
×
102
                    $formRuntime,
×
103
                    $processorConfiguration,
×
104
                    $renderingContext,
×
105
                    $formRuntime,
×
106
                    $cObj,
×
107
                    $viewModel,
×
108
                ) ?? [];
×
109

110
                return '';
×
111
            },
×
112
        );
×
113

114
        $formContent = $tag?->getContent();
×
115

116
        // Replace content placeholder with final rendered form content
117
        if ($formContent !== null) {
×
118
            array_walk_recursive($processedData, static function (&$value) use ($formContent) {
×
119
                // Only strings may contain the content placeholder, other scalars must not be converted to strings
120
                if (is_string($value) || $value instanceof \Stringable) {
×
121
                    $value = Utility\StringUtility::processStringable(
×
122
                        $value,
×
123
                        static fn(string $string) => str_replace(self::CONTENT_PLACEHOLDER, $formContent, $string),
×
124
                    );
×
125
                }
126
            });
×
127
        }
128

129
        return $processedData;
×
130
    }
131

132
    /**
133
     * @param array<string|int, mixed> $configuration
134
     * @return array<string|int, mixed>|null
135
     */
136
    private function processRenderable(
×
137
        Form\Domain\Model\Renderable\RootRenderableInterface $renderable,
138
        array $configuration,
139
        Fluid\Core\Rendering\RenderingContext $renderingContext,
140
        Form\Domain\Runtime\FormRuntime $formRuntime,
141
        Frontend\ContentObject\ContentObjectRenderer $cObj,
142
        Domain\ViewModel\ViewModel $viewModel,
143
    ): ?array {
144
        $processedData = [];
×
145

146
        // Early return on configured "if" condition evaluating to false
147
        if (!$this->checkIf($configuration, $renderable, $renderingContext, $formRuntime, $cObj, $viewModel)) {
×
148
            return null;
×
149
        }
150

151
        // Merge TS reference (=<) and replace configuration with merged configuration
152
        $this->mergeTypoScriptReferences($configuration, $cObj);
×
153

154
        foreach ($configuration as $key => $value) {
×
155
            $keyWithoutDot = rtrim((string)$key, '.');
×
156
            $keyWithDot = $keyWithoutDot . '.';
×
157

158
            if (is_array($value) && !array_key_exists($keyWithoutDot, $processedData)) {
×
159
                $resolvedValue = $this->processRenderable($renderable, $value, $renderingContext, $formRuntime, $cObj, $viewModel);
×
160

161
                if (is_array($resolvedValue)) {
×
162
                    $processedData[$keyWithoutDot] = $resolvedValue;
×
163
                }
164
            }
165

166
            // Arrays are already processed above
NEW
167
            if (is_array($value)) {
×
NEW
168
                continue;
×
169
            }
170

171
            // Pass through already resolved non-string values (e.g. when re-processing
172
            // resolved values within HBS_VALIDATION_RESULTS with EACH_RENDERABLE instruction)
173
            if (!is_string($value)) {
×
NEW
174
                $processedData[$keyWithoutDot] = $value;
×
UNCOV
175
                continue;
×
176
            }
177

178
            $valueConfiguration = $configuration[$keyWithDot] ?? [];
×
179
            $contentObject = $cObj->getContentObject($value);
×
180

181
            // Unwrap context-aware content object to avoid conversion of collected scalar values to strings
182
            if ($contentObject instanceof ContentObject\Context\ContextAwareContentObject) {
×
183
                $contentObject = $contentObject->contentObject;
×
184
            }
185

186
            if (!is_array($valueConfiguration)) {
×
187
                $valueConfiguration = [];
×
188
            }
189

190
            // Resolve configured value
191
            if ($contentObject !== null) {
×
192
                $context = new ContentObject\Context\ValueResolutionContext(
×
193
                    $renderable,
×
194
                    $viewModel,
×
195
                    $renderingContext,
×
196
                    $formRuntime,
×
197
                    fn(
×
198
                        array $contextConfiguration,
×
199
                        ?Form\Domain\Model\Renderable\RootRenderableInterface $contextRenderable = null,
×
200
                        ?Domain\ViewModel\ViewModel $contextViewModel = null,
×
201
                    ) => $this->processRenderable(
×
202
                        $contextRenderable ?? $renderable,
×
203
                        $contextConfiguration,
×
204
                        $renderingContext,
×
205
                        $formRuntime,
×
206
                        $cObj,
×
207
                        $contextViewModel ?? $viewModel,
×
208
                    ),
×
209
                );
×
210

211
                $this->contextStack->push($context);
×
212

213
                try {
214
                    $resolvedValue = $cObj->render($contentObject, $valueConfiguration);
×
215
                } finally {
216
                    $this->contextStack->pop();
×
217
                }
218

219
                if ($this->valueCollector->has($resolvedValue)) {
×
220
                    $resolvedValue = $this->valueCollector->load($resolvedValue);
×
221
                }
222
            } else {
223
                $resolvedValue = $value;
×
224
            }
225

226
            // Skip further processing if processed value is not a string (all COR related methods require a string value)
227
            if (!Utility\StringUtility::isStringable($resolvedValue)) {
×
228
                $processedData[$keyWithoutDot] = $resolvedValue;
×
229
                continue;
×
230
            }
231

232
            // Skip value if a configured "if" evaluates to false
233
            if (is_array($valueConfiguration['if.'] ?? null)) {
×
234
                $valueConfiguration['if.']['value'] ??= (string)$resolvedValue;
×
235

236
                if (!$this->checkIf($valueConfiguration, $renderable, $renderingContext, $formRuntime, $cObj, $viewModel)) {
×
237
                    continue;
×
238
                }
239
            }
240

241
            $processedData[$keyWithoutDot] = $resolvedValue;
×
242
        }
243

244
        return $processedData;
×
245
    }
246

247
    /**
248
     * @param array<string|int, mixed> $configuration
249
     */
250
    private function mergeTypoScriptReferences(
×
251
        array &$configuration,
252
        Frontend\ContentObject\ContentObjectRenderer $cObj,
253
    ): void {
254
        $processedKeys = [];
×
255

256
        foreach ($configuration as $key => $value) {
×
257
            if (in_array($key, $processedKeys, true)) {
×
258
                continue;
×
259
            }
260

261
            $keyWithoutDot = rtrim((string)$key, '.');
×
262
            $keyWithDot = $keyWithoutDot . '.';
×
263

264
            if (array_key_exists($keyWithDot, $configuration) && is_array($configuration[$keyWithDot])) {
×
265
                $this->mergeTypoScriptReferences($configuration[$keyWithDot], $cObj);
×
266
            }
267

268
            if (!is_string($configuration[$keyWithoutDot] ?? null)) {
×
269
                continue;
×
270
            }
271

272
            if (str_starts_with($configuration[$keyWithoutDot], '<')) {
×
273
                $mergedConfig = $cObj->mergeTSRef(
×
274
                    [
×
275
                        $keyWithoutDot => $configuration[$keyWithoutDot],
×
276
                        $keyWithDot => $configuration[$keyWithDot] ?? [],
×
277
                    ],
×
278
                    $keyWithoutDot,
×
279
                );
×
280

281
                // Apply merged config only if a substition was found and applied
282
                if (is_string($mergedConfig[$keyWithoutDot]) && '<' . $mergedConfig[$keyWithoutDot] !== $configuration[$keyWithoutDot]) {
×
283
                    $configuration[$keyWithoutDot] = $mergedConfig[$keyWithoutDot];
×
284
                }
285

286
                if ($mergedConfig[$keyWithDot] !== []) {
×
287
                    $configuration[$keyWithDot] = $mergedConfig[$keyWithDot];
×
288
                }
289
            }
290

291
            $processedKeys[] = $keyWithoutDot;
×
292
            $processedKeys[] = $keyWithDot;
×
293
        }
294
    }
295

296
    /**
297
     * @param array<string|int, mixed> $configuration
298
     */
299
    private function checkIf(
×
300
        array &$configuration,
301
        Form\Domain\Model\Renderable\RootRenderableInterface $renderable,
302
        Fluid\Core\Rendering\RenderingContext $renderingContext,
303
        Form\Domain\Runtime\FormRuntime $formRuntime,
304
        Frontend\ContentObject\ContentObjectRenderer $cObj,
305
        Domain\ViewModel\ViewModel $viewModel,
306
    ): bool {
307
        if (!is_array($configuration['if.'] ?? null)) {
×
308
            return true;
×
309
        }
310

311
        $cObjTemp = clone $cObj;
×
312

313
        if (is_string($configuration['if.']['currentValue'] ?? null) || is_array($configuration['if.']['currentValue.'] ?? null)) {
×
314
            $processedValue = $this->processRenderable(
×
315
                $renderable,
×
316
                [
×
317
                    'currentValue' => $configuration['if.']['currentValue'] ?? '',
×
318
                    'currentValue.' => $configuration['if.']['currentValue.'] ?? [],
×
319
                ],
×
320
                $renderingContext,
×
321
                $formRuntime,
×
322
                $cObj,
×
323
                $viewModel,
×
324
            );
×
325

326
            $cObjTemp->setCurrentVal($processedValue['currentValue'] ?? null);
×
327

328
            unset($configuration['if.']['currentValue'], $configuration['if.']['currentValue.']);
×
329
        }
330

331
        if (!$cObjTemp->checkIf($configuration['if.'])) {
×
332
            return false;
×
333
        }
334

335
        unset($configuration['if.']);
×
336

337
        return true;
×
338
    }
339
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc