[TASK] FormEngine: Simplify element name resolving
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / FormDataTraverser.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form;
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\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Utility\GeneralUtility;
19
20 /**
21 * Utility class for traversing related fields in the TCA.
22 *
23 * @author Sebastian Fischer <typo3@evoweb.de>
24 * @author Alexander Stehlik <astehlik.deleteme@intera.de>
25 */
26 class FormDataTraverser {
27
28 /**
29 * If this value is set during traversal and the traversal chain can
30 * not be walked to the end this value will be returned instead.
31 *
32 * @var string
33 */
34 protected $alternativeFieldValue;
35
36 /**
37 * If this is TRUE the alternative field value will be used even if
38 * the detected field value is not empty.
39 *
40 * @var bool
41 */
42 protected $forceAlternativeFieldValueUse = FALSE;
43
44 /**
45 * The row data of the record that is currently traversed.
46 *
47 * @var array
48 */
49 protected $currentRow;
50
51 /**
52 * Name of the table that is currently traversed.
53 *
54 * @var string
55 */
56 protected $currentTable;
57
58 /**
59 * If the first record in the chain is translatable the language
60 * UID of this record is stored in this variable.
61 *
62 * @var int
63 */
64 protected $originalLanguageUid = NULL;
65
66 /**
67 * Inline first pid
68 *
69 * @var integer
70 */
71 protected $inlineFirstPid;
72
73 /**
74 * Traverses the array of given field names by using the TCA.
75 *
76 * @param array $fieldNameArray The field names that should be traversed.
77 * @param string $tableName The starting table name.
78 * @param array $row The starting record row.
79 * @param int $inlineFirstPid Inline first pid
80 * @return mixed The value of the last field in the chain.
81 */
82 public function getTraversedFieldValue(array $fieldNameArray, $tableName, array $row, $inlineFirstPid) {
83 $this->currentTable = $tableName;
84 $this->currentRow = $row;
85 $this->inlineFirstPid = $inlineFirstPid;
86 $fieldValue = '';
87 if (count($fieldNameArray) > 0) {
88 $this->initializeOriginalLanguageUid();
89 $fieldValue = $this->getFieldValueRecursive($fieldNameArray);
90 }
91 return $fieldValue;
92 }
93
94 /**
95 * Checks if the current table is translatable and initializes the
96 * originalLanguageUid with the value of the languageField of the
97 * current row.
98 *
99 * @return void
100 */
101 protected function initializeOriginalLanguageUid() {
102 $fieldCtrlConfig = $GLOBALS['TCA'][$this->currentTable]['ctrl'];
103 if (!empty($fieldCtrlConfig['languageField']) && isset($this->currentRow[$fieldCtrlConfig['languageField']])) {
104 $this->originalLanguageUid = (int)$this->currentRow[$fieldCtrlConfig['languageField']];
105 } else {
106 $this->originalLanguageUid = FALSE;
107 }
108 }
109
110 /**
111 * Traverses the fields in the $fieldNameArray and tries to read
112 * the field values.
113 *
114 * @param array $fieldNameArray The field names that should be traversed.
115 * @return mixed The value of the last field.
116 */
117 protected function getFieldValueRecursive(array $fieldNameArray) {
118 $value = '';
119
120 foreach ($fieldNameArray as $fieldName) {
121 // Skip if a defined field was actually not present in the database row
122 // Using array_key_exists here, since TYPO3 supports NULL values as well
123 if (!array_key_exists($fieldName, $this->currentRow)) {
124 $value = '';
125 break;
126 }
127
128 $value = $this->currentRow[$fieldName];
129 if (empty($value)) {
130 break;
131 }
132
133 $this->currentRow = $this->getRelatedRecordRow($fieldName, $value);
134 if ($this->currentRow === FALSE) {
135 break;
136 }
137 }
138
139 if ((empty($value) || $this->forceAlternativeFieldValueUse) && !empty($this->alternativeFieldValue)) {
140 $value = $this->alternativeFieldValue;
141 }
142
143 return $value;
144 }
145
146 /**
147 * Tries to read the related record from the database depending on
148 * the TCA. Supported types are group (db), select and inline.
149 *
150 * @param string $fieldName The name of the field of which the related record should be fetched.
151 * @param string $value The current field value.
152 * @return array|boolean The related row if it could be found otherwise FALSE.
153 */
154 protected function getRelatedRecordRow($fieldName, $value) {
155 $fieldConfig = $GLOBALS['TCA'][$this->currentTable]['columns'][$fieldName]['config'];
156 $possibleUids = array();
157
158 switch ($fieldConfig['type']) {
159 case 'group':
160 $possibleUids = $this->getRelatedGroupFieldUids($fieldConfig, $value);
161 break;
162 case 'select':
163 // @todo: This misuses SelectElement and should be solved differently
164 /** @var $selectObject \TYPO3\CMS\Backend\Form\Element\SelectElement */
165 $selectObject = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\Element\SelectElement::class);
166 $selectObject->setCurrentRow($this->currentRow);
167 $selectObject->setCurrentTable($this->currentTable);
168 $selectObject->setAlternativeFieldValue($this->alternativeFieldValue);
169 $selectObject->setForceAlternativeFieldValueUse($this->forceAlternativeFieldValueUse);
170 $possibleUids = $selectObject->getRelatedSelectFieldUids($fieldConfig, $fieldName, $value);
171 $this->alternativeFieldValue = $selectObject->getAlternativeFieldValue();
172 $this->forceAlternativeFieldValueUse = $selectObject->isForceAlternativeFieldValueUse();
173 break;
174 case 'inline':
175 $possibleUids = $this->getRelatedInlineFieldUids($fieldConfig, $fieldName, $value);
176 break;
177 }
178
179 $relatedRow = FALSE;
180 if (count($possibleUids) === 1) {
181 $relatedRow = $this->getRecordRow($possibleUids[0]);
182 } elseif (count($possibleUids) > 1) {
183 $relatedRow = $this->getMatchingRecordRowByTranslation($possibleUids, $fieldConfig);
184 }
185
186 return $relatedRow;
187 }
188
189 /**
190 * Tries to get the related UIDs of a group field.
191 *
192 * @param array $fieldConfig "config" section from the TCA for the current field.
193 * @param string $value The current value (normally a comma separated record list, possibly consisting of multiple parts [table]_[uid]|[title]).
194 * @return array Array of related UIDs.
195 */
196 protected function getRelatedGroupFieldUids(array $fieldConfig, $value) {
197 $relatedUids = array();
198 $allowedTable = $this->getAllowedTableForGroupField($fieldConfig);
199
200 if (($fieldConfig['internal_type'] !== 'db') || ($allowedTable === FALSE)) {
201 return $relatedUids;
202 }
203
204 $values = GeneralUtility::trimExplode(',', $value, TRUE);
205 foreach ($values as $groupValue) {
206 list($foreignIdentifier, $foreignTitle) = GeneralUtility::trimExplode('|', $groupValue);
207 list($recordForeignTable, $foreignUid) = BackendUtility::splitTable_Uid($foreignIdentifier);
208 // skip records that do not match the allowed table
209 if (!empty($recordForeignTable) && ($recordForeignTable !== $allowedTable)) {
210 continue;
211 }
212 if (!empty($foreignTitle)) {
213 $this->alternativeFieldValue = rawurldecode($foreignTitle);
214 }
215 $relatedUids[] = $foreignUid;
216 }
217
218 if (count($relatedUids) > 0) {
219 $this->currentTable = $allowedTable;
220 }
221
222 return $relatedUids;
223 }
224
225 /**
226 * Tries to get the related UID of an inline field.
227 *
228 * @param array $fieldConfig "config" section of the TCA configuration of the related inline field.
229 * @param string $fieldName The name of the inline field.
230 * @param string $value The value in the local table (normally a comma separated list of the inline record UIDs).
231 * @return array Array of related UIDs.
232 */
233 protected function getRelatedInlineFieldUids(array $fieldConfig, $fieldName, $value) {
234 $relatedUids = array();
235
236 $PA = array('itemFormElValue' => $value);
237 $inlineRelatedRecordResolver = GeneralUtility::makeInstance(InlineRelatedRecordResolver::class);
238 $items = $inlineRelatedRecordResolver->getRelatedRecords($this->currentTable, $fieldName, $this->currentRow, $PA, $fieldConfig, $this->inlineFirstPid);
239 if ($items['count'] > 0) {
240 $this->currentTable = $fieldConfig['foreign_table'];
241 foreach ($items['records'] as $inlineRecord) {
242 $relatedUids[] = $inlineRecord['uid'];
243 }
244 }
245
246 return $relatedUids;
247 }
248
249 /**
250 * Will read the "allowed" value from the given field configuration
251 * and returns FALSE if none was defined or more than one.
252 *
253 * If exactly one table was defined the name of that table is returned.
254 *
255 * @param array $fieldConfig "config" section of a group field from the TCA.
256 * @return bool|string FALSE if none ore more than one table was found, otherwise the name of the table.
257 */
258 protected function getAllowedTableForGroupField(array $fieldConfig) {
259 $allowedTable = FALSE;
260
261 $allowedTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed'], TRUE);
262 if (count($allowedTables) === 1) {
263 $allowedTable = $allowedTables[0];
264 }
265
266 return $allowedTable;
267 }
268
269 /**
270 * Uses the DataPreprocessor to read a value from the database.
271 *
272 * The table name is read from the currentTable class variable.
273 *
274 * @param int $uid The UID of the record that should be fetched.
275 * @return array|boolean FALSE if the record can not be accessed, otherwise the data of the requested record.
276 */
277 protected function getRecordRow($uid) {
278 /** @var \TYPO3\CMS\Backend\Form\DataPreprocessor $dataPreprocessor */
279 $dataPreprocessor = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\DataPreprocessor::class);
280 $dataPreprocessor->fetchRecord($this->currentTable, $uid, '');
281 return reset($dataPreprocessor->regTableItems_data);
282 }
283
284 /**
285 * Tries to get the correct record based on the parent translation by
286 * traversing all given related UIDs and checking if their language UID
287 * matches the stored original language UID.
288 *
289 * If exactly one match was found for the original language the resulting
290 * row is returned, otherwise FALSE.
291 *
292 * @param array $relatedUids All possible matching UIDs.
293 * @return bool|array The row data if a matching record was found, FALSE otherwise.
294 */
295 protected function getMatchingRecordRowByTranslation(array $relatedUids) {
296 if ($this->originalLanguageUid === FALSE) {
297 return FALSE;
298 }
299
300 $fieldCtrlConfig = $GLOBALS['TCA'][$this->currentTable]['ctrl'];
301 if (empty($fieldCtrlConfig['languageField'])) {
302 return FALSE;
303 }
304
305 $languageField = $fieldCtrlConfig['languageField'];
306 $foundRows = array();
307 foreach ($relatedUids as $relatedUid) {
308 $currentRow = $this->getRecordRow($relatedUid);
309 if (!isset($currentRow[$languageField])) {
310 continue;
311 }
312 if ((int)$currentRow[$languageField] === $this->originalLanguageUid) {
313 $foundRows[] = $currentRow;
314 }
315 }
316
317 $relatedRow = FALSE;
318 if (count($foundRows) === 1) {
319 $relatedRow = $foundRows[0];
320 }
321
322 return $relatedRow;
323 }
324
325 }