88 return GeneralUtility::makeInstance(
101 $this->allDataMap = $dataMap;
102 $this->modifiedDataMap = $dataMap;
116 while (!empty($this->modifiedDataMap)) {
117 $this->nextItems = [];
118 foreach ($this->modifiedDataMap as $tableName => $idValues) {
122 $this->modifiedDataMap = [];
123 if (empty($this->nextItems)) {
127 if ($iterations++ === 0) {
130 $this->
enrich($this->nextItems);
145 foreach ($dataMap as $tableName => $idValues) {
146 foreach ($idValues as $id => $values) {
147 if (empty($values)) {
148 unset($dataMap[$tableName][$id]);
151 if (empty($dataMap[$tableName])) {
152 unset($dataMap[$tableName]);
164 protected function collectItems(
string $tableName, array $idValues)
172 'l10n_state' =>
'l10n_state',
173 'language' =>
$GLOBALS[
'TCA'][$tableName][
'ctrl'][
'languageField'],
174 'parent' =>
$GLOBALS[
'TCA'][$tableName][
'ctrl'][
'transOrigPointerField'],
176 if (!empty(
$GLOBALS[
'TCA'][$tableName][
'ctrl'][
'translationSource'])) {
177 $fieldNames[
'source'] =
$GLOBALS[
'TCA'][$tableName][
'ctrl'][
'translationSource'];
194 foreach ($idValues as $id => $values) {
195 $item = $this->
findItem($tableName, $id);
197 if ($item ===
null) {
198 $recordValues = $translationValues[$id] ?? [];
208 if ($item->getLanguage() === -1) {
213 if ($item->getLanguage() > 0 && empty($item->getParent())) {
218 if (!empty($dependencies[$id])) {
219 $item->setDependencies($dependencies[$id]);
233 protected function sanitize(array $items)
235 foreach ([
'directChild',
'grandChild'] as $type) {
247 protected function enrich(array $items)
249 foreach ([
'directChild',
'grandChild'] as $type) {
251 foreach ($item->getApplicableScopes() as $scope) {
252 $fromId = $item->getIdForScope($scope);
276 $fieldNames = array_merge(
282 $fieldNameMap = array_combine($fieldNames, $fieldNames);
304 if (empty($fieldNames)) {
308 $fieldNameList =
'uid,' . implode(
',', $fieldNames);
310 $fromRecord = [
'uid' => $fromId];
313 $item->getTableName(),
320 if (!$item->isNew()) {
322 $item->getTableName(),
328 if (is_array($fromRecord) && is_array($forRecord)) {
329 foreach ($fieldNames as $fieldName) {
349 foreach ($item->findDependencies($scope) as $dependentItem) {
351 $suggestedDependentItem = $this->
findItem(
352 $dependentItem->getTableName(),
353 $dependentItem->getId()
355 if ($suggestedDependentItem !==
null) {
356 $dependentItem = $suggestedDependentItem;
406 $fromId = $fromRecord[
'uid'];
409 $fromValue = $this->allDataMap[$item->
getTableName()][$fromId][$fieldName];
410 } elseif (array_key_exists($fieldName, $fromRecord)) {
412 $fromValue = $fromRecord[$fieldName];
423 [$fieldName => $fromValue]
443 $configuration =
$GLOBALS[
'TCA'][$item->getTableName()][
'columns'][$fieldName];
444 $isSpecialLanguageField = ($configuration[
'config'][
'special'] ??
null) ===
'languages';
446 $fromId = $fromRecord[
'uid'];
447 if ($this->
isSetInDataMap($item->getTableName(), $fromId, $fieldName)) {
448 $fromValue = $this->allDataMap[$item->getTableName()][$fromId][$fieldName];
450 $fromValue = $fromRecord[$fieldName];
456 empty($configuration[
'config'][
'MM'])
458 || $isSpecialLanguageField
461 $item->getTableName(),
463 [$fieldName => $fromValue]
468 if ($isSpecialLanguageField) {
469 $specialTableName =
'sys_language';
472 $type = $configuration[
'config'][
'type'];
473 $manyToManyTable = $configuration[
'config'][
'MM'];
474 if ($type ===
'group' && $configuration[
'config'][
'internal_type'] ===
'db') {
475 $tableNames = trim($configuration[
'config'][
'allowed'] ??
'');
476 } elseif ($configuration[
'config'][
'type'] ===
'select') {
477 $tableNames = ($specialTableName ?? $configuration[
'config'][
'foreign_table'] ??
'');
483 $relationHandler->start(
488 $item->getTableName(),
489 $configuration[
'config']
495 $item->getTableName(),
497 [$fieldName => implode(
',', $relationHandler->getValueArray())]
515 $configuration =
$GLOBALS[
'TCA'][$item->getTableName()][
'columns'][$fieldName];
516 $isLocalizationModeExclude = ($configuration[
'l10n_mode'] ??
null) ===
'exclude';
517 $foreignTableName = $configuration[
'config'][
'foreign_table'];
520 'language' =>
$GLOBALS[
'TCA'][$foreignTableName][
'ctrl'][
'languageField'] ??
null,
521 'parent' =>
$GLOBALS[
'TCA'][$foreignTableName][
'ctrl'][
'transOrigPointerField'] ??
null,
522 'source' =>
$GLOBALS[
'TCA'][$foreignTableName][
'ctrl'][
'translationSource'] ??
null,
524 $isTranslatable = (!empty($fieldNames[
'language']) && !empty($fieldNames[
'parent']));
525 $isLocalized = !empty($item->getLanguage());
540 $dependentIdMap = $this->
fetchDependentIdMap($foreignTableName, $suggestedAncestorIds, $item->getLanguage());
543 $suggestedAncestorIds = array_diff($suggestedAncestorIds, array_values($dependentIdMap));
546 $removeIds = array_diff($persistedIds, array_values($dependentIdMap));
548 $removeAncestorIds = array_diff(array_keys($dependentIdMap), $suggestedAncestorIds);
550 $missingAncestorIds = array_diff($suggestedAncestorIds, array_keys($dependentIdMap));
554 $populateAncestorIds = array_diff($missingAncestorIds, $createAncestorIds);
556 $desiredIdMap = array_combine($suggestedAncestorIds, $suggestedAncestorIds);
558 foreach ($dependentIdMap as $ancestorId => $translationId) {
559 if (isset($desiredIdMap[$ancestorId])) {
560 $desiredIdMap[$ancestorId] = $translationId;
564 if (empty($removeAncestorIds) && empty($missingAncestorIds)) {
566 $item->getTableName(),
568 [$fieldName => implode(
',', array_values($desiredIdMap))]
580 $sanitizedValue = $this->sanitizationMap[$item->getTableName()][$item->getId()][$fieldName] ??
null;
582 !empty($missingAncestorIds) && $item->isNew() && $sanitizedValue !==
null
583 && count(GeneralUtility::trimExplode(
',', $sanitizedValue,
true)) === count($missingAncestorIds)
586 $item->getTableName(),
588 [$fieldName => $sanitizedValue]
593 $localCommandMap = [];
594 foreach ($removeIds as $removeId) {
595 $localCommandMap[$foreignTableName][$removeId][
'delete'] =
true;
597 foreach ($removeAncestorIds as $removeAncestorId) {
598 $removeId = $dependentIdMap[$removeAncestorId];
599 $localCommandMap[$foreignTableName][$removeId][
'delete'] =
true;
601 foreach ($createAncestorIds as $createAncestorId) {
603 if ($isLocalizationModeExclude || !$isTranslatable) {
604 $localCommandMap[$foreignTableName][$createAncestorId][
'copy'] = [
605 'target' => -$createAncestorId,
606 'ignoreLocalization' =>
true,
610 $localCommandMap[$foreignTableName][$createAncestorId][
'localize'] = $item->getLanguage();
614 if (!empty($localCommandMap)) {
615 $localDataHandler = GeneralUtility::makeInstance(DataHandler::class);
616 $localDataHandler->start([], $localCommandMap, $this->backendUser);
617 $localDataHandler->process_cmdmap();
619 foreach ($createAncestorIds as $createAncestorId) {
620 if (empty($localDataHandler->copyMappingArray_merged[$foreignTableName][$createAncestorId])) {
621 $additionalInformation =
'';
622 if (!empty($localDataHandler->errorLog)) {
623 $additionalInformation =
', reason "'
624 . implode(
', ', $localDataHandler->errorLog) .
'"';
626 throw new \RuntimeException(
627 'Child record was not processed' . $additionalInformation,
631 $newLocalizationId = $localDataHandler->copyMappingArray_merged[$foreignTableName][$createAncestorId];
632 $newLocalizationId = $localDataHandler->getAutoVersionId($foreignTableName, $newLocalizationId) ?? $newLocalizationId;
633 $desiredIdMap[$createAncestorId] = $newLocalizationId;
636 if ($isLocalizationModeExclude && $isTranslatable && $isLocalized) {
640 $item->getLanguage(),
653 if (!empty($populateAncestorIds)) {
654 foreach ($populateAncestorIds as $populateAncestorId) {
656 $desiredIdMap[$populateAncestorId] = $newLocalizationId;
657 $duplicatedValues = $this->allDataMap[$foreignTableName][$populateAncestorId] ?? [];
659 if ($isTranslatable && $isLocalized) {
663 $item->getLanguage(),
669 if ($isTranslatable && $isLocalized && !$isLocalizationModeExclude) {
673 $item->getLanguage(),
686 $item->getTableName(),
688 [$fieldName => implode(
',', array_values($desiredIdMap))]
704 $suggestedAncestorIds = [];
705 $fromId = $fromRecord[
'uid'];
706 $configuration =
$GLOBALS[
'TCA'][$item->getTableName()][
'columns'][$fieldName];
707 $foreignTableName = $configuration[
'config'][
'foreign_table'];
708 $manyToManyTable = ($configuration[
'config'][
'MM'] ??
'');
712 if ($this->
isSetInDataMap($item->getTableName(), $fromId, $fieldName)) {
713 $suggestedAncestorIds = GeneralUtility::trimExplode(
715 $this->allDataMap[$item->getTableName()][$fromId][$fieldName],
721 $relationHandler->start(
722 $fromRecord[$fieldName],
726 $item->getTableName(),
727 $configuration[
'config']
732 return array_filter($suggestedAncestorIds);
746 $configuration =
$GLOBALS[
'TCA'][$item->getTableName()][
'columns'][$fieldName];
747 $foreignTableName = $configuration[
'config'][
'foreign_table'];
748 $manyToManyTable = ($configuration[
'config'][
'MM'] ??
'');
751 if (!$item->isNew()) {
753 $relationHandler->start(
754 $forRecord[$fieldName] ??
'',
758 $item->getTableName(),
759 $configuration[
'config']
764 return array_filter($persistedIds);
777 protected function isSetInDataMap(
string $tableName, $id,
string $fieldName)
781 isset($this->allDataMap[$tableName][$id][$fieldName])
783 || isset($this->allDataMap[$tableName][$id])
784 && is_array($this->allDataMap[$tableName][$id])
785 && array_key_exists($fieldName, $this->allDataMap[$tableName][$id]);
798 protected function modifyDataMap(
string $tableName, $id, array $values)
802 $sameValues = array_intersect_assoc(
803 $this->allDataMap[$tableName][$id] ?? [],
806 if (!empty($sameValues)) {
807 $fieldNames = implode(
', ', array_keys($sameValues));
808 throw new \RuntimeException(
810 'Issued data-map change for table %s with same values '
811 .
'for these fields names %s',
819 $this->modifiedDataMap[$tableName][$id] = array_merge(
820 $this->modifiedDataMap[$tableName][$id] ?? [],
823 $this->allDataMap[$tableName][$id] = array_merge(
824 $this->allDataMap[$tableName][$id] ?? [],
835 if (!isset($this->allItems[$identifier])) {
836 $this->allItems[$identifier] = $item;
838 $this->nextItems[$identifier] = $item;
852 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
853 ->getQueryBuilderForTable($tableName);
854 $queryBuilder->getRestrictions()
856 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
857 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, $this->backendUser->workspace,
false));
858 $statement = $queryBuilder
859 ->select(...array_values($fieldNames))
862 $queryBuilder->expr()->in(
864 $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
869 $translationValues = [];
870 foreach ($statement as $record) {
871 $translationValues[$record[
'uid']] = $record;
873 return $translationValues;
901 'l10n_state' =>
'l10n_state',
902 'language' =>
$GLOBALS[
'TCA'][$tableName][
'ctrl'][
'languageField'],
903 'parent' =>
$GLOBALS[
'TCA'][$tableName][
'ctrl'][
'transOrigPointerField'],
905 if (!empty(
$GLOBALS[
'TCA'][$tableName][
'ctrl'][
'translationSource'])) {
906 $fieldNames[
'source'] =
$GLOBALS[
'TCA'][$tableName][
'ctrl'][
'translationSource'];
908 $fieldNamesMap = array_combine($fieldNames, $fieldNames);
911 $createdIds = array_diff($ids, $persistedIds);
914 foreach ($createdIds as $createdId) {
915 $data = $this->allDataMap[$tableName][$createdId] ??
null;
916 if ($data ===
null) {
919 $dependentElements[] = array_merge(
920 [
'uid' => $createdId],
921 array_intersect_key($data, $fieldNamesMap)
926 foreach ($dependentElements as $dependentElement) {
929 $dependentElement[
'uid'],
935 if ($dependentItem->isDirectChildType()) {
938 if ($dependentItem->isGrandChildType()) {
943 return $dependencyMap;
974 $originFieldName = (
$GLOBALS[
'TCA'][$tableName][
'ctrl'][
'origUid'] ??
null);
976 if (!$isTranslatable && $originFieldName ===
null) {
981 if ($isTranslatable) {
984 'l10n_state' =>
'l10n_state',
985 'language' =>
$GLOBALS[
'TCA'][$tableName][
'ctrl'][
'languageField'],
986 'parent' =>
$GLOBALS[
'TCA'][$tableName][
'ctrl'][
'transOrigPointerField'],
988 if (!empty(
$GLOBALS[
'TCA'][$tableName][
'ctrl'][
'translationSource'])) {
989 $fieldNames[
'source'] =
$GLOBALS[
'TCA'][$tableName][
'ctrl'][
'translationSource'];
994 'origin' => $originFieldName,
999 if ($isTranslatable) {
1003 $fetchIds = array_unique(array_merge($ids, array_keys($ancestorIdMap)));
1008 $dependentIdMap = [];
1009 foreach ($dependentElements as $dependentElement) {
1010 $dependentId = $dependentElement[
'uid'];
1012 if (!$isTranslatable) {
1013 $ancestorId = (int)$dependentElement[$fieldNames[
'origin']];
1015 } elseif ((
int)$dependentElement[$fieldNames[
'language']] === $desiredLanguage) {
1022 if (in_array($ancestorId, $ids,
true)) {
1023 $dependentIdMap[$ancestorId] = $dependentId;
1024 } elseif (!empty($ancestorIdMap[$ancestorId])) {
1026 $possibleChainedIds = array_intersect(
1028 $ancestorIdMap[$ancestorId]
1030 if (!empty($possibleChainedIds)) {
1031 $ancestorId = $possibleChainedIds[0];
1032 $dependentIdMap[$ancestorId] = $dependentId;
1036 return $dependentIdMap;
1052 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1053 ->getQueryBuilderForTable($tableName);
1054 $queryBuilder->getRestrictions()
1056 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1057 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, $this->backendUser->workspace,
false));
1059 $zeroParameter = $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT);
1061 $idsParameter = $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY);
1064 if (!empty($fieldNames[
'language']) && !empty($fieldNames[
'parent'])) {
1065 $ancestorPredicates = [
1066 $queryBuilder->expr()->in(
1067 $fieldNames[
'parent'],
1071 if (!empty($fieldNames[
'source'])) {
1072 $ancestorPredicates[] = $queryBuilder->expr()->in(
1073 $fieldNames[
'source'],
1079 $queryBuilder->expr()->gt(
1080 $fieldNames[
'language'],
1084 $queryBuilder->expr()->gt(
1085 $fieldNames[
'parent'],
1089 $queryBuilder->expr()->orX(...$ancestorPredicates),
1091 } elseif (!empty($fieldNames[
'origin'])) {
1094 $queryBuilder->expr()->in(
1095 $fieldNames[
'origin'],
1101 throw new \InvalidArgumentException(
1102 'Invalid combination of query field names given',
1107 $statement = $queryBuilder
1108 ->select(...array_values($fieldNames))
1110 ->andWhere(...$predicates)
1113 $dependentElements = [];
1114 foreach ($statement as $record) {
1115 $dependentElements[] = $record;
1117 return $dependentElements;
1129 return array_filter(
1131 function (DataMapItem $item) use ($type) {
1132 return $item->getType() === $type;
1145 $ids = array_filter(
1151 return array_map(
'intval', $ids);
1163 return array_filter(
1165 function ($id) use ($tableName) {
1166 return $this->
findItem($tableName, $id) ===
null;
1180 function (array $relationItem) {
1181 return (
int)$relationItem[
'id'];
1194 $sourceName = $fieldNames[
'source'] ??
null;
1195 if ($sourceName !==
null && !empty($element[$sourceName])) {
1197 return (
int)$element[$sourceName];
1199 $parentName = $fieldNames[
'parent'] ??
null;
1200 if ($parentName !==
null && !empty($element[$parentName])) {
1202 return (
int)$element[$parentName];
1219 $ancestorIdMap = [];
1220 foreach ($elements as $element) {
1222 if ($ancestorId !==
null) {
1223 $ancestorIdMap[$ancestorId][] = (int)$element[
'uid'];
1226 return $ancestorIdMap;
1236 protected function findItem(
string $tableName, $id)
1238 return $this->allItems[$tableName .
':' . $id] ??
null;
1253 protected function duplicateFromDataMap(
string $tableName, $fromId,
int $language, array $fieldNames,
bool $localize): array
1255 $data = $this->allDataMap[$tableName][$fromId] ?? [];
1257 if (empty($language) || !$localize) {
1278 if (empty($language)) {
1283 $data[$fieldNames[
'language']] = $language;
1285 if (empty($data[$fieldNames[
'parent']])) {
1287 $data[$fieldNames[
'parent']] = $fromId;
1290 if (!empty($fieldNames[
'source'])) {
1292 $data[$fieldNames[
'source']] = $fromId;
1296 unset($data[$fieldName]);
1311 protected function prefixLanguageTitle(
string $tableName, $fromId,
int $language, array $data): array
1313 $prefixFieldNames = array_intersect(
1317 if (empty($prefixFieldNames)) {
1326 if (!empty($tsConfigTranslateToMessage)) {
1327 $prefix = $tsConfigTranslateToMessage;
1328 if ($languageService !==
null) {
1329 $prefix = $languageService->sL($prefix);
1331 $prefix = sprintf($prefix, $languageRecord[
'title']);
1333 if (empty($prefix)) {
1334 $prefix =
'Translate to ' . $languageRecord[
'title'] .
':';
1337 foreach ($prefixFieldNames as $prefixFieldName) {
1339 $data[$prefixFieldName] =
'[' . $prefix .
'] ' . $data[$prefixFieldName];
1365 return $item->getState()->filterFieldNames($scope, $modified);
1369 $item->getTableName()
1383 $localizationExcludeFieldNames = [];
1384 if (empty(
$GLOBALS[
'TCA'][$tableName][
'columns'])) {
1385 return $localizationExcludeFieldNames;
1388 foreach (
$GLOBALS[
'TCA'][$tableName][
'columns'] as $fieldName => $configuration) {
1389 if (($configuration[
'l10n_mode'] ??
null) ===
'exclude'
1390 && ($configuration[
'config'][
'type'] ??
null) !==
'none'
1392 $localizationExcludeFieldNames[] = $fieldName;
1396 return $localizationExcludeFieldNames;
1422 $prefixLanguageTitleFieldNames = [];
1423 if (empty(
$GLOBALS[
'TCA'][$tableName][
'columns'])) {
1424 return $prefixLanguageTitleFieldNames;
1427 foreach (
$GLOBALS[
'TCA'][$tableName][
'columns'] as $fieldName => $configuration) {
1428 $type = $configuration[
'config'][
'type'] ??
null;
1430 ($configuration[
'l10n_mode'] ??
null) ===
'prefixLangTitle'
1431 && ($type ===
'input' || $type ===
'text')
1433 $prefixLanguageTitleFieldNames[] = $fieldName;
1437 return $prefixLanguageTitleFieldNames;
1447 protected function isRelationField(
string $tableName,
string $fieldName): bool
1449 if (empty(
$GLOBALS[
'TCA'][$tableName][
'columns'][$fieldName][
'config'][
'type'])) {
1453 $configuration =
$GLOBALS[
'TCA'][$tableName][
'columns'][$fieldName][
'config'];
1456 $configuration[
'type'] ===
'group'
1457 && ($configuration[
'internal_type'] ??
null) ===
'db'
1458 && !empty($configuration[
'allowed'])
1459 || $configuration[
'type'] ===
'select'
1461 !empty($configuration[
'foreign_table'])
1462 && !empty(
$GLOBALS[
'TCA'][$configuration[
'foreign_table']])
1463 || ($configuration[
'special'] ??
null) ===
'languages'
1478 if (empty(
$GLOBALS[
'TCA'][$tableName][
'columns'][$fieldName][
'config'][
'type'])) {
1482 $configuration =
$GLOBALS[
'TCA'][$tableName][
'columns'][$fieldName][
'config'];
1485 $configuration[
'type'] ===
'inline'
1486 && !empty($configuration[
'foreign_table'])
1487 && !empty(
$GLOBALS[
'TCA'][$configuration[
'foreign_table']])
1512 $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
1513 $relationHandler->setWorkspaceId($this->backendUser->workspace);
1514 return $relationHandler;