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

CPS-IT / handlebars / 34090556062

07 Sep 2026 06:22AM UTC coverage: 90.11% (-0.08%) from 90.188%
34090556062

Pull #631

github

web-flow
Merge ad5f338cc into a14fa95b8
Pull Request #631: [FEATURE] Introduce `process-each` data processor

59 of 67 new or added lines in 1 file covered. (88.06%)

1640 of 1820 relevant lines covered (90.11%)

6.83 hits per line

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

88.06
/Classes/DataProcessing/ProcessEachProcessor.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\DataProcessing;
19

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

26
/**
27
 * Data processor to iterate over an array (or other iterable) and, for each item, evaluate a
28
 * "variables" configuration block and/or run a nested "dataProcessing" chain against that item.
29
 *
30
 * Example:
31
 * ========
32
 *
33
 * tt_content.textpic = HANDLEBARSTEMPLATE
34
 * tt_content.textpic {
35
 *   templateName = @textpic
36
 *
37
 *   dataProcessing {
38
 *     10 = files
39
 *     10 {
40
 *       references.fieldName = image
41
 *       as = files
42
 *     }
43
 *
44
 *     20 = process-each
45
 *     20 {
46
 *       dataSource = processedData:files
47
 *       as = processedFiles
48
 *
49
 *       dataProcessing {
50
 *         10 = object-access
51
 *         10 {
52
 *           object = value
53
 *           path = publicUrl
54
 *           as = url
55
 *         }
56
 *       }
57
 *     }
58
 *   }
59
 * }
60
 *
61
 * Each file reference resolved by the core "files" processor is made available to the nested
62
 * "dataProcessing" chain as "value", so "object-access" can pull individual properties off it.
63
 * The per-item results are collected — keyed by the original array keys — under "processedFiles".
64
 *
65
 * @author Elias Häußler <e.haeussler@familie-redlich.de>
66
 * @license GPL-2.0-or-later
67
 */
68
#[DependencyInjection\Attribute\AutoconfigureTag('data.processor', ['identifier' => 'process-each'])]
69
final readonly class ProcessEachProcessor implements Frontend\ContentObject\DataProcessorInterface
70
{
71
    use DataSource\SupportsDataSourceAwareProcessing;
72

73
    public function __construct(
12✔
74
        private Frontend\ContentObject\ContentDataProcessor $contentDataProcessor,
75
        private DataSource\DataSourceProvider $dataSourceProvider,
76
        private Log\LoggerInterface $logger,
77
    ) {}
12✔
78

79
    /**
80
     * @param array<string, mixed> $contentObjectConfiguration
81
     * @param array<string, mixed> $processorConfiguration
82
     * @param array<string, mixed> $processedData
83
     * @return array<string, mixed>
84
     * @throws Exception\ReservedVariableCannotBeUsed
85
     * @throws Frontend\ContentObject\Exception\ContentRenderingException
86
     */
87
    public function process(
12✔
88
        Frontend\ContentObject\ContentObjectRenderer $cObj,
89
        array $contentObjectConfiguration,
90
        array $processorConfiguration,
91
        array $processedData,
92
    ): array {
93
        $collection = DataSource\DataSourceCollection::for(
12✔
94
            $cObj,
12✔
95
            $contentObjectConfiguration,
12✔
96
            $processorConfiguration,
12✔
97
            $processedData,
12✔
98
        );
12✔
99

100
        /** @var string $as */
101
        $as = $collection->resolve('as', DataSource\DataSource::ProcessorConfiguration, 'result');
12✔
102
        $variables = $collection->resolve('variables.', DataSource\DataSource::ProcessorConfiguration);
12✔
103
        $dataProcessing = $collection->resolve('dataProcessing.', DataSource\DataSource::ProcessorConfiguration);
12✔
104
        $data = null;
12✔
105

106
        try {
107
            $data = $this->dataSourceProvider->provide($collection);
12✔
108
        } catch (Exception\DataSourceIsMissingInCollection $exception) {
2✔
NEW
109
            $this->logger->warning(
×
NEW
110
                'No variables provided for data source "{source}" while processing {table}:{uid}.',
×
NEW
111
                [
×
NEW
112
                    'source' => $exception->dataSource->value,
×
NEW
113
                    'table' => $cObj->getCurrentTable(),
×
NEW
114
                    'uid' => $collection->resolveCurrentUid(),
×
NEW
115
                ],
×
NEW
116
            );
×
117
        } catch (Exception\DataSourceIsNotSupported $exception) {
2✔
118
            $this->logger->warning(
1✔
119
                'Invalid data source keyword "{source}" passed while processing {table}:{uid}.',
1✔
120
                [
1✔
121
                    'source' => $exception->dataSourceIdentifier,
1✔
122
                    'table' => $cObj->getCurrentTable(),
1✔
123
                    'uid' => $collection->resolveCurrentUid(),
1✔
124
                ],
1✔
125
            );
1✔
126
        } catch (Exception\PathIsMissingInDataSource $exception) {
1✔
127
            $this->logger->warning(
1✔
128
                'Invalid path "{path}" for data source "{source}" passed while processing {table}:{uid}.',
1✔
129
                [
1✔
130
                    'path' => $exception->path,
1✔
131
                    'source' => $exception->dataSource->value,
1✔
132
                    'table' => $cObj->getCurrentTable(),
1✔
133
                    'uid' => $collection->resolveCurrentUid(),
1✔
134
                ],
1✔
135
            );
1✔
136
        }
137

138
        // Early return if resolved variables are not iterable
139
        if (!is_iterable($data)) {
12✔
140
            return $processedData;
4✔
141
        }
142

143
        // Early return if neither "variables." nor "dataProcessing." are properly defined
144
        if (!is_array($variables) && !is_array($dataProcessing)) {
8✔
145
            $processedData[$as] = $data;
4✔
146

147
            return $processedData;
4✔
148
        }
149

150
        $processedVariables = [];
4✔
151
        $currentValue = $cObj->getCurrentVal();
4✔
152

153
        try {
154
            // Process each variable (both "variables." as well as "dataProcessing." are respected)
155
            foreach ($data as $key => $value) {
4✔
156
                $cObj->setCurrentVal($value);
4✔
157

158
                // Process "variables."
159
                if (is_array($variables)) {
4✔
160
                    $processed = Renderer\Variables\VariablesProcessor::for($cObj)->process($variables);
3✔
161
                } else {
162
                    $processed = [];
1✔
163
                }
164

165
                // Process "dataProcessing."
166
                if (is_array($dataProcessing)) {
3✔
167
                    $processed = $this->contentDataProcessor->process(
2✔
168
                        $cObj,
2✔
169
                        [
2✔
170
                            'dataProcessing.' => $dataProcessing,
2✔
171
                            'value' => $value,
2✔
172
                        ],
2✔
173
                        $processed,
2✔
174
                    );
2✔
175
                }
176

177
                $processedVariables[$key] = $processed;
3✔
178
            }
179
        } finally {
180
            // Restore current value
181
            $cObj->setCurrentVal($currentValue);
4✔
182
        }
183

184
        // Apply processed variables
185
        $processedData[$as] = $processedVariables;
3✔
186

187
        return $processedData;
3✔
188
    }
189
}
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