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

FluidTYPO3 / flux / 27757015719

18 Jun 2026 11:42AM UTC coverage: 89.162% (-3.5%) from 92.646%
27757015719

Pull #2288

github

web-flow
Merge f2162927f into 2614049c6
Pull Request #2288: [FEATURE] Prepare for v14 support

210 of 348 new or added lines in 56 files covered. (60.34%)

121 existing lines in 9 files now uncovered.

6228 of 6985 relevant lines covered (89.16%)

40.84 hits per line

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

89.69
/Classes/Form/AbstractMultiValueFormField.php
1
<?php
2
namespace FluidTYPO3\Flux\Form;
3

4
/*
5
 * This file is part of the FluidTYPO3/Flux project under GPLv2 or later.
6
 *
7
 * For the full copyright and license information, please read the
8
 * LICENSE.md file that was distributed with this source code.
9
 */
10

11
use FluidTYPO3\Flux\Service\TypoScriptService;
12
use TYPO3\CMS\Core\Utility\GeneralUtility;
13
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
14
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
15
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
16

17
abstract class AbstractMultiValueFormField extends AbstractFormField implements MultiValueFieldInterface
18
{
19
    protected int $size = 1;
20
    protected bool $multiple = false;
21
    protected int $minItems = 0;
22
    protected ?int $maxItems = null;
23
    protected ?string $itemListStyle = '';
24
    protected ?string $selectedListStyle = '';
25

26
    /**
27
     * Special rendering type of this component - supports all values normally
28
     * supported by TCA of the "select" field type.
29
     *
30
     * @see https://docs.typo3.org/typo3cms/TCAReference/Reference/Columns/Select/Index.html#rendertype
31
     */
32
    protected ?string $renderType = 'selectSingle';
33

34
    /**
35
     * Mixed - string (CSV), Traversable or array of items. Format of key/value
36
     * pairs is also optional. For single-dim arrays, key becomes option value
37
     * and each member value becomes label. For multidim/Traversable each member
38
     * is inspected; if it is a raw value it is used for both value and label,
39
     * if it is a scalar value the first item is used as value and the second
40
     * as label.
41
     *
42
     * @var mixed
43
     */
44
    protected $items = null;
45

46
    /**
47
     * @see https://docs.typo3.org/typo3cms/TCAReference/ColumnsConfig/Type/Select.html#itemsprocfunc
48
     */
49
    protected ?string $itemsProcFunc = null;
50

51
    /**
52
     * If not-FALSE, adds one empty option/value pair to the generated selector
53
     * box and tries to use this property's value (cast to string) as label.
54
     * Can also be an array of [$value, $label, $iconName] where label and icon
55
     * name are optional - use this when you need to specify an icon for "empty".
56
     *
57
     * @var boolean|string|array
58
     */
59
    protected $emptyOption = false;
60

61
    /**
62
     * If set to TRUE, Flux will attempt to translate the LLL labels of items
63
     * provided as CSV values, e.g. items "foo,bar" would try to resolve LLL
64
     * values for "LLL:EXT:myext/Resources/Private/Languages/locallang.xlf:foo"
65
     * and "LLL:EXT:myext/Resources/Private/Languages/locallang.xlf:bar" to be
66
     * used as value labels.
67
     */
68
    protected bool $translateCsvItems = false;
69

70
    public function prepareConfiguration(string $type): array
71
    {
72
        $configuration = parent::prepareConfiguration($type);
84✔
73
        $configuration['size'] = $this->getSize();
84✔
74
        $configuration['maxitems'] = $this->getMaxItems();
84✔
75
        $configuration['minitems'] = $this->getMinItems();
84✔
76
        $configuration['multiple'] = $this->getMultiple();
84✔
77
        $configuration['itemListStyle'] = $this->getItemListStyle();
84✔
78
        $configuration['selectedListStyle'] = $this->getSelectedListStyle();
84✔
79
        $configuration['renderType'] = $this->getRenderType();
84✔
80
        $configuration['items'] = $this->getItems();
84✔
81
        $configuration['itemsProcFunc'] = $this->getItemsProcFunc();
84✔
82
        return $configuration;
84✔
83
    }
84

85
    public function setSize(int $size): self
86
    {
87
        $this->size = $size;
108✔
88
        return $this;
108✔
89
    }
90

91
    public function getSize(): int
92
    {
93
        return $this->size;
84✔
94
    }
95

96
    public function setMultiple(bool $multiple): self
97
    {
98
        $this->multiple = $multiple;
108✔
99
        return $this;
108✔
100
    }
101

102
    public function getMultiple(): bool
103
    {
104
        return $this->multiple;
84✔
105
    }
106

107
    public function setMaxItems(int $maxItems): self
108
    {
109
        $this->maxItems = $maxItems;
144✔
110
        return $this;
144✔
111
    }
112

113
    public function getMaxItems(): ?int
114
    {
115
        return $this->maxItems;
88✔
116
    }
117

118
    public function setMinItems(int $minItems): self
119
    {
120
        $this->minItems = $minItems;
144✔
121
        return $this;
144✔
122
    }
123

124
    public function getMinItems(): int
125
    {
126
        return $this->minItems;
88✔
127
    }
128

129
    public function setItemListStyle(?string $itemListStyle): self
130
    {
131
        $this->itemListStyle = $itemListStyle;
144✔
132
        return $this;
144✔
133
    }
134

135
    public function getItemListStyle(): ?string
136
    {
137
        return $this->itemListStyle;
88✔
138
    }
139

140
    public function setSelectedListStyle(?string $selectedListStyle): self
141
    {
142
        $this->selectedListStyle = $selectedListStyle;
144✔
143
        return $this;
144✔
144
    }
145

146
    public function getSelectedListStyle(): ?string
147
    {
148
        return $this->selectedListStyle;
88✔
149
    }
150

151
    public function getRenderType(): ?string
152
    {
153
        return $this->renderType;
84✔
154
    }
155

156
    public function setRenderType(?string $renderType): self
157
    {
158
        $this->renderType = $renderType;
8✔
159
        return $this;
8✔
160
    }
161

162
    public function getTranslateCsvItems(): bool
163
    {
164
        return $this->translateCsvItems;
20✔
165
    }
166

167
    public function setTranslateCsvItems(bool $translateCsvItems): self
168
    {
169
        $this->translateCsvItems = $translateCsvItems;
112✔
170
        return $this;
112✔
171
    }
172

173
    /**
174
     * @param array|string $items
175
     */
176
    public function setItems($items): self
177
    {
178
        $this->items = $items;
148✔
179
        return $this;
148✔
180
    }
181

182
    public function getItems(): array
183
    {
184
        $items = [];
116✔
185
        if (true === $this->items instanceof QueryInterface) {
116✔
UNCOV
186
            $items = $this->addOptionsFromResults($this->items);
×
187
        } elseif (true === is_string($this->items)) {
116✔
188
            if (false !== strpos($this->items, '..')) {
20✔
189
                [$low, $high] = explode('..', $this->items);
4✔
190
                $itemNames = range($low, $high, 1);
4✔
191
            } else {
192
                $itemNames = GeneralUtility::trimExplode(',', $this->items);
16✔
193
            }
194
            if (!$this->getTranslateCsvItems()) {
20✔
195
                foreach ($itemNames as $itemName) {
20✔
196
                    $items[] = [
20✔
197
                        'label' => $itemName,
20✔
198
                        'value' => $itemName,
20✔
199
                    ];
20✔
200
                }
201
            } else {
202
                foreach ($itemNames as $itemName) {
4✔
203
                    $resolvedLabel = $this->resolveLocalLanguageValueOfLabel(
4✔
204
                        '',
4✔
205
                        $this->getPath() . '.option.' . $itemName
4✔
206
                    );
4✔
207
                    $items[] = [
4✔
208
                        'label' => $resolvedLabel,
4✔
209
                        'value' => $itemName,
4✔
210
                    ];
4✔
211
                }
212
            }
213
        } elseif (true === is_array($this->items) || true === $this->items instanceof \Traversable) {
96✔
214
            foreach ($this->items as $itemIndex => $itemValue) {
16✔
215
                if (true === is_array($itemValue) || true === $itemValue instanceof \ArrayObject) {
16✔
216
                    $items[] = $itemValue;
8✔
217
                } else {
218
                    $items[] = [
8✔
219
                        'label' => $itemValue,
8✔
220
                        'value' => $itemIndex,
8✔
221
                    ];
8✔
222
                }
223
            }
224
        }
225
        $emptyOption = $this->getEmptyOption();
116✔
226
        if (false !== $emptyOption) {
116✔
227
            if (is_array($emptyOption)) {
8✔
228
                $items[] = $emptyOption;
4✔
229
            } else {
230
                $items[] = [
4✔
231
                    'label' => $emptyOption,
4✔
232
                    'value' => '',
4✔
233
                ];
4✔
234
            }
235
        }
236

237
        return $items;
116✔
238
    }
239

240
    public function setItemsProcFunc(?string $itemsProcFunc): self
241
    {
242
        $this->itemsProcFunc = $itemsProcFunc;
108✔
243
        return $this;
108✔
244
    }
245

246
    public function getItemsProcFunc(): ?string
247
    {
248
        return $this->itemsProcFunc;
84✔
249
    }
250

251
    /**
252
     * @param boolean|string|array $emptyOption
253
     */
254
    public function setEmptyOption($emptyOption): self
255
    {
256
        $this->emptyOption = $emptyOption;
152✔
257
        return $this;
152✔
258
    }
259

260
    /**
261
     * @return boolean|string|array
262
     */
263
    public function getEmptyOption()
264
    {
265
        return $this->emptyOption;
120✔
266
    }
267

268
    protected function getLabelPropertyName(string $table, string $type): string
269
    {
270
        $path = sprintf('config.tx_extbase.persistence.classes.%s.mapping.tableName', $type);
4✔
271
        $mappedTable = $this->getTypoScriptService()->getTypoScriptByPath($path);
4✔
272
        $labelField = $GLOBALS['TCA'][$mappedTable ?: $table]['ctrl']['label'];
4✔
273
        $propertyName = GeneralUtility::underscoredToLowerCamelCase($labelField);
4✔
274
        return $propertyName;
4✔
275
    }
276

277
    protected function addOptionsFromResults(QueryInterface $query): array
278
    {
UNCOV
279
        $items = [];
×
UNCOV
280
        $results = $query->execute();
×
UNCOV
281
        $type = $query->getType();
×
UNCOV
282
        $table = strtolower(str_replace('\\', '_', $type));
×
UNCOV
283
        $propertyName = $this->getLabelPropertyName($table, $type);
×
284
        /** @var DomainObjectInterface $result */
UNCOV
285
        foreach ($results as $result) {
×
UNCOV
286
            $uid = $result->getUid();
×
UNCOV
287
            array_push($items, [ObjectAccess::getProperty($result, $propertyName), $uid]);
×
288
        }
UNCOV
289
        return $items;
×
290
    }
291

292
    /**
293
     * @codeCoverageIgnore
294
     */
295
    protected function getTypoScriptService(): TypoScriptService
296
    {
297
        /** @var TypoScriptService $typoScriptService */
298
        $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
299
        return $typoScriptService;
300
    }
301
}
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