[BUGFIX] Improve Performance for Inline Elements
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / FormDataProvider / TcaSelectItems.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form\FormDataProvider;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
18 use TYPO3\CMS\Core\Utility\MathUtility;
19
20 /**
21 * Resolve select items, set processed item list in processedTca, sanitize and resolve database field
22 */
23 class TcaSelectItems extends AbstractItemProvider implements FormDataProviderInterface
24 {
25 /**
26 * Resolve select items
27 *
28 * @param array $result
29 * @return array
30 * @throws \UnexpectedValueException
31 */
32 public function addData(array $result)
33 {
34 $table = $result['tableName'];
35
36 foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
37 if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'select') {
38 continue;
39 }
40
41 // Make sure we are only processing supported renderTypes
42 if (!$this->isTargetRenderType($fieldConfig)) {
43 continue;
44 }
45
46 // Make sure we only process useful dataFields
47 if ($this->isSkippableInlineField($result, $fieldName) || $this->isSkippableLanguageField($result, $fieldName)) {
48 continue;
49 }
50
51 $fieldConfig['config']['items'] = $this->sanitizeItemArray($fieldConfig['config']['items'] ?? [], $table, $fieldName);
52
53 // Resolve "itemsProcFunc"
54 if (!empty($fieldConfig['config']['itemsProcFunc'])) {
55 $fieldConfig['config']['items'] = $this->resolveItemProcessorFunction($result, $fieldName, $fieldConfig['config']['items']);
56 // itemsProcFunc must not be used anymore
57 unset($fieldConfig['config']['itemsProcFunc']);
58 }
59
60 $fieldConfig['config']['maxitems'] = MathUtility::forceIntegerInRange($fieldConfig['config']['maxitems'] ?? 0, 0, 99999);
61 if ($fieldConfig['config']['maxitems'] === 0) {
62 $fieldConfig['config']['maxitems'] = 99999;
63 }
64
65 $fieldConfig['config']['items'] = $this->addItemsFromSpecial($result, $fieldName, $fieldConfig['config']['items']);
66 $fieldConfig['config']['items'] = $this->addItemsFromFolder($result, $fieldName, $fieldConfig['config']['items']);
67 $staticItems = $fieldConfig['config']['items'];
68
69 $fieldConfig['config']['items'] = $this->addItemsFromForeignTable($result, $fieldName, $fieldConfig['config']['items']);
70 // removing items before $dynamicItems and $removedItems have been built results in having them
71 // not populated to the dynamic database row and displayed as "invalid value" in the forms view
72 $fieldConfig['config']['items'] = $this->removeItemsByUserStorageRestriction($result, $fieldName, $fieldConfig['config']['items']);
73
74 $dynamicItems = array_diff_key($fieldConfig['config']['items'], $staticItems);
75 $removedItems = $fieldConfig['config']['items'];
76
77 $fieldConfig['config']['items'] = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
78 $fieldConfig['config']['items'] = $this->addItemsFromPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
79 $fieldConfig['config']['items'] = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
80
81 $fieldConfig['config']['items'] = $this->removeItemsByUserLanguageFieldRestriction($result, $fieldName, $fieldConfig['config']['items']);
82 $fieldConfig['config']['items'] = $this->removeItemsByUserAuthMode($result, $fieldName, $fieldConfig['config']['items']);
83 $fieldConfig['config']['items'] = $this->removeItemsByDoktypeUserRestriction($result, $fieldName, $fieldConfig['config']['items']);
84
85 $removedItems = array_diff_key($removedItems, $fieldConfig['config']['items']);
86
87 // needed to determine the items for invalid values
88 $currentDatabaseValuesArray = $this->processDatabaseFieldValue($result['databaseRow'], $fieldName);
89 $result['databaseRow'][$fieldName] = $currentDatabaseValuesArray;
90
91 $staticValues = $this->getStaticValues($fieldConfig['config']['items'], $dynamicItems);
92 $result['databaseRow'][$fieldName] = $this->processSelectFieldValue($result, $fieldName, $staticValues);
93
94 $fieldConfig['config']['items'] = $this->addInvalidItemsFromDatabase(
95 $result,
96 $table,
97 $fieldName,
98 $fieldConfig,
99 $currentDatabaseValuesArray,
100 $removedItems
101 );
102
103 // Translate labels
104 $fieldConfig['config']['items'] = $this->translateLabels($result, $fieldConfig['config']['items'], $table, $fieldName);
105
106 // Keys may contain table names, so a numeric array is created
107 $fieldConfig['config']['items'] = array_values($fieldConfig['config']['items']);
108
109 $result['processedTca']['columns'][$fieldName] = $fieldConfig;
110 }
111
112 return $result;
113 }
114
115 /**
116 * Checks if the field is an inlineChild and not not exposed
117 *
118 * @param array $result The current result array
119 * @param string $fieldName Current handle field name
120 * @return bool
121 */
122 protected function isSkippableInlineField(array $result, string $fieldName): bool
123 {
124 // is inline record
125 if (empty($result['isInlineChild'])) {
126 return false;
127 }
128 // skip data loading for inline children if they are not visible
129 if (empty($result['isInlineChildExpanded']) && empty($result['isInlineAjaxOpeningContext'])) {
130 return true;
131 }
132
133 // skip data loading for inline children if there are the parent relation fields
134 $inlineParentConfig = $result['inlineParentConfig'];
135 if ($inlineParentConfig && isset($inlineParentConfig['foreign_field']) && $inlineParentConfig['foreign_field'] === $fieldName) {
136 return true;
137 }
138
139 return false;
140 }
141
142 /**
143 * If the current form shows only the default language record, processing of language parent field can be skipped
144 *
145 * @param array $result The current result array
146 * @param string $fieldName Field name being processed
147 * @return bool
148 */
149 protected function isSkippableLanguageField(array $result, string $fieldName): bool
150 {
151 // look for a translation for this record although we're on the default language (0)
152 $transOrigPointerFieldName = $result['processedTca']['ctrl']['transOrigPointerField'] ?? '';
153 if (empty($transOrigPointerFieldName) || $fieldName !== $transOrigPointerFieldName) {
154 return false;
155 }
156
157 // get TCA language field name
158 $languageField = $result['processedTca']['ctrl']['languageField'];
159 $languageUids = $result['databaseRow'][$languageField];
160
161 // languageField can be an array or a scalar value (try to normalize it)
162 if (!is_array($languageUids)) {
163 $languageUids = [(int)$result['databaseRow'][$languageField]];
164 }
165
166 // only default language available?
167 return count($languageUids) === 1 && (int)$languageUids[0] === 0;
168 }
169
170 /**
171 * Add values that are currently listed in the database columns but not in the selectable items list
172 * back to the list.
173 *
174 * @param array $result The current result array.
175 * @param string $table The current table name
176 * @param string $fieldName The current field name
177 * @param array $fieldConf The configuration of the current field.
178 * @param array $databaseValues The item values from the database, can contain invalid items!
179 * @param array $removedItems Items removed by access checks and restrictions, must not be added as invalid values
180 * @return array
181 */
182 public function addInvalidItemsFromDatabase(array $result, $table, $fieldName, array $fieldConf, array $databaseValues, array $removedItems)
183 {
184 // Early return if there are no items or invalid values should not be displayed
185 if (empty($fieldConf['config']['items'])
186 || $fieldConf['config']['renderType'] !== 'selectSingle'
187 || ($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['disableNoMatchingValueElement'] ?? false)
188 || ($fieldConf['config']['disableNoMatchingValueElement'] ?? false)
189 ) {
190 return $fieldConf['config']['items'];
191 }
192
193 $languageService = $this->getLanguageService();
194 $noMatchingLabel = isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['noMatchingValue_label'])
195 ? $languageService->sL(trim($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['noMatchingValue_label']))
196 : '[ ' . $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue') . ' ]';
197
198 $unmatchedValues = array_diff(
199 array_values($databaseValues),
200 array_column($fieldConf['config']['items'], 1),
201 array_column($removedItems, 1)
202 );
203
204 foreach ($unmatchedValues as $unmatchedValue) {
205 $invalidItem = [
206 @sprintf($noMatchingLabel, $unmatchedValue),
207 $unmatchedValue
208 ];
209 array_unshift($fieldConf['config']['items'], $invalidItem);
210 }
211
212 return $fieldConf['config']['items'];
213 }
214
215 /**
216 * Determines whether the current field is a valid target for this DataProvider
217 *
218 * @param array $fieldConfig
219 * @return bool
220 */
221 protected function isTargetRenderType(array $fieldConfig)
222 {
223 return $fieldConfig['config']['renderType'] !== 'selectTree';
224 }
225 }