| Total Complexity | 41 |
| Total Lines | 297 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
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 |
||
| 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 |
||
| 322 | } |
||
| 323 | } |
||
| 324 |