Total Complexity | 57 |
Total Lines | 478 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Complex classes like TcaInline 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 TcaInline, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
37 | class TcaInline extends AbstractDatabaseRecordProvider implements FormDataProviderInterface |
||
38 | { |
||
39 | /** |
||
40 | * Resolve inline fields |
||
41 | * |
||
42 | * @param array $result |
||
43 | * @return array |
||
44 | */ |
||
45 | public function addData(array $result) |
||
46 | { |
||
47 | $result = $this->addInlineFirstPid($result); |
||
48 | |||
49 | foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) { |
||
50 | if (!$this->isInlineField($fieldConfig)) { |
||
51 | continue; |
||
52 | } |
||
53 | $result['processedTca']['columns'][$fieldName]['children'] = []; |
||
54 | if (!$this->isUserAllowedToModify($fieldConfig)) { |
||
55 | continue; |
||
56 | } |
||
57 | if ($result['inlineResolveExistingChildren']) { |
||
58 | $result = $this->resolveRelatedRecords($result, $fieldName); |
||
59 | $result = $this->addForeignSelectorAndUniquePossibleRecords($result, $fieldName); |
||
60 | } |
||
61 | } |
||
62 | |||
63 | return $result; |
||
64 | } |
||
65 | |||
66 | /** |
||
67 | * Is column of type "inline" |
||
68 | * |
||
69 | * @param array $fieldConfig |
||
70 | * @return bool |
||
71 | */ |
||
72 | protected function isInlineField($fieldConfig) |
||
73 | { |
||
74 | return !empty($fieldConfig['config']['type']) && $fieldConfig['config']['type'] === 'inline'; |
||
75 | } |
||
76 | |||
77 | /** |
||
78 | * Is user allowed to modify child elements |
||
79 | * |
||
80 | * @param array $fieldConfig |
||
81 | * @return bool |
||
82 | */ |
||
83 | protected function isUserAllowedToModify($fieldConfig) |
||
84 | { |
||
85 | return $this->getBackendUser()->check('tables_modify', $fieldConfig['config']['foreign_table']); |
||
86 | } |
||
87 | |||
88 | /** |
||
89 | * The "entry" pid for inline records. Nested inline records can potentially hang around on different |
||
90 | * pid's, but the entry pid is needed for AJAX calls, so that they would know where the action takes place on the page structure. |
||
91 | * |
||
92 | * @param array $result Incoming result |
||
93 | * @return array Modified result |
||
94 | * @todo: Find out when and if this is different from 'effectivePid' |
||
95 | */ |
||
96 | protected function addInlineFirstPid(array $result) |
||
97 | { |
||
98 | if ($result['inlineFirstPid'] === null) { |
||
99 | $table = $result['tableName']; |
||
100 | $row = $result['databaseRow']; |
||
101 | // If the parent is a page, use the uid(!) of the (new?) page as pid for the child records: |
||
102 | if ($table === 'pages') { |
||
103 | $liveVersionId = BackendUtility::getLiveVersionIdOfRecord('pages', $row['uid']); |
||
104 | $pid = $liveVersionId ?? $row['uid']; |
||
105 | } elseif (($row['pid'] ?? 0) < 0) { |
||
106 | $prevRec = BackendUtility::getRecord($table, (int)abs($row['pid'])); |
||
107 | $pid = $prevRec['pid']; |
||
108 | } else { |
||
109 | $pid = $row['pid'] ?? 0; |
||
110 | } |
||
111 | if (MathUtility::canBeInterpretedAsInteger($pid)) { |
||
112 | $pageRecord = BackendUtility::getRecord('pages', (int)$pid); |
||
113 | if (($pageRecord[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'] ?? null] ?? 0) > 0) { |
||
114 | $pid = (int)$pageRecord[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']]; |
||
115 | } |
||
116 | } elseif (strpos($pid, 'NEW') !== 0) { |
||
117 | throw new \RuntimeException( |
||
118 | 'inlineFirstPid should either be an integer or a "NEW..." string', |
||
119 | 1521220142 |
||
120 | ); |
||
121 | } |
||
122 | $result['inlineFirstPid'] = $pid; |
||
123 | } |
||
124 | return $result; |
||
125 | } |
||
126 | |||
127 | /** |
||
128 | * Substitute the value in databaseRow of this inline field with an array |
||
129 | * that contains the databaseRows of currently connected records and some meta information. |
||
130 | * |
||
131 | * @param array $result Result array |
||
132 | * @param string $fieldName Current handle field name |
||
133 | * @return array Modified item array |
||
134 | */ |
||
135 | protected function resolveRelatedRecordsOverlays(array $result, $fieldName) |
||
214 | } |
||
215 | |||
216 | /** |
||
217 | * Substitute the value in databaseRow of this inline field with an array |
||
218 | * that contains the databaseRows of currently connected records and some meta information. |
||
219 | * |
||
220 | * @param array $result Result array |
||
221 | * @param string $fieldName Current handle field name |
||
222 | * @return array Modified item array |
||
223 | */ |
||
224 | protected function resolveRelatedRecords(array $result, $fieldName) |
||
225 | { |
||
226 | if ($result['defaultLanguageRow'] !== null) { |
||
227 | return $this->resolveRelatedRecordsOverlays($result, $fieldName); |
||
228 | } |
||
229 | |||
230 | $childTableName = $result['processedTca']['columns'][$fieldName]['config']['foreign_table']; |
||
231 | $connectedUidsOfDefaultLanguageRecord = $this->resolveConnectedRecordUids( |
||
232 | $result['processedTca']['columns'][$fieldName]['config'], |
||
233 | $result['tableName'], |
||
234 | $result['databaseRow']['uid'], |
||
235 | $result['databaseRow'][$fieldName] |
||
236 | ); |
||
237 | $result['databaseRow'][$fieldName] = implode(',', $connectedUidsOfDefaultLanguageRecord); |
||
238 | |||
239 | $connectedUidsOfDefaultLanguageRecord = $this->getWorkspacedUids($connectedUidsOfDefaultLanguageRecord, $childTableName); |
||
240 | |||
241 | if ($result['inlineCompileExistingChildren']) { |
||
242 | foreach ($connectedUidsOfDefaultLanguageRecord as $uid) { |
||
243 | try { |
||
244 | $compiledChild = $this->compileChild($result, $fieldName, $uid); |
||
245 | $result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild; |
||
246 | } catch (DatabaseRecordException $e) { |
||
247 | // Nothing to do here, missing child is just not being rendered. |
||
248 | } |
||
249 | } |
||
250 | } |
||
251 | return $result; |
||
252 | } |
||
253 | |||
254 | /** |
||
255 | * If there is a foreign_selector or foreign_unique configuration, fetch |
||
256 | * the list of possible records that can be connected and attach the to the |
||
257 | * inline configuration. |
||
258 | * |
||
259 | * @param array $result Result array |
||
260 | * @param string $fieldName Current handle field name |
||
261 | * @return array Modified item array |
||
262 | */ |
||
263 | protected function addForeignSelectorAndUniquePossibleRecords(array $result, $fieldName) |
||
264 | { |
||
265 | if (!is_array($result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'] ?? null)) { |
||
266 | return $result; |
||
267 | } |
||
268 | |||
269 | $selectorOrUniqueConfiguration = $result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration']; |
||
270 | $foreignFieldName = $selectorOrUniqueConfiguration['fieldName']; |
||
271 | $selectorOrUniquePossibleRecords = []; |
||
272 | |||
273 | if ($selectorOrUniqueConfiguration['config']['type'] === 'select') { |
||
274 | // Compile child table data for this field only |
||
275 | $selectDataInput = [ |
||
276 | 'tableName' => $result['processedTca']['columns'][$fieldName]['config']['foreign_table'], |
||
277 | 'command' => 'new', |
||
278 | // Since there is no existing record that may have a type, it does not make sense to |
||
279 | // do extra handling of pageTsConfig merged here. Just provide "parent" pageTS as is |
||
280 | 'pageTsConfig' => $result['pageTsConfig'], |
||
281 | 'userTsConfig' => $result['userTsConfig'], |
||
282 | 'databaseRow' => $result['databaseRow'], |
||
283 | 'processedTca' => [ |
||
284 | 'ctrl' => [], |
||
285 | 'columns' => [ |
||
286 | $foreignFieldName => [ |
||
287 | 'config' => $selectorOrUniqueConfiguration['config'], |
||
288 | ], |
||
289 | ], |
||
290 | ], |
||
291 | 'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'], |
||
292 | ]; |
||
293 | /** @var OnTheFly $formDataGroup */ |
||
294 | $formDataGroup = GeneralUtility::makeInstance(OnTheFly::class); |
||
295 | $formDataGroup->setProviderList([TcaSelectItems::class]); |
||
296 | /** @var FormDataCompiler $formDataCompiler */ |
||
297 | $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup); |
||
298 | $compilerResult = $formDataCompiler->compile($selectDataInput); |
||
299 | $selectorOrUniquePossibleRecords = $compilerResult['processedTca']['columns'][$foreignFieldName]['config']['items']; |
||
300 | } |
||
301 | |||
302 | $result['processedTca']['columns'][$fieldName]['config']['selectorOrUniquePossibleRecords'] = $selectorOrUniquePossibleRecords; |
||
303 | |||
304 | return $result; |
||
305 | } |
||
306 | |||
307 | /** |
||
308 | * Compile a full child record |
||
309 | * |
||
310 | * @param array $result Result array of parent |
||
311 | * @param string $parentFieldName Name of parent field |
||
312 | * @param int $childUid Uid of child to compile |
||
313 | * @return array Full result array |
||
314 | */ |
||
315 | protected function compileChild(array $result, $parentFieldName, $childUid) |
||
316 | { |
||
317 | $parentConfig = $result['processedTca']['columns'][$parentFieldName]['config']; |
||
318 | $childTableName = $parentConfig['foreign_table']; |
||
319 | |||
320 | /** @var InlineStackProcessor $inlineStackProcessor */ |
||
321 | $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class); |
||
322 | $inlineStackProcessor->initializeByGivenStructure($result['inlineStructure']); |
||
323 | $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0) ?: []; |
||
324 | |||
325 | /** @var TcaDatabaseRecord $formDataGroup */ |
||
326 | $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class); |
||
327 | /** @var FormDataCompiler $formDataCompiler */ |
||
328 | $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup); |
||
329 | $formDataCompilerInput = [ |
||
330 | 'command' => 'edit', |
||
331 | 'tableName' => $childTableName, |
||
332 | 'vanillaUid' => (int)$childUid, |
||
333 | // Give incoming returnUrl down to children so they generate a returnUrl back to |
||
334 | // the originally opening record, also see "originalReturnUrl" in inline container |
||
335 | // and FormInlineAjaxController |
||
336 | 'returnUrl' => $result['returnUrl'], |
||
337 | 'isInlineChild' => true, |
||
338 | 'inlineStructure' => $result['inlineStructure'], |
||
339 | 'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'], |
||
340 | 'inlineFirstPid' => $result['inlineFirstPid'], |
||
341 | 'inlineParentConfig' => $parentConfig, |
||
342 | |||
343 | // values of the current parent element |
||
344 | // it is always a string either an id or new... |
||
345 | 'inlineParentUid' => $result['databaseRow']['uid'], |
||
346 | 'inlineParentTableName' => $result['tableName'], |
||
347 | 'inlineParentFieldName' => $parentFieldName, |
||
348 | |||
349 | // values of the top most parent element set on first level and not overridden on following levels |
||
350 | 'inlineTopMostParentUid' => $result['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'] ?? '', |
||
351 | 'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'] ?? '', |
||
352 | 'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'] ?? '', |
||
353 | ]; |
||
354 | |||
355 | // For foreign_selector with useCombination $mainChild is the mm record |
||
356 | // and $combinationChild is the child-child. For 1:n "normal" relations, |
||
357 | // $mainChild is just the normal child record and $combinationChild is empty. |
||
358 | $mainChild = $formDataCompiler->compile($formDataCompilerInput); |
||
359 | if (($parentConfig['foreign_selector'] ?? false) && ($parentConfig['appearance']['useCombination'] ?? false)) { |
||
360 | try { |
||
361 | $mainChild['combinationChild'] = $this->compileChildChild($mainChild, $parentConfig); |
||
362 | } catch (DatabaseRecordException $e) { |
||
363 | // The child could not be compiled, probably it was deleted and a dangling mm record |
||
364 | // exists. This is a data inconsistency, we catch this exception and create a flash message |
||
365 | $message = vsprintf( |
||
366 | $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:formEngine.databaseRecordErrorInlineChildChild'), |
||
367 | [$e->getTableName(), $e->getUid(), $childTableName, (int)$childUid] |
||
368 | ); |
||
369 | $flashMessage = GeneralUtility::makeInstance( |
||
370 | FlashMessage::class, |
||
371 | $message, |
||
372 | '', |
||
373 | FlashMessage::ERROR |
||
374 | ); |
||
375 | GeneralUtility::makeInstance(FlashMessageService::class)->getMessageQueueByIdentifier()->enqueue($flashMessage); |
||
376 | } |
||
377 | } |
||
378 | return $mainChild; |
||
379 | } |
||
380 | |||
381 | /** |
||
382 | * With useCombination set, not only content of the intermediate table, but also |
||
383 | * the connected child should be rendered in one go. Prepare this here. |
||
384 | * |
||
385 | * @param array $child Full data array of "mm" record |
||
386 | * @param array $parentConfig TCA configuration of "parent" |
||
387 | * @return array Full data array of child |
||
388 | */ |
||
389 | protected function compileChildChild(array $child, array $parentConfig) |
||
390 | { |
||
391 | // foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already |
||
392 | $childChildUid = $child['databaseRow'][$parentConfig['foreign_selector']][0]; |
||
393 | // child-child table name is set in child tca "the selector field" foreign_table |
||
394 | $childChildTableName = $child['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table']; |
||
395 | /** @var TcaDatabaseRecord $formDataGroup */ |
||
396 | $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class); |
||
397 | /** @var FormDataCompiler $formDataCompiler */ |
||
398 | $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup); |
||
399 | |||
400 | $formDataCompilerInput = [ |
||
401 | 'command' => 'edit', |
||
402 | 'tableName' => $childChildTableName, |
||
403 | 'vanillaUid' => (int)$childChildUid, |
||
404 | 'isInlineChild' => true, |
||
405 | 'isInlineChildExpanded' => $child['isInlineChildExpanded'], |
||
406 | // @todo: this is the wrong inline structure, isn't it? Shouldn't it contain the part from child child, too? |
||
407 | 'inlineStructure' => $child['inlineStructure'], |
||
408 | 'inlineFirstPid' => $child['inlineFirstPid'], |
||
409 | // values of the top most parent element set on first level and not overridden on following levels |
||
410 | 'inlineTopMostParentUid' => $child['inlineTopMostParentUid'], |
||
411 | 'inlineTopMostParentTableName' => $child['inlineTopMostParentTableName'], |
||
412 | 'inlineTopMostParentFieldName' => $child['inlineTopMostParentFieldName'], |
||
413 | ]; |
||
414 | $childChild = $formDataCompiler->compile($formDataCompilerInput); |
||
415 | return $childChild; |
||
416 | } |
||
417 | |||
418 | /** |
||
419 | * Substitute given list of uids in child table with workspace uid if needed |
||
420 | * |
||
421 | * @param array $connectedUids List of connected uids |
||
422 | * @param string $childTableName Name of child table |
||
423 | * @return array List of uids in workspace |
||
424 | */ |
||
425 | protected function getWorkspacedUids(array $connectedUids, $childTableName) |
||
426 | { |
||
427 | $backendUser = $this->getBackendUser(); |
||
428 | $newConnectedUids = []; |
||
429 | foreach ($connectedUids as $uid) { |
||
430 | // Fetch workspace version of a record (if any): |
||
431 | // @todo: Needs handling |
||
432 | if ($backendUser->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($childTableName)) { |
||
433 | $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($backendUser->workspace, $childTableName, $uid, 'uid,t3ver_state'); |
||
434 | if (!empty($workspaceVersion)) { |
||
435 | $versionState = VersionState::cast($workspaceVersion['t3ver_state']); |
||
436 | if ($versionState->equals(VersionState::DELETE_PLACEHOLDER)) { |
||
437 | continue; |
||
438 | } |
||
439 | $uid = $workspaceVersion['uid']; |
||
440 | } |
||
441 | } |
||
442 | $newConnectedUids[] = $uid; |
||
443 | } |
||
444 | return $newConnectedUids; |
||
445 | } |
||
446 | |||
447 | /** |
||
448 | * Use RelationHandler to resolve connected uids. |
||
449 | * |
||
450 | * @param array $parentConfig TCA config section of parent |
||
451 | * @param string $parentTableName Name of parent table |
||
452 | * @param int $parentUid Uid of parent record |
||
453 | * @param string $parentFieldValue Database value of parent record of this inline field |
||
454 | * @return array Array with connected uids |
||
455 | * @todo: Cover with unit tests |
||
456 | */ |
||
457 | protected function resolveConnectedRecordUids(array $parentConfig, $parentTableName, $parentUid, $parentFieldValue) |
||
458 | { |
||
459 | $directlyConnectedIds = GeneralUtility::trimExplode(',', $parentFieldValue); |
||
460 | if (empty($parentConfig['MM'])) { |
||
461 | $parentUid = $this->getLiveDefaultId($parentTableName, $parentUid); |
||
462 | } |
||
463 | /** @var RelationHandler $relationHandler */ |
||
464 | $relationHandler = GeneralUtility::makeInstance(RelationHandler::class); |
||
465 | $relationHandler->registerNonTableValues = (bool)($parentConfig['allowedIdValues'] ?? false); |
||
466 | $relationHandler->start($parentFieldValue, $parentConfig['foreign_table'] ?? '', $parentConfig['MM'] ?? '', $parentUid, $parentTableName, $parentConfig); |
||
467 | $foreignRecordUids = $relationHandler->getValueArray(); |
||
468 | $resolvedForeignRecordUids = []; |
||
469 | foreach ($foreignRecordUids as $aForeignRecordUid) { |
||
470 | if ($parentConfig['MM'] ?? $parentConfig['foreign_field'] ?? false) { |
||
471 | $resolvedForeignRecordUids[] = (int)$aForeignRecordUid; |
||
472 | } else { |
||
473 | foreach ($directlyConnectedIds as $id) { |
||
474 | if ((int)$aForeignRecordUid === (int)$id) { |
||
475 | $resolvedForeignRecordUids[] = (int)$aForeignRecordUid; |
||
476 | } |
||
477 | } |
||
478 | } |
||
479 | } |
||
480 | return $resolvedForeignRecordUids; |
||
481 | } |
||
482 | |||
483 | /** |
||
484 | * Gets the record uid of the live default record. If already |
||
485 | * pointing to the live record, the submitted record uid is returned. |
||
486 | * |
||
487 | * @param string $tableName |
||
488 | * @param int $uid |
||
489 | * @return int |
||
490 | * @todo: the workspace mess still must be resolved somehow |
||
491 | */ |
||
492 | protected function getLiveDefaultId($tableName, $uid) |
||
493 | { |
||
494 | $liveDefaultId = BackendUtility::getLiveVersionIdOfRecord($tableName, $uid); |
||
495 | if ($liveDefaultId === null) { |
||
496 | $liveDefaultId = $uid; |
||
497 | } |
||
498 | return $liveDefaultId; |
||
499 | } |
||
500 | |||
501 | /** |
||
502 | * @return BackendUserAuthentication |
||
503 | */ |
||
504 | protected function getBackendUser() |
||
505 | { |
||
506 | return $GLOBALS['BE_USER']; |
||
507 | } |
||
508 | |||
509 | /** |
||
510 | * @return LanguageService |
||
511 | */ |
||
512 | protected function getLanguageService() |
||
515 | } |
||
516 | } |
||
517 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.