Passed
Push — master ( b8dac1...9827a7 )
by
unknown
24:45 queued 10:04
created

TcaSelectItems   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 297
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 43
eloc 135
dl 0
loc 297
rs 8.96
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
F groupAndSortItems() 0 69 16
B addInvalidItemsFromDatabase() 0 33 7
C addData() 0 96 11
A isTargetRenderType() 0 3 1
B sortItems() 0 34 8

How to fix   Complexity   

Complex Class

Complex classes like TcaSelectItems often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TcaSelectItems, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
17
18
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
19
use TYPO3\CMS\Core\Utility\GeneralUtility;
20
use TYPO3\CMS\Core\Utility\MathUtility;
21
22
/**
23
 * Resolve select items, set processed item list in processedTca, sanitize and resolve database field
24
 */
25
class TcaSelectItems extends AbstractItemProvider implements FormDataProviderInterface
26
{
27
    /**
28
     * Resolve select items
29
     *
30
     * @param array $result
31
     * @return array
32
     * @throws \UnexpectedValueException
33
     */
34
    public function addData(array $result)
35
    {
36
        $table = $result['tableName'];
37
38
        foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
39
            if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'select') {
40
                continue;
41
            }
42
43
            // Make sure we are only processing supported renderTypes
44
            if (!$this->isTargetRenderType($fieldConfig)) {
45
                continue;
46
            }
47
48
            $fieldConfig['config']['items'] = $this->sanitizeItemArray($fieldConfig['config']['items'] ?? [], $table, $fieldName);
49
50
            $fieldConfig['config']['maxitems'] = MathUtility::forceIntegerInRange($fieldConfig['config']['maxitems'] ?? 0, 0, 99999);
51
            if ($fieldConfig['config']['maxitems'] === 0) {
52
                $fieldConfig['config']['maxitems'] = 99999;
53
            }
54
55
            $fieldConfig['config']['items'] = $this->addItemsFromSpecial($result, $fieldName, $fieldConfig['config']['items']);
56
            $fieldConfig['config']['items'] = $this->addItemsFromFolder($result, $fieldName, $fieldConfig['config']['items']);
57
58
            $fieldConfig['config']['items'] = $this->addItemsFromForeignTable($result, $fieldName, $fieldConfig['config']['items']);
59
60
            // Resolve "itemsProcFunc"
61
            if (!empty($fieldConfig['config']['itemsProcFunc'])) {
62
                $fieldConfig['config']['items'] = $this->resolveItemProcessorFunction($result, $fieldName, $fieldConfig['config']['items']);
63
                // itemsProcFunc must not be used anymore
64
                unset($fieldConfig['config']['itemsProcFunc']);
65
            }
66
67
            // removing items before $dynamicItems and $removedItems have been built results in having them
68
            // not populated to the dynamic database row and displayed as "invalid value" in the forms view
69
            $fieldConfig['config']['items'] = $this->removeItemsByUserStorageRestriction($result, $fieldName, $fieldConfig['config']['items']);
70
71
            $removedItems = $fieldConfig['config']['items'];
72
73
            $fieldConfig['config']['items'] = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
74
            $fieldConfig['config']['items'] = $this->addItemsFromPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
75
            $fieldConfig['config']['items'] = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
76
77
            $fieldConfig['config']['items'] = $this->removeItemsByUserLanguageFieldRestriction($result, $fieldName, $fieldConfig['config']['items']);
78
            $fieldConfig['config']['items'] = $this->removeItemsByUserAuthMode($result, $fieldName, $fieldConfig['config']['items']);
79
            $fieldConfig['config']['items'] = $this->removeItemsByDoktypeUserRestriction($result, $fieldName, $fieldConfig['config']['items']);
80
81
            $removedItems = array_diff_key($removedItems, $fieldConfig['config']['items']);
82
83
            $currentDatabaseValuesArray = $this->processDatabaseFieldValue($result['databaseRow'], $fieldName);
84
            // Check if it's a new record to respect TCAdefaults
85
            if (!empty($fieldConfig['config']['MM']) && $result['command'] !== 'new') {
86
                // Getting the current database value on a mm relation doesn't make sense since the amount of selected
87
                // relations is stored in the field and not the uids of the items
88
                $currentDatabaseValuesArray = [];
89
            }
90
91
            $result['databaseRow'][$fieldName] = $currentDatabaseValuesArray;
92
93
            // add item values as keys to determine which items are stored in the database and should be preselected
94
            $itemArrayValues = array_column($fieldConfig['config']['items'], 1);
95
            $itemArray = array_fill_keys(
96
                $itemArrayValues,
97
                $fieldConfig['config']['items']
98
            );
99
            $result['databaseRow'][$fieldName] = $this->processSelectFieldValue($result, $fieldName, $itemArray);
100
101
            $fieldConfig['config']['items'] = $this->addInvalidItemsFromDatabase(
102
                $result,
103
                $table,
104
                $fieldName,
105
                $fieldConfig,
106
                $currentDatabaseValuesArray,
107
                $removedItems
108
            );
109
110
            // Translate labels and add icons
111
            // skip file of sys_file_metadata which is not rendered anyway but can use all memory
112
            if (!($table === 'sys_file_metadata' && $fieldName === 'file')) {
113
                $fieldConfig['config']['items'] = $this->translateLabels($result, $fieldConfig['config']['items'], $table, $fieldName);
114
                $fieldConfig['config']['items'] = $this->addIconFromAltIcons($result, $fieldConfig['config']['items'], $table, $fieldName);
115
            }
116
117
            // Keys may contain table names, so a numeric array is created
118
            $fieldConfig['config']['items'] = array_values($fieldConfig['config']['items']);
119
120
            $fieldConfig['config']['items'] = $this->groupAndSortItems(
121
                $fieldConfig['config']['items'],
122
                $fieldConfig['config']['itemGroups'] ?? [],
123
                $fieldConfig['config']['sortItems'] ?? []
124
            );
125
126
            $result['processedTca']['columns'][$fieldName] = $fieldConfig;
127
        }
128
129
        return $result;
130
    }
131
132
    /**
133
     * Add values that are currently listed in the database columns but not in the selectable items list
134
     * back to the list.
135
     *
136
     * @param array $result The current result array.
137
     * @param string $table The current table name
138
     * @param string $fieldName The current field name
139
     * @param array $fieldConf The configuration of the current field.
140
     * @param array $databaseValues The item values from the database, can contain invalid items!
141
     * @param array $removedItems Items removed by access checks and restrictions, must not be added as invalid values
142
     * @return array
143
     */
144
    public function addInvalidItemsFromDatabase(array $result, $table, $fieldName, array $fieldConf, array $databaseValues, array $removedItems)
145
    {
146
        // Early return if there are no items or invalid values should not be displayed
147
        if (empty($fieldConf['config']['items'])
148
            || $fieldConf['config']['renderType'] !== 'selectSingle'
149
            || ($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['disableNoMatchingValueElement'] ?? false)
150
            || ($fieldConf['config']['disableNoMatchingValueElement'] ?? false)
151
        ) {
152
            return $fieldConf['config']['items'];
153
        }
154
155
        $languageService = $this->getLanguageService();
156
        $noMatchingLabel = isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['noMatchingValue_label'])
157
            ? $languageService->sL(trim($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['noMatchingValue_label']))
158
            : '[ ' . $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue') . ' ]';
159
160
        $unmatchedValues = array_diff(
161
            array_values($databaseValues),
162
            array_column($fieldConf['config']['items'], 1),
163
            array_column($removedItems, 1)
164
        );
165
166
        foreach ($unmatchedValues as $unmatchedValue) {
167
            $invalidItem = [
168
                @sprintf($noMatchingLabel, $unmatchedValue),
169
                $unmatchedValue,
170
                null,
171
                'none' // put it in the very first position in the "none" group
172
            ];
173
            array_unshift($fieldConf['config']['items'], $invalidItem);
174
        }
175
176
        return $fieldConf['config']['items'];
177
    }
178
179
    /**
180
     * Determines whether the current field is a valid target for this DataProvider
181
     *
182
     * @param array $fieldConfig
183
     * @return bool
184
     */
185
    protected function isTargetRenderType(array $fieldConfig)
186
    {
187
        return $fieldConfig['config']['renderType'] !== 'selectTree';
188
    }
189
190
    /**
191
     * Is used when --div-- elements in the item list are used, or if groups are defined via "groupItems" config array.
192
     *
193
     * This method takes the --div-- elements out of the list, and adds them to the group lists.
194
     *
195
     * A main "none" group is added, which is always on top, when items are not set to be in a group.
196
     * All items without a groupId - which is defined by the fourth key of an item in the item array - are added
197
     * to the "none" group, or to the last group used previously, to ensure ordering as much as possible as before.
198
     *
199
     * Then the found groups are iterated over the order in the [itemGroups] list,
200
     * and items within a group can be sorted via "sortOrders" configuration.
201
     *
202
     * All grouped items are then "flattened" out and --div-- items are added for each group to keep backwards-compatibility.
203
     *
204
     * @param array $allItems all resolved items including the ones from foreign_table values. The group ID information can be found in fourth key [3] of an item.
205
     * @param array $definedGroups [config][itemGroups]
206
     * @param array $sortOrders [config][sortOrders]
207
     * @return array
208
     */
209
    protected function groupAndSortItems(array $allItems, array $definedGroups, array $sortOrders): array
210
    {
211
        $groupedItems = [];
212
        // Append defined groups at first, as their order is prioritized
213
        $itemGroups = ['none' => ''];
214
        foreach ($definedGroups as $groupId => $groupLabel) {
215
            $itemGroups[$groupId] = $this->getLanguageService()->sL($groupLabel);
216
        }
217
        $currentGroup = 'none';
218
        // Extract --div-- into itemGroups
219
        foreach ($allItems as $key => $item) {
220
            if ($item[1] === '--div--') {
221
                // A divider is added as a group (existing groups will get their label overridden)
222
                if (isset($item[3])) {
223
                    $currentGroup = $item[3];
224
                    $itemGroups[$currentGroup] = $item[0];
225
                } else {
226
                    $currentGroup = 'none';
227
                }
228
                continue;
229
            }
230
            // Put the given item in the currentGroup if no group has been given already
231
            if (!isset($item[3])) {
232
                $item[3] = $currentGroup;
233
            }
234
            $groupIdOfItem = !empty($item[3]) ? $item[3] : 'none';
235
            // It is still possible to have items that have an "unassigned" group, so they are moved to the "none" group
236
            if (!isset($itemGroups[$groupIdOfItem])) {
237
                $itemGroups[$groupIdOfItem] = '';
238
            }
239
240
            // Put the item in its corresponding group (and create it if it does not exist yet)
241
            if (!is_array($groupedItems[$groupIdOfItem] ?? null)) {
242
                $groupedItems[$groupIdOfItem] = [];
243
            }
244
            $groupedItems[$groupIdOfItem][] = $item;
245
        }
246
        // Only "none" = no grouping used explicitly via "itemGroups" or via "--div--"
247
        if (count($itemGroups) === 1) {
248
            if (!empty($sortOrders)) {
249
                $allItems = $this->sortItems($allItems, $sortOrders);
250
            }
251
            return $allItems;
252
        }
253
254
        // $groupedItems contains all items per group
255
        // $itemGroups contains all groups in order of each group
256
257
        // Let's add the --div-- items again ("unpacking")
258
        // And use the group ordering given by the itemGroups
259
        $finalItems = [];
260
        foreach ($itemGroups as $groupId => $groupLabel) {
261
            $itemsInGroup = $groupedItems[$groupId] ?? [];
262
            if (empty($itemsInGroup)) {
263
                continue;
264
            }
265
            // If sorting is defined, sort within each group now
266
            if (!empty($sortOrders)) {
267
                $itemsInGroup = $this->sortItems($itemsInGroup, $sortOrders);
268
            }
269
            // Add the --div-- if it is not the "none" default item
270
            if ($groupId !== 'none') {
271
                // Fall back to the groupId, if there is no label for it
272
                $groupLabel = $groupLabel ?: $groupId;
273
                $finalItems[] = [$groupLabel, '--div--', null, $groupId, null];
274
            }
275
            $finalItems = array_merge($finalItems, $itemsInGroup);
276
        }
277
        return $finalItems;
278
    }
279
280
    /**
281
     * Sort given items by label or value or a custom user function built like
282
     * "MyVendor\MyExtension\TcaSorter->sortItems" or a callable.
283
     *
284
     * @param array $items
285
     * @param array $sortOrders should be something like like [label => desc]
286
     * @return array the sorted items
287
     */
288
    protected function sortItems(array $items, array $sortOrders): array
289
    {
290
        foreach ($sortOrders as $order => $direction) {
291
            switch ($order) {
292
                case 'label':
293
                    $direction = strtolower($direction);
294
                    @usort(
295
                        $items,
296
                        function ($item1, $item2) use ($direction) {
297
                            if ($direction === 'desc') {
298
                                return (strcasecmp($item1[0], $item2[0]) <= 0) ? 1 : 0;
299
                            }
300
                            return strcasecmp($item1[0], $item2[0]);
301
                        }
302
                    );
303
                    break;
304
                case 'value':
305
                    $direction = strtolower($direction);
306
                    @usort(
307
                        $items,
308
                        function ($item1, $item2) use ($direction) {
309
                            if ($direction === 'desc') {
310
                                return (strcasecmp($item1[1], $item2[1]) <= 0) ? 1 : 0;
311
                            }
312
                            return strcasecmp($item1[1], $item2[1]);
313
                        }
314
                    );
315
                    break;
316
                default:
317
                    $reference = null;
318
                    GeneralUtility::callUserFunction($direction, $items, $reference);
319
            }
320
        }
321
        return $items;
322
    }
323
}
324