TYPO3  7.6
TcaInline.php
Go to the documentation of this file.
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 
27 
32 {
39  public function addData(array $result)
40  {
41  $result = $this->addInlineFirstPid($result);
42 
43  foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
44  if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'inline') {
45  continue;
46  }
47  $result['processedTca']['columns'][$fieldName]['children'] = [];
48  if ($result['inlineResolveExistingChildren']) {
49  $result = $this->resolveRelatedRecords($result, $fieldName);
50  $result = $this->addForeignSelectorAndUniquePossibleRecords($result, $fieldName);
51  }
52  }
53 
54  return $result;
55  }
56 
65  protected function addInlineFirstPid(array $result)
66  {
67  if (is_null($result['inlineFirstPid'])) {
68  $table = $result['tableName'];
69  $row = $result['databaseRow'];
70  // If the parent is a page, use the uid(!) of the (new?) page as pid for the child records:
71  if ($table == 'pages') {
72  $liveVersionId = BackendUtility::getLiveVersionIdOfRecord('pages', $row['uid']);
73  $pid = is_null($liveVersionId) ? $row['uid'] : $liveVersionId;
74  } elseif ($row['pid'] < 0) {
75  $prevRec = BackendUtility::getRecord($table, abs($row['pid']));
76  $pid = $prevRec['pid'];
77  } else {
78  $pid = $row['pid'];
79  }
80  $result['inlineFirstPid'] = (int)$pid;
81  }
82  return $result;
83  }
84 
93  protected function resolveRelatedRecords(array $result, $fieldName)
94  {
95  $childTableName = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'];
96 
97  // localizationMode is either "none", "keep" or "select":
98  // * none: Handled parent row is not a localized record, or if it is a localized row, this is ignored.
99  // Default language records and overlays have distinct children that are not connected to each other.
100  // * keep: Handled parent row is a localized record, but child table is either not localizable, or
101  // "keep" is explicitly set. A localized parent and its default language row share the same
102  // children records. Editing a child from a localized record will change this record for the
103  // default language row, too.
104  // * select: Handled parent row is a localized record, child table is localizable. Children records are
105  // localized overlays of a default language record. Three scenarios can happen:
106  // ** Localized child overlay and its default language row exist - show localized overlay record
107  // ** Default child language row exists but child overlay doesn't - show a "synchronize this record" button
108  // ** Localized child overlay exists but default language row does not - this dangling child is a data inconsistency
109 
110  // Mode was prepared by TcaInlineConfiguration provider
111  $mode = $result['processedTca']['columns'][$fieldName]['config']['behaviour']['localizationMode'];
112  if ($mode === 'none') {
113  $connectedUids = [];
114  // A new record that has distinct children can not have children yet, fetch connected uids for existing only
115  if ($result['command'] === 'edit') {
116  $connectedUids = $this->resolveConnectedRecordUids(
117  $result['processedTca']['columns'][$fieldName]['config'],
118  $result['tableName'],
119  $result['databaseRow']['uid'],
120  $result['databaseRow'][$fieldName]
121  );
122  }
123  $result['databaseRow'][$fieldName] = implode(',', $connectedUids);
124  $connectedUids = $this->getWorkspacedUids($connectedUids, $childTableName);
125  // @todo: If inlineCompileExistingChildren must be kept, it might be better to change the data
126  // @todo: format of databaseRow for this field and separate the child compilation to an own provider?
127  if ($result['inlineCompileExistingChildren']) {
128  foreach ($connectedUids as $childUid) {
129  $result['processedTca']['columns'][$fieldName]['children'][] = $this->compileChild($result, $fieldName, $childUid);
130  }
131  }
132  } elseif ($mode === 'keep') {
133  // Fetch connected uids of default language record
134  $connectedUids = $this->resolveConnectedRecordUids(
135  $result['processedTca']['columns'][$fieldName]['config'],
136  $result['tableName'],
137  $result['defaultLanguageRow']['uid'],
138  $result['defaultLanguageRow'][$fieldName]
139  );
140  $result['databaseRow'][$fieldName] = implode(',', $connectedUids);
141  $connectedUids = $this->getWorkspacedUids($connectedUids, $childTableName);
142  if ($result['inlineCompileExistingChildren']) {
143  foreach ($connectedUids as $childUid) {
144  $result['processedTca']['columns'][$fieldName]['children'][] = $this->compileChild($result, $fieldName, $childUid);
145  }
146  }
147  } else {
148  $connectedUidsOfLocalizedOverlay = [];
149  if ($result['command'] === 'edit') {
150  $connectedUidsOfLocalizedOverlay = $this->resolveConnectedRecordUids(
151  $result['processedTca']['columns'][$fieldName]['config'],
152  $result['tableName'],
153  $result['databaseRow']['uid'],
154  $result['databaseRow'][$fieldName]
155  );
156  }
157  $result['databaseRow'][$fieldName] = implode(',', $connectedUidsOfLocalizedOverlay);
158  if ($result['inlineCompileExistingChildren']) {
159  $connectedUidsOfDefaultLanguageRecord = $this->resolveConnectedRecordUids(
160  $result['processedTca']['columns'][$fieldName]['config'],
161  $result['tableName'],
162  $result['defaultLanguageRow']['uid'],
163  $result['defaultLanguageRow'][$fieldName]
164  );
165 
166  $showPossible = $result['processedTca']['columns'][$fieldName]['config']['appearance']['showPossibleLocalizationRecords'];
167 
168  // Find which records are localized, which records are not localized and which are
169  // localized but miss default language record
170  $fieldNameWithDefaultLanguageUid = $GLOBALS['TCA'][$childTableName]['ctrl']['transOrigPointerField'];
171  foreach ($connectedUidsOfLocalizedOverlay as $localizedUid) {
172  $localizedRecord = $this->getRecordFromDatabase($childTableName, $localizedUid);
173  $uidOfDefaultLanguageRecord = $localizedRecord[$fieldNameWithDefaultLanguageUid];
174  if (in_array($uidOfDefaultLanguageRecord, $connectedUidsOfDefaultLanguageRecord)) {
175  // This localized child has a default language record. Remove this record from list of default language records
176  $connectedUidsOfDefaultLanguageRecord = array_diff($connectedUidsOfDefaultLanguageRecord, array($uidOfDefaultLanguageRecord));
177  // Compile localized record
178  $compiledChild = $this->compileChild($result, $fieldName, $localizedUid);
179  $result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild;
180  }
181  }
182  if ($showPossible) {
183  foreach ($connectedUidsOfDefaultLanguageRecord as $defaultLanguageUid) {
184  // If there are still uids in $connectedUidsOfDefaultLanguageRecord, these are records that
185  // exist in default language, but are not localized yet. Compile and mark those
186  $compiledChild = $this->compileChild($result, $fieldName, $defaultLanguageUid);
187  $compiledChild['isInlineDefaultLanguageRecordInLocalizedParentContext'] = true;
188  $result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild;
189  }
190  }
191  }
192  }
193 
194  return $result;
195  }
196 
206  protected function addForeignSelectorAndUniquePossibleRecords(array $result, $fieldName)
207  {
208  if (!is_array($result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'])) {
209  return $result;
210  }
211 
212  $selectorOrUniqueConfiguration = $result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'];
213  $foreignFieldName = $selectorOrUniqueConfiguration['fieldName'];
214  $selectorOrUniquePossibleRecords = [];
215 
216  if ($selectorOrUniqueConfiguration['config']['type'] === 'select') {
217  // Compile child table data for this field only
218  $selectDataInput = [
219  'tableName' => $result['processedTca']['columns'][$fieldName]['config']['foreign_table'],
220  'command' => 'new',
221  // Since there is no existing record that may have a type, it does not make sense to
222  // do extra handling of pageTsConfig merged here. Just provide "parent" pageTS as is
223  'pageTsConfig' => $result['pageTsConfig'],
224  'userTsConfig' => $result['userTsConfig'],
225  'processedTca' => [
226  'ctrl' => [],
227  'columns' => [
228  $foreignFieldName => [
229  'config' => $selectorOrUniqueConfiguration['config'],
230  ],
231  ],
232  ],
233  'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
234  ];
236  $formDataGroup = GeneralUtility::makeInstance(OnTheFly::class);
237  $formDataGroup->setProviderList([ TcaSelectItems::class ]);
239  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
240  $compilerResult = $formDataCompiler->compile($selectDataInput);
241  $selectorOrUniquePossibleRecords = $compilerResult['processedTca']['columns'][$foreignFieldName]['config']['items'];
242  }
243 
244  $result['processedTca']['columns'][$fieldName]['config']['selectorOrUniquePossibleRecords'] = $selectorOrUniquePossibleRecords;
245 
246  return $result;
247  }
248 
257  protected function compileChild(array $result, $parentFieldName, $childUid)
258  {
259  $parentConfig = $result['processedTca']['columns'][$parentFieldName]['config'];
260  $childTableName = $parentConfig['foreign_table'];
261 
263  $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
264  $inlineStackProcessor->initializeByGivenStructure($result['inlineStructure']);
265  $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
266 
268  $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
270  $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
271  $formDataCompilerInput = [
272  'command' => 'edit',
273  'tableName' => $childTableName,
274  'vanillaUid' => (int)$childUid,
275  'isInlineChild' => true,
276  'inlineStructure' => $result['inlineStructure'],
277  'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
278  'inlineFirstPid' => $result['inlineFirstPid'],
279  'inlineParentConfig' => $parentConfig,
280 
281  // values of the current parent element
282  // it is always a string either an id or new...
283  'inlineParentUid' => $result['databaseRow']['uid'],
284  'inlineParentTableName' => $result['tableName'],
285  'inlineParentFieldName' => $parentFieldName,
286 
287  // values of the top most parent element set on first level and not overridden on following levels
288  'inlineTopMostParentUid' => $result['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'],
289  'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'],
290  'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'],
291  ];
292 
293  // For foreign_selector with useCombination $mainChild is the mm record
294  // and $combinationChild is the child-child. For "normal" relations, $mainChild
295  // is just the normal child record and $combinationChild is empty.
296  $mainChild = $formDataCompiler->compile($formDataCompilerInput);
297  if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
298  $mainChild['combinationChild'] = $this->compileCombinationChild($mainChild, $parentConfig);
299  }
300  return $mainChild;
301  }
302 
311  protected function compileCombinationChild(array $intermediate, array $parentConfig)
312  {
313  // foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already
314  $intermediateUid = $intermediate['databaseRow'][$parentConfig['foreign_selector']][0];
315  $combinationChild = $this->compileChild($intermediate, $parentConfig['foreign_selector'], $intermediateUid);
316  return $combinationChild;
317  }
318 
326  protected function getWorkspacedUids(array $connectedUids, $childTableName)
327  {
328  $backendUser = $this->getBackendUser();
329  $newConnectedUids = [];
330  foreach ($connectedUids as $uid) {
331  // Fetch workspace version of a record (if any):
332  // @todo: Needs handling
333  if ($backendUser->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($childTableName)) {
334  $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($backendUser->workspace, $childTableName, $uid, 'uid,t3ver_state');
335  if ($workspaceVersion !== false) {
336  $versionState = VersionState::cast($workspaceVersion['t3ver_state']);
337  if ($versionState->equals(VersionState::DELETE_PLACEHOLDER)) {
338  return [];
339  }
340  $uid = $workspaceVersion['uid'];
341  }
342  }
343  $newConnectedUids[] = $uid;
344  }
345  return $newConnectedUids;
346  }
347 
358  protected function resolveConnectedRecordUids(array $parentConfig, $parentTableName, $parentUid, $parentFieldValue)
359  {
360  $directlyConnectedIds = GeneralUtility::trimExplode(',', $parentFieldValue);
361  if (empty($parentConfig['MM'])) {
362  $parentUid = $this->getLiveDefaultId($parentTableName, $parentUid);
363  }
365  $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
366  $relationHandler->registerNonTableValues = (bool)$parentConfig['allowedIdValues'];
367  $relationHandler->start($parentFieldValue, $parentConfig['foreign_table'], $parentConfig['MM'], $parentUid, $parentTableName, $parentConfig);
368  $foreignRecordUids = $relationHandler->getValueArray();
369  $resolvedForeignRecordUids = [];
370  foreach ($foreignRecordUids as $aForeignRecordUid) {
371  if ($parentConfig['MM'] || $parentConfig['foreign_field']) {
372  $resolvedForeignRecordUids[] = (int)$aForeignRecordUid;
373  } else {
374  foreach ($directlyConnectedIds as $id) {
375  if ((int)$aForeignRecordUid === (int)$id) {
376  $resolvedForeignRecordUids[] = (int)$aForeignRecordUid;
377  }
378  }
379  }
380  }
381  return $resolvedForeignRecordUids;
382  }
383 
393  protected function getLiveDefaultId($tableName, $uid)
394  {
395  $liveDefaultId = BackendUtility::getLiveVersionIdOfRecord($tableName, $uid);
396  if ($liveDefaultId === null) {
397  $liveDefaultId = $uid;
398  }
399  return $liveDefaultId;
400  }
401 
405  protected function getBackendUser()
406  {
407  return $GLOBALS['BE_USER'];
408  }
409 }