Test Failed
Branch master (137376)
by Tymoteusz
20:39
created

DataMapProcessor   F

Complexity

Total Complexity 179

Size/Duplication

Total Lines 1402
Duplicated Lines 2.07 %

Importance

Changes 0
Metric Value
dl 29
loc 1402
rs 0.6314
c 0
b 0
f 0
wmc 179

40 Methods

Rating   Name   Duplication   Size   Complexity  
B process() 0 23 5
A mapRelationItemId() 0 7 1
A getLocalizationModeExcludeFieldNames() 0 14 4
A filterItemsByType() 0 6 1
B getPrefixLanguageTitleFieldNames() 0 18 6
B modifyDataMap() 0 28 2
A getFieldNamesToBeHandled() 0 5 1
C fetchDependencies() 3 51 8
A createRelationHandler() 0 5 1
B resolveSuggestedInlineRelations() 0 31 3
A isSetInDataMap() 0 9 4
A sanitize() 0 5 3
B synchronizeDirectRelations() 5 57 9
C fetchDependentIdMap() 3 66 12
A instance() 0 6 1
A resolveAncestorId() 0 14 4
A addNextItem() 0 7 2
C duplicateFromDataMap() 0 55 11
B enrich() 0 15 5
F synchronizeInlineRelations() 0 143 22
A __construct() 0 5 1
A filterNewItemIds() 0 6 1
A isApplicable() 0 6 3
A isInlineRelationField() 3 12 4
A sanitizeTranslationItem() 0 20 2
A getLanguageService() 0 3 1
A buildElementAncestorIdMap() 0 10 3
B fetchDependentElements() 0 70 6
A findItem() 0 3 1
B populateTranslationItem() 0 22 5
C collectItems() 3 60 9
B synchronizeFieldValues() 9 32 6
A finishTranslationItem() 0 10 3
B getFieldNamesForItemScope() 0 20 5
B fetchTranslationValues() 0 24 2
A filterNumericIds() 0 6 1
B purgeDataMap() 0 13 5
B isRelationField() 3 19 9
B synchronizeTranslationItem() 0 32 5
A resolvePersistedInlineRelations() 0 22 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DataMapProcessor 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 DataMapProcessor, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace TYPO3\CMS\Core\DataHandling\Localization;
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\Authentication\BackendUserAuthentication;
19
use TYPO3\CMS\Core\Database\Connection;
20
use TYPO3\CMS\Core\Database\ConnectionPool;
21
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
22
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
23
use TYPO3\CMS\Core\Database\RelationHandler;
24
use TYPO3\CMS\Core\DataHandling\DataHandler;
25
use TYPO3\CMS\Core\Localization\LanguageService;
26
use TYPO3\CMS\Core\Utility\GeneralUtility;
27
use TYPO3\CMS\Core\Utility\MathUtility;
28
use TYPO3\CMS\Core\Utility\StringUtility;
29
30
/**
31
 * This processor analyses the provided data-map before actually being process
32
 * in the calling DataHandler instance. Field names that are configured to have
33
 * "allowLanguageSynchronization" enabled are either synchronized from there
34
 * relative parent records (could be a default language record, or a l10n_source
35
 * record) or to their dependent records (in case a default language record or
36
 * nested records pointing upwards with l10n_source).
37
 *
38
 * Except inline relational record editing, all modifications are applied to
39
 * the data-map directly, which ensures proper history entries as a side-effect.
40
 * For inline relational record editing, this processor either triggers the copy
41
 * or localize actions by instantiation a new local DataHandler instance.
42
 *
43
 * Namings in this class:
44
 * + forTableName, forId always refers to dependencies data is provided *for*
45
 * + fromTableName, fromId always refers to ancestors data is retrieved *from*
46
 */
47
class DataMapProcessor
48
{
49
    /**
50
     * @var array
51
     */
52
    protected $allDataMap = [];
53
54
    /**
55
     * @var array
56
     */
57
    protected $modifiedDataMap = [];
58
59
    /**
60
     * @var array
61
     */
62
    protected $sanitizationMap = [];
63
64
    /**
65
     * @var BackendUserAuthentication
66
     */
67
    protected $backendUser;
68
69
    /**
70
     * @var DataMapItem[]
71
     */
72
    protected $allItems = [];
73
74
    /**
75
     * @var DataMapItem[]
76
     */
77
    protected $nextItems = [];
78
79
    /**
80
     * Class generator
81
     *
82
     * @param array $dataMap The submitted data-map to be worked on
83
     * @param BackendUserAuthentication $backendUser Forwared backend-user scope
84
     * @return DataMapProcessor
85
     */
86
    public static function instance(array $dataMap, BackendUserAuthentication $backendUser)
87
    {
88
        return GeneralUtility::makeInstance(
89
            static::class,
90
            $dataMap,
91
            $backendUser
0 ignored issues
show
Bug introduced by
$backendUser of type TYPO3\CMS\Core\Authentic...ckendUserAuthentication is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

91
            /** @scrutinizer ignore-type */ $backendUser
Loading history...
92
        );
93
    }
94
95
    /**
96
     * @param array $dataMap The submitted data-map to be worked on
97
     * @param BackendUserAuthentication $backendUser Forwared backend-user scope
98
     */
99
    public function __construct(array $dataMap, BackendUserAuthentication $backendUser)
100
    {
101
        $this->allDataMap = $dataMap;
102
        $this->modifiedDataMap = $dataMap;
103
        $this->backendUser = $backendUser;
104
    }
105
106
    /**
107
     * Processes the submitted data-map and returns the sanitized and enriched
108
     * version depending on accordant localization states and dependencies.
109
     *
110
     * @return array
111
     */
112
    public function process()
113
    {
114
        $iterations = 0;
115
116
        while (!empty($this->modifiedDataMap)) {
117
            $this->nextItems = [];
118
            foreach ($this->modifiedDataMap as $tableName => $idValues) {
119
                $this->collectItems($tableName, $idValues);
120
            }
121
122
            $this->modifiedDataMap = [];
123
            if (empty($this->nextItems)) {
124
                break;
125
            }
126
127
            if ($iterations++ === 0) {
128
                $this->sanitize($this->allItems);
129
            }
130
            $this->enrich($this->nextItems);
131
        }
132
133
        $this->allDataMap = $this->purgeDataMap($this->allDataMap);
134
        return $this->allDataMap;
135
    }
136
137
    /**
138
     * Purges superfluous empty data-map sections.
139
     *
140
     * @param array $dataMap
141
     * @return array
142
     */
143
    protected function purgeDataMap(array $dataMap): array
144
    {
145
        foreach ($dataMap as $tableName => $idValues) {
146
            foreach ($idValues as $id => $values) {
147
                if (empty($values)) {
148
                    unset($dataMap[$tableName][$id]);
149
                }
150
            }
151
            if (empty($dataMap[$tableName])) {
152
                unset($dataMap[$tableName]);
153
            }
154
        }
155
        return $dataMap;
156
    }
157
158
    /**
159
     * Create data map items of all affected rows
160
     *
161
     * @param string $tableName
162
     * @param array $idValues
163
     */
164
    protected function collectItems(string $tableName, array $idValues)
165
    {
166
        if (!$this->isApplicable($tableName)) {
167
            return;
168
        }
169
170
        $fieldNames = [
171
            'uid' => 'uid',
172
            'l10n_state' => 'l10n_state',
173
            'language' => $GLOBALS['TCA'][$tableName]['ctrl']['languageField'],
174
            'parent' => $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'],
175
        ];
176 View Code Duplication
        if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['translationSource'])) {
177
            $fieldNames['source'] = $GLOBALS['TCA'][$tableName]['ctrl']['translationSource'];
178
        }
179
180
        $translationValues = $this->fetchTranslationValues(
181
            $tableName,
182
            $fieldNames,
183
            $this->filterNewItemIds(
184
                $tableName,
185
                $this->filterNumericIds(array_keys($idValues))
186
            )
187
        );
188
189
        $dependencies = $this->fetchDependencies(
190
            $tableName,
191
            $this->filterNewItemIds($tableName, array_keys($idValues))
192
        );
193
194
        foreach ($idValues as $id => $values) {
195
            $item = $this->findItem($tableName, $id);
196
            // build item if it has not been created in a previous iteration
197
            if ($item === null) {
198
                $recordValues = $translationValues[$id] ?? [];
199
                $item = DataMapItem::build(
200
                    $tableName,
201
                    $id,
202
                    $values,
203
                    $recordValues,
204
                    $fieldNames
205
                );
206
207
                // elements using "all language" cannot be localized
208
                if ($item->getLanguage() === -1) {
209
                    unset($item);
210
                    continue;
211
                }
212
                // must be any kind of localization and in connected mode
213
                if ($item->getLanguage() > 0 && empty($item->getParent())) {
214
                    unset($item);
215
                    continue;
216
                }
217
                // add dependencies
218
                if (!empty($dependencies[$id])) {
219
                    $item->setDependencies($dependencies[$id]);
220
                }
221
            }
222
            // add item to $this->allItems and $this->nextItems
223
            $this->addNextItem($item);
224
        }
225
    }
226
227
    /**
228
     * Sanitizes the submitted data-map items and removes fields which are not
229
     * defined as custom and thus rely on either parent or source values.
230
     *
231
     * @param DataMapItem[] $items
232
     */
233
    protected function sanitize(array $items)
234
    {
235
        foreach (['directChild', 'grandChild'] as $type) {
236
            foreach ($this->filterItemsByType($type, $items) as $item) {
237
                $this->sanitizeTranslationItem($item);
238
            }
239
        }
240
    }
241
242
    /**
243
     * Handle synchronization of an item list
244
     *
245
     * @param DataMapItem[] $items
246
     */
247
    protected function enrich(array $items)
248
    {
249
        foreach (['directChild', 'grandChild'] as $type) {
250
            foreach ($this->filterItemsByType($type, $items) as $item) {
251
                foreach ($item->getApplicableScopes() as $scope) {
252
                    $fromId = $item->getIdForScope($scope);
253
                    $fieldNames = $this->getFieldNamesForItemScope($item, $scope, !$item->isNew());
254
                    $this->synchronizeTranslationItem($item, $fieldNames, $fromId);
255
                }
256
                $this->populateTranslationItem($item);
257
                $this->finishTranslationItem($item);
258
            }
259
        }
260
        foreach ($this->filterItemsByType('parent', $items) as $item) {
261
            $this->populateTranslationItem($item);
262
        }
263
    }
264
265
    /**
266
     * Sanitizes the submitted data-map for a particular item and removes
267
     * fields which are not defined as custom and thus rely on either parent
268
     * or source values.
269
     *
270
     * @param DataMapItem $item
271
     */
272
    protected function sanitizeTranslationItem(DataMapItem $item)
273
    {
274
        $fieldNames = [];
275
        foreach ($item->getApplicableScopes() as $scope) {
276
            $fieldNames = array_merge(
277
                $fieldNames,
278
                $this->getFieldNamesForItemScope($item, $scope, false)
279
            );
280
        }
281
282
        $fieldNameMap = array_combine($fieldNames, $fieldNames);
283
        // separate fields, that are submitted in data-map, but not defined as custom
284
        $this->sanitizationMap[$item->getTableName()][$item->getId()] = array_intersect_key(
285
            $this->allDataMap[$item->getTableName()][$item->getId()],
286
            $fieldNameMap
287
        );
288
        // remove fields, that are submitted in data-map, but not defined as custom
289
        $this->allDataMap[$item->getTableName()][$item->getId()] = array_diff_key(
290
            $this->allDataMap[$item->getTableName()][$item->getId()],
291
            $fieldNameMap
292
        );
293
    }
294
295
    /**
296
     * Synchronize a single item
297
     *
298
     * @param DataMapItem $item
299
     * @param array $fieldNames
300
     * @param string|int $fromId
301
     */
302
    protected function synchronizeTranslationItem(DataMapItem $item, array $fieldNames, $fromId)
303
    {
304
        if (empty($fieldNames)) {
305
            return;
306
        }
307
308
        $fieldNameList = 'uid,' . implode(',', $fieldNames);
309
310
        $fromRecord = ['uid' => $fromId];
311
        if (MathUtility::canBeInterpretedAsInteger($fromId)) {
312
            $fromRecord = BackendUtility::getRecordWSOL(
313
                $item->getTableName(),
314
                $fromId,
0 ignored issues
show
Bug introduced by
It seems like $fromId can also be of type string; however, parameter $uid of TYPO3\CMS\Backend\Utilit...tility::getRecordWSOL() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

314
                /** @scrutinizer ignore-type */ $fromId,
Loading history...
315
                $fieldNameList
316
            );
317
        }
318
319
        $forRecord = [];
320
        if (!$item->isNew()) {
321
            $forRecord = BackendUtility::getRecordWSOL(
322
                $item->getTableName(),
323
                $item->getId(),
324
                $fieldNameList
325
            );
326
        }
327
328
        foreach ($fieldNames as $fieldName) {
329
            $this->synchronizeFieldValues(
330
                $item,
331
                $fieldName,
332
                $fromRecord,
333
                $forRecord
334
            );
335
        }
336
    }
337
338
    /**
339
     * Populates values downwards, either from a parent language item or
340
     * a source language item to an accordant dependent translation item.
341
     *
342
     * @param DataMapItem $item
343
     */
344
    protected function populateTranslationItem(DataMapItem $item)
345
    {
346
        foreach ([DataMapItem::SCOPE_PARENT, DataMapItem::SCOPE_SOURCE] as $scope) {
347
            foreach ($item->findDependencies($scope) as $dependentItem) {
348
                // use suggested item, if it was submitted in data-map
349
                $suggestedDependentItem = $this->findItem(
350
                    $dependentItem->getTableName(),
351
                    $dependentItem->getId()
352
                );
353
                if ($suggestedDependentItem !== null) {
354
                    $dependentItem = $suggestedDependentItem;
355
                }
356
                foreach ([$scope, DataMapItem::SCOPE_EXCLUDE] as $dependentScope) {
357
                    $fieldNames = $this->getFieldNamesForItemScope(
358
                        $dependentItem,
359
                        $dependentScope,
360
                        false
361
                    );
362
                    $this->synchronizeTranslationItem(
363
                        $dependentItem,
364
                        $fieldNames,
365
                        $item->getId()
366
                    );
367
                }
368
            }
369
        }
370
    }
371
372
    /**
373
     * Finishes a translation item by updating states to be persisted.
374
     *
375
     * @param DataMapItem $item
376
     */
377
    protected function finishTranslationItem(DataMapItem $item)
378
    {
379
        if (
380
            $item->isParentType()
381
            || !State::isApplicable($item->getTableName())
382
        ) {
383
            return;
384
        }
385
386
        $this->allDataMap[$item->getTableName()][$item->getId()]['l10n_state'] = $item->getState()->export();
387
    }
388
389
    /**
390
     * Synchronize simple values like text and similar
391
     *
392
     * @param DataMapItem $item
393
     * @param string $fieldName
394
     * @param array $fromRecord
395
     * @param array $forRecord
396
     */
397
    protected function synchronizeFieldValues(DataMapItem $item, string $fieldName, array $fromRecord, array $forRecord)
398
    {
399
        // skip if this field has been processed already, assumed that proper sanitation happened
400
        if ($this->isSetInDataMap($item->getTableName(), $item->getId(), $fieldName)) {
401
            return;
402
        }
403
404
        $fromId = $fromRecord['uid'];
405
        // retrieve value from in-memory data-map
406 View Code Duplication
        if ($this->isSetInDataMap($item->getTableName(), $fromId, $fieldName)) {
407
            $fromValue = $this->allDataMap[$item->getTableName()][$fromId][$fieldName];
408
        } elseif (array_key_exists($fieldName, $fromRecord)) {
409
            // retrieve value from record
410
            $fromValue = $fromRecord[$fieldName];
411
        } else {
412
            // otherwise abort synchronization
413
            return;
414
        }
415
416
        // plain values
417
        if (!$this->isRelationField($item->getTableName(), $fieldName)) {
418
            $this->modifyDataMap(
419
                $item->getTableName(),
420
                $item->getId(),
421
                [$fieldName => $fromValue]
422
            );
423
        } elseif (!$this->isInlineRelationField($item->getTableName(), $fieldName)) {
424
            // direct relational values
425
            $this->synchronizeDirectRelations($item, $fieldName, $fromRecord);
426
        } else {
427
            // inline relational values
428
            $this->synchronizeInlineRelations($item, $fieldName, $fromRecord, $forRecord);
429
        }
430
    }
431
432
    /**
433
     * Synchronize select and group field localizations
434
     *
435
     * @param DataMapItem $item
436
     * @param string $fieldName
437
     * @param array $fromRecord
438
     */
439
    protected function synchronizeDirectRelations(DataMapItem $item, string $fieldName, array $fromRecord)
440
    {
441
        $configuration = $GLOBALS['TCA'][$item->getTableName()]['columns'][$fieldName];
442
        $isSpecialLanguageField = ($configuration['config']['special'] ?? null) === 'languages';
443
444
        $fromId = $fromRecord['uid'];
445 View Code Duplication
        if ($this->isSetInDataMap($item->getTableName(), $fromId, $fieldName)) {
446
            $fromValue = $this->allDataMap[$item->getTableName()][$fromId][$fieldName];
447
        } else {
448
            $fromValue = $fromRecord[$fieldName];
449
        }
450
451
        // non-MM relations are stored as comma separated values, just use them
452
        // if values are available in data-map already, just use them as well
453
        if (
454
            empty($configuration['config']['MM'])
455
            || $this->isSetInDataMap($item->getTableName(), $fromId, $fieldName)
456
            || $isSpecialLanguageField
457
        ) {
458
            $this->modifyDataMap(
459
                $item->getTableName(),
460
                $item->getId(),
461
                [$fieldName => $fromValue]
462
            );
463
            return;
464
        }
465
        // resolve the language special table name
466
        if ($isSpecialLanguageField) {
467
            $specialTableName = 'sys_language';
468
        }
469
        // fetch MM relations from storage
470
        $type = $configuration['config']['type'];
471
        $manyToManyTable = $configuration['config']['MM'];
472
        if ($type === 'group' && $configuration['config']['internal_type'] === 'db') {
473
            $tableNames = trim($configuration['config']['allowed'] ?? '');
474
        } elseif ($configuration['config']['type'] === 'select') {
475
            $tableNames = ($specialTableName ?? $configuration['config']['foreign_table'] ?? '');
476
        } else {
477
            return;
478
        }
479
480
        $relationHandler = $this->createRelationHandler();
481
        $relationHandler->start(
482
            '',
483
            $tableNames,
484
            $manyToManyTable,
485
            $fromId,
486
            $item->getTableName(),
487
            $configuration['config']
488
        );
489
490
        // provide list of relations, optionally prepended with table name
491
        // e.g. "13,19,23" or "tt_content_27,tx_extension_items_28"
492
        $this->modifyDataMap(
493
            $item->getTableName(),
494
            $item->getId(),
495
            [$fieldName => implode(',', $relationHandler->getValueArray())]
496
        );
497
    }
498
499
    /**
500
     * Handle synchronization of inline relations
501
     *
502
     * @param DataMapItem $item
503
     * @param string $fieldName
504
     * @param array $fromRecord
505
     * @param array $forRecord
506
     * @throws \RuntimeException
507
     */
508
    protected function synchronizeInlineRelations(DataMapItem $item, string $fieldName, array $fromRecord, array $forRecord)
509
    {
510
        $configuration = $GLOBALS['TCA'][$item->getTableName()]['columns'][$fieldName];
511
        $isLocalizationModeExclude = ($configuration['l10n_mode'] ?? null) === 'exclude';
512
        $foreignTableName = $configuration['config']['foreign_table'];
513
514
        $fieldNames = [
515
            'language' => ($GLOBALS['TCA'][$foreignTableName]['ctrl']['languageField'] ?? null),
516
            'parent' => ($GLOBALS['TCA'][$foreignTableName]['ctrl']['transOrigPointerField'] ?? null),
517
            'source' => ($GLOBALS['TCA'][$foreignTableName]['ctrl']['translationSource'] ?? null),
518
        ];
519
        $isTranslatable = (!empty($fieldNames['language']) && !empty($fieldNames['parent']));
520
521
        $suggestedAncestorIds = $this->resolveSuggestedInlineRelations(
522
            $item,
523
            $fieldName,
524
            $fromRecord
525
        );
526
        $persistedIds = $this->resolvePersistedInlineRelations(
527
            $item,
528
            $fieldName,
529
            $forRecord
530
        );
531
532
        // The dependent ID map points from language parent/source record to
533
        // localization, thus keys: parents/sources & values: localizations
534
        $dependentIdMap = $this->fetchDependentIdMap($foreignTableName, $suggestedAncestorIds, $item->getLanguage());
0 ignored issues
show
Bug introduced by
It seems like $item->getLanguage() can also be of type string; however, parameter $desiredLanguage of TYPO3\CMS\Core\DataHandl...::fetchDependentIdMap() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

534
        $dependentIdMap = $this->fetchDependentIdMap($foreignTableName, $suggestedAncestorIds, /** @scrutinizer ignore-type */ $item->getLanguage());
Loading history...
535
        // filter incomplete structures - this is a drawback of DataHandler's remap stack, since
536
        // just created IRRE translations still belong to the language parent - filter them out
537
        $suggestedAncestorIds = array_diff($suggestedAncestorIds, array_values($dependentIdMap));
538
        // compile element differences to be resolved
539
        // remove elements that are persisted at the language translation, but not required anymore
540
        $removeIds = array_diff($persistedIds, array_values($dependentIdMap));
541
        // remove elements that are persisted at the language parent/source, but not required anymore
542
        $removeAncestorIds = array_diff(array_keys($dependentIdMap), $suggestedAncestorIds);
543
        // missing elements that are persisted at the language parent/source, but not translated yet
544
        $missingAncestorIds = array_diff($suggestedAncestorIds, array_keys($dependentIdMap));
545
        // persisted elements that should be copied or localized
546
        $createAncestorIds = $this->filterNumericIds($missingAncestorIds, true);
547
        // non-persisted elements that should be duplicated in data-map directly
548
        $populateAncestorIds = $this->filterNumericIds($missingAncestorIds, false);
549
        // this desired state map defines the final result of child elements in their parent translation
550
        $desiredIdMap = array_combine($suggestedAncestorIds, $suggestedAncestorIds);
551
        // update existing translations in the desired state map
552
        foreach ($dependentIdMap as $ancestorId => $translationId) {
553
            if (isset($desiredIdMap[$ancestorId])) {
554
                $desiredIdMap[$ancestorId] = $translationId;
555
            }
556
        }
557
        // no children to be synchronized, but element order could have been changed
558
        if (empty($removeAncestorIds) && empty($missingAncestorIds)) {
559
            $this->modifyDataMap(
560
                $item->getTableName(),
561
                $item->getId(),
562
                [$fieldName => implode(',', array_values($desiredIdMap))]
563
            );
564
            return;
565
        }
566
        // In case only missing elements shall be created, re-use previously sanitized
567
        // values IF the relation parent item is new and the count of missing relations
568
        // equals the count of previously sanitized relations.
569
        // This is caused during copy processes, when the child relations
570
        // already have been cloned in DataHandler::copyRecord_procBasedOnFieldType()
571
        // without the possibility to resolve the initial connections at this point.
572
        // Otherwise child relations would superfluously be duplicated again here.
573
        // @todo Invalid manually injected child relations cannot be determined here
574
        $sanitizedValue = $this->sanitizationMap[$item->getTableName()][$item->getId()][$fieldName] ?? null;
575
        if (
576
            !empty($missingAncestorIds) && $item->isNew() && $sanitizedValue !== null
577
            && count(GeneralUtility::trimExplode(',', $sanitizedValue, true)) === count($missingAncestorIds)
578
        ) {
579
            $this->modifyDataMap(
580
                $item->getTableName(),
581
                $item->getId(),
582
                [$fieldName => $sanitizedValue]
583
            );
584
            return;
585
        }
586
587
        $localCommandMap = [];
588
        foreach ($removeIds as $removeId) {
589
            $localCommandMap[$foreignTableName][$removeId]['delete'] = true;
590
        }
591
        foreach ($removeAncestorIds as $removeAncestorId) {
592
            $removeId = $dependentIdMap[$removeAncestorId];
593
            $localCommandMap[$foreignTableName][$removeId]['delete'] = true;
594
        }
595
        foreach ($createAncestorIds as $createAncestorId) {
596
            // if child table is not aware of localization, just copy
597
            if ($isLocalizationModeExclude || !$isTranslatable) {
598
                $localCommandMap[$foreignTableName][$createAncestorId]['copy'] = -$createAncestorId;
599
            } else {
600
                // otherwise, trigger the localization process
601
                $localCommandMap[$foreignTableName][$createAncestorId]['localize'] = $item->getLanguage();
602
            }
603
        }
604
        // execute copy, localize and delete actions on persisted child records
605
        if (!empty($localCommandMap)) {
606
            $localDataHandler = GeneralUtility::makeInstance(DataHandler::class);
607
            $localDataHandler->start([], $localCommandMap, $this->backendUser);
608
            $localDataHandler->process_cmdmap();
609
            // update copied or localized ids
610
            foreach ($createAncestorIds as $createAncestorId) {
611
                if (empty($localDataHandler->copyMappingArray_merged[$foreignTableName][$createAncestorId])) {
612
                    $additionalInformation = '';
613
                    if (!empty($localDataHandler->errorLog)) {
614
                        $additionalInformation = ', reason "'
615
                        . implode(', ', $localDataHandler->errorLog) . '"';
616
                    }
617
                    throw new \RuntimeException(
618
                        'Child record was not processed' . $additionalInformation,
619
                        1486233164
620
                    );
621
                }
622
                $newLocalizationId = $localDataHandler->copyMappingArray_merged[$foreignTableName][$createAncestorId];
623
                $newLocalizationId = $localDataHandler->getAutoVersionId($foreignTableName, $newLocalizationId) ?? $newLocalizationId;
624
                $desiredIdMap[$createAncestorId] = $newLocalizationId;
625
            }
626
        }
627
        // populate new child records in data-map
628
        if (!empty($populateAncestorIds)) {
629
            foreach ($populateAncestorIds as $populateAncestorId) {
630
                $newLocalizationId = StringUtility::getUniqueId('NEW');
631
                $desiredIdMap[$populateAncestorId] = $newLocalizationId;
632
                $duplicatedValues = $this->duplicateFromDataMap(
633
                    $foreignTableName,
634
                    $populateAncestorId,
635
                    $item->getLanguage(),
0 ignored issues
show
Bug introduced by
It seems like $item->getLanguage() can also be of type string; however, parameter $language of TYPO3\CMS\Core\DataHandl...:duplicateFromDataMap() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

635
                    /** @scrutinizer ignore-type */ $item->getLanguage(),
Loading history...
636
                    $fieldNames,
637
                    !$isLocalizationModeExclude && $isTranslatable
638
                );
639
                $this->modifyDataMap(
640
                    $foreignTableName,
641
                    $newLocalizationId,
642
                    $duplicatedValues
643
                );
644
            }
645
        }
646
        // update inline parent field references - required to update pointer fields
647
        $this->modifyDataMap(
648
            $item->getTableName(),
649
            $item->getId(),
650
            [$fieldName => implode(',', array_values($desiredIdMap))]
651
        );
652
    }
653
654
    /**
655
     * Determines suggest inline relations of either translation parent or
656
     * source record from data-map or storage in case records have been
657
     * persisted already.
658
     *
659
     * @param DataMapItem $item
660
     * @param string $fieldName
661
     * @param array $fromRecord
662
     * @return int[]|string[]
663
     */
664
    protected function resolveSuggestedInlineRelations(DataMapItem $item, string $fieldName, array $fromRecord): array
665
    {
666
        $suggestedAncestorIds = [];
667
        $fromId = $fromRecord['uid'];
668
        $configuration = $GLOBALS['TCA'][$item->getTableName()]['columns'][$fieldName];
669
        $foreignTableName = $configuration['config']['foreign_table'];
670
        $manyToManyTable = ($configuration['config']['MM'] ?? '');
671
672
        // determine suggested elements of either translation parent or source record
673
        // from data-map, in case the accordant language parent/source record was modified
674
        if ($this->isSetInDataMap($item->getTableName(), $fromId, $fieldName)) {
675
            $suggestedAncestorIds = GeneralUtility::trimExplode(
676
                ',',
677
                $this->allDataMap[$item->getTableName()][$fromId][$fieldName],
678
                true
679
            );
680
        } elseif (MathUtility::canBeInterpretedAsInteger($fromId)) {
681
            // determine suggested elements of either translation parent or source record from storage
682
            $relationHandler = $this->createRelationHandler();
683
            $relationHandler->start(
684
                $fromRecord[$fieldName],
685
                $foreignTableName,
686
                $manyToManyTable,
687
                $fromId,
688
                $item->getTableName(),
689
                $configuration['config']
690
            );
691
            $suggestedAncestorIds = $this->mapRelationItemId($relationHandler->itemArray);
692
        }
693
694
        return array_filter($suggestedAncestorIds);
695
    }
696
697
    /**
698
     * Determine persisted inline relations for current data-map-item.
699
     *
700
     * @param DataMapItem $item
701
     * @param string $fieldName
702
     * @param array $forRecord
703
     * @return int[]
704
     */
705
    private function resolvePersistedInlineRelations(DataMapItem $item, string $fieldName, array $forRecord): array
706
    {
707
        $persistedIds = [];
708
        $configuration = $GLOBALS['TCA'][$item->getTableName()]['columns'][$fieldName];
709
        $foreignTableName = $configuration['config']['foreign_table'];
710
        $manyToManyTable = ($configuration['config']['MM'] ?? '');
711
712
        // determine persisted elements for the current data-map item
713
        if (!$item->isNew()) {
714
            $relationHandler = $this->createRelationHandler();
715
            $relationHandler->start(
716
                $forRecord[$fieldName] ?? '',
717
                $foreignTableName,
718
                $manyToManyTable,
719
                $item->getId(),
0 ignored issues
show
Bug introduced by
It seems like $item->getId() can also be of type string; however, parameter $MMuid of TYPO3\CMS\Core\Database\RelationHandler::start() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

719
                /** @scrutinizer ignore-type */ $item->getId(),
Loading history...
720
                $item->getTableName(),
721
                $configuration['config']
722
            );
723
            $persistedIds = $this->mapRelationItemId($relationHandler->itemArray);
724
        }
725
726
        return array_filter($persistedIds);
727
    }
728
729
    /**
730
     * Determines whether a combination of table name, id and field name is
731
     * set in data-map. This method considers null values as well, that would
732
     * not be considered by a plain isset() invocation.
733
     *
734
     * @param string $tableName
735
     * @param string|int $id
736
     * @param string $fieldName
737
     * @return bool
738
     */
739
    protected function isSetInDataMap(string $tableName, $id, string $fieldName)
740
    {
741
        return
742
            // directly look-up field name
743
            isset($this->allDataMap[$tableName][$id][$fieldName])
744
            // check existence of field name as key for null values
745
            || isset($this->allDataMap[$tableName][$id])
746
            && is_array($this->allDataMap[$tableName][$id])
747
            && array_key_exists($fieldName, $this->allDataMap[$tableName][$id]);
748
    }
749
750
    /**
751
     * Applies modifications to the data-map, calling this method is essential
752
     * to determine new data-map items to be process for synchronizing chained
753
     * record localizations.
754
     *
755
     * @param string $tableName
756
     * @param string|int $id
757
     * @param array $values
758
     * @throws \RuntimeException
759
     */
760
    protected function modifyDataMap(string $tableName, $id, array $values)
761
    {
762
        // avoid superfluous iterations by data-map changes with values
763
        // that actually have not been changed and were available already
764
        $sameValues = array_intersect_assoc(
765
            $this->allDataMap[$tableName][$id] ?? [],
766
            $values
767
        );
768
        if (!empty($sameValues)) {
769
            $fieldNames = implode(', ', array_keys($sameValues));
770
            throw new \RuntimeException(
771
                sprintf(
772
                    'Issued data-map change for table %s with same values '
773
                    . 'for these fields names %s',
774
                    $tableName,
775
                    $fieldNames
776
                ),
777
                1488634845
778
            );
779
        }
780
781
        $this->modifiedDataMap[$tableName][$id] = array_merge(
782
            $this->modifiedDataMap[$tableName][$id] ?? [],
783
            $values
784
        );
785
        $this->allDataMap[$tableName][$id] = array_merge(
786
            $this->allDataMap[$tableName][$id] ?? [],
787
            $values
788
        );
789
    }
790
791
    /**
792
     * @param DataMapItem $item
793
     */
794
    protected function addNextItem(DataMapItem $item)
795
    {
796
        $identifier = $item->getTableName() . ':' . $item->getId();
797
        if (!isset($this->allItems[$identifier])) {
798
            $this->allItems[$identifier] = $item;
799
        }
800
        $this->nextItems[$identifier] = $item;
801
    }
802
803
    /**
804
     * Fetches translation related field values for the items submitted in
805
     * the data-map.
806
     *
807
     * @param string $tableName
808
     * @param array $fieldNames
809
     * @param array $ids
810
     * @return array
811
     */
812
    protected function fetchTranslationValues(string $tableName, array $fieldNames, array $ids)
813
    {
814
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
815
            ->getQueryBuilderForTable($tableName);
816
        $queryBuilder->getRestrictions()
817
            ->removeAll()
818
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
819
            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, $this->backendUser->workspace, false));
0 ignored issues
show
Bug introduced by
$this->backendUser->workspace of type integer is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

819
            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, /** @scrutinizer ignore-type */ $this->backendUser->workspace, false));
Loading history...
Bug introduced by
false of type false is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

819
            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, $this->backendUser->workspace, /** @scrutinizer ignore-type */ false));
Loading history...
820
        $statement = $queryBuilder
821
            ->select(...array_values($fieldNames))
822
            ->from($tableName)
823
            ->where(
824
                $queryBuilder->expr()->in(
825
                    'uid',
826
                    $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY)
827
                )
828
            )
829
            ->execute();
830
831
        $translationValues = [];
832
        foreach ($statement as $record) {
833
            $translationValues[$record['uid']] = $record;
834
        }
835
        return $translationValues;
836
    }
837
838
    /**
839
     * Fetches translation dependencies for a given parent/source record ids.
840
     *
841
     * Existing records in database:
842
     * + [uid:5, l10n_parent=0, l10n_source=0, sys_language_uid=0]
843
     * + [uid:6, l10n_parent=5, l10n_source=5, sys_language_uid=1]
844
     * + [uid:7, l10n_parent=5, l10n_source=6, sys_language_uid=2]
845
     *
846
     * Input $ids and their results:
847
     * + [5]   -> [DataMapItem(6), DataMapItem(7)] # since 5 is parent/source
848
     * + [6]   -> [DataMapItem(7)]                 # since 6 is source
849
     * + [7]   -> []                               # since there's nothing
850
     *
851
     * @param string $tableName
852
     * @param int[]|string[] $ids
853
     * @return DataMapItem[][]
854
     */
855
    protected function fetchDependencies(string $tableName, array $ids)
856
    {
857
        if (!BackendUtility::isTableLocalizable($tableName)) {
858
            return [];
859
        }
860
861
        $fieldNames = [
862
            'uid' => 'uid',
863
            'l10n_state' => 'l10n_state',
864
            'language' => $GLOBALS['TCA'][$tableName]['ctrl']['languageField'],
865
            'parent' => $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'],
866
        ];
867 View Code Duplication
        if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['translationSource'])) {
868
            $fieldNames['source'] = $GLOBALS['TCA'][$tableName]['ctrl']['translationSource'];
869
        }
870
        $fieldNamesMap = array_combine($fieldNames, $fieldNames);
871
872
        $persistedIds = $this->filterNumericIds($ids, true);
873
        $createdIds = $this->filterNumericIds($ids, false);
874
        $dependentElements = $this->fetchDependentElements($tableName, $persistedIds, $fieldNames);
875
876
        foreach ($createdIds as $createdId) {
877
            $data = $this->allDataMap[$tableName][$createdId] ?? null;
878
            if ($data === null) {
879
                continue;
880
            }
881
            $dependentElements[] = array_merge(
882
                ['uid' => $createdId],
883
                array_intersect_key($data, $fieldNamesMap)
884
            );
885
        }
886
887
        $dependencyMap = [];
888
        foreach ($dependentElements as $dependentElement) {
889
            $dependentItem = DataMapItem::build(
890
                $tableName,
891
                $dependentElement['uid'],
892
                [],
893
                $dependentElement,
894
                $fieldNames
895
            );
896
897
            if ($dependentItem->isDirectChildType()) {
898
                $dependencyMap[$dependentItem->getParent()][State::STATE_PARENT][] = $dependentItem;
899
            }
900
            if ($dependentItem->isGrandChildType()) {
901
                $dependencyMap[$dependentItem->getParent()][State::STATE_PARENT][] = $dependentItem;
902
                $dependencyMap[$dependentItem->getSource()][State::STATE_SOURCE][] = $dependentItem;
903
            }
904
        }
905
        return $dependencyMap;
906
    }
907
908
    /**
909
     * Fetches dependent records that depend on given record id's in in either
910
     * their parent or source field for translatable tables or their origin
911
     * field for non-translatable tables and creates an id mapping.
912
     *
913
     * This method expands the search criteria by expanding to ancestors.
914
     *
915
     * Existing records in database:
916
     * + [uid:5, l10n_parent=0, l10n_source=0, sys_language_uid=0]
917
     * + [uid:6, l10n_parent=5, l10n_source=5, sys_language_uid=1]
918
     * + [uid:7, l10n_parent=5, l10n_source=6, sys_language_uid=2]
919
     *
920
     * Input $ids and $desiredLanguage and their results:
921
     * + $ids=[5], $lang=1 -> [5 => 6] # since 5 is source of 6
922
     * + $ids=[5], $lang=2 -> []       # since 5 is parent of 7, but different language
923
     * + $ids=[6], $lang=1 -> []       # since there's nothing
924
     * + $ids=[6], $lang=2 -> [6 => 7] # since 6 has source 5, which is ancestor of 7
925
     * + $ids=[7], $lang=* -> []       # since there's nothing
926
     *
927
     * @param string $tableName
928
     * @param array $ids
929
     * @param int $desiredLanguage
930
     * @return array
931
     */
932
    protected function fetchDependentIdMap(string $tableName, array $ids, int $desiredLanguage)
933
    {
934
        $ids = $this->filterNumericIds($ids, true);
935
        $isTranslatable = BackendUtility::isTableLocalizable($tableName);
936
        $originFieldName = ($GLOBALS['TCA'][$tableName]['ctrl']['origUid'] ?? null);
937
938
        if (!$isTranslatable && $originFieldName === null) {
939
            return [];
940
        }
941
942
        if ($isTranslatable) {
943
            $fieldNames = [
944
                'uid' => 'uid',
945
                'l10n_state' => 'l10n_state',
946
                'language' => $GLOBALS['TCA'][$tableName]['ctrl']['languageField'],
947
                'parent' => $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'],
948
            ];
949 View Code Duplication
            if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['translationSource'])) {
950
                $fieldNames['source'] = $GLOBALS['TCA'][$tableName]['ctrl']['translationSource'];
951
            }
952
        } else {
953
            $fieldNames = [
954
                'uid' => 'uid',
955
                'origin' => $originFieldName,
956
            ];
957
        }
958
959
        $fetchIds = $ids;
960
        if ($isTranslatable) {
961
            // expand search criteria via parent and source elements
962
            $translationValues = $this->fetchTranslationValues($tableName, $fieldNames, $ids);
963
            $ancestorIdMap = $this->buildElementAncestorIdMap($fieldNames, $translationValues);
964
            $fetchIds = array_unique(array_merge($ids, array_keys($ancestorIdMap)));
965
        }
966
967
        $dependentElements = $this->fetchDependentElements($tableName, $fetchIds, $fieldNames);
968
969
        $dependentIdMap = [];
970
        foreach ($dependentElements as $dependentElement) {
971
            $dependentId = $dependentElement['uid'];
972
            // implicit: use origin pointer if table cannot be translated
973
            if (!$isTranslatable) {
974
                $ancestorId = (int)$dependentElement[$fieldNames['origin']];
975
                // only consider element if it reflects the desired language
976
            } elseif ((int)$dependentElement[$fieldNames['language']] === $desiredLanguage) {
977
                $ancestorId = $this->resolveAncestorId($fieldNames, $dependentElement);
978
            } else {
979
                // otherwise skip the element completely
980
                continue;
981
            }
982
            // only keep ancestors that were initially requested before expanding
983
            if (in_array($ancestorId, $ids)) {
984
                $dependentIdMap[$ancestorId] = $dependentId;
985
            } elseif (!empty($ancestorIdMap[$ancestorId])) {
986
                // resolve from previously expanded search criteria
987
                $possibleChainedIds = array_intersect(
988
                    $ids,
989
                    $ancestorIdMap[$ancestorId]
990
                );
991
                if (!empty($possibleChainedIds)) {
992
                    $ancestorId = $possibleChainedIds[0];
993
                    $dependentIdMap[$ancestorId] = $dependentId;
994
                }
995
            }
996
        }
997
        return $dependentIdMap;
998
    }
999
1000
    /**
1001
     * Fetch all elements that depend on given record id's in either their
1002
     * parent or source field for translatable tables or their origin field
1003
     * for non-translatable tables.
1004
     *
1005
     * @param string $tableName
1006
     * @param array $ids
1007
     * @param array $fieldNames
1008
     * @return array
1009
     * @throws \InvalidArgumentException
1010
     */
1011
    protected function fetchDependentElements(string $tableName, array $ids, array $fieldNames)
1012
    {
1013
        $ids = $this->filterNumericIds($ids, true);
1014
1015
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1016
            ->getQueryBuilderForTable($tableName);
1017
        $queryBuilder->getRestrictions()
1018
            ->removeAll()
1019
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1020
            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, $this->backendUser->workspace, false));
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1020
            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, $this->backendUser->workspace, /** @scrutinizer ignore-type */ false));
Loading history...
Bug introduced by
$this->backendUser->workspace of type integer is incompatible with the type array<integer,mixed> expected by parameter $constructorArguments of TYPO3\CMS\Core\Utility\G...Utility::makeInstance(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1020
            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, /** @scrutinizer ignore-type */ $this->backendUser->workspace, false));
Loading history...
1021
1022
        $zeroParameter = $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT);
1023
        $ids = array_filter($ids, [MathUtility::class, 'canBeInterpretedAsInteger']);
1024
        $idsParameter = $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY);
1025
1026
        // fetch by language dependency
1027
        if (!empty($fieldNames['language']) && !empty($fieldNames['parent'])) {
1028
            $ancestorPredicates = [
1029
                $queryBuilder->expr()->in(
1030
                    $fieldNames['parent'],
1031
                    $idsParameter
1032
                )
1033
            ];
1034
            if (!empty($fieldNames['source'])) {
1035
                $ancestorPredicates[] = $queryBuilder->expr()->in(
1036
                    $fieldNames['source'],
1037
                    $idsParameter
1038
                );
1039
            }
1040
            $predicates = [
1041
                // must be any kind of localization
1042
                $queryBuilder->expr()->gt(
1043
                    $fieldNames['language'],
1044
                    $zeroParameter
1045
                ),
1046
                // must be in connected mode
1047
                $queryBuilder->expr()->gt(
1048
                    $fieldNames['parent'],
1049
                    $zeroParameter
1050
                ),
1051
                // any parent or source pointers
1052
                $queryBuilder->expr()->orX(...$ancestorPredicates),
1053
            ];
1054
        } elseif (!empty($fieldNames['origin'])) {
1055
            // fetch by origin dependency ("copied from")
1056
            $predicates = [
1057
                $queryBuilder->expr()->in(
1058
                    $fieldNames['origin'],
1059
                    $idsParameter
1060
                )
1061
            ];
1062
        } else {
1063
            // otherwise: stop execution
1064
            throw new \InvalidArgumentException(
1065
                'Invalid combination of query field names given',
1066
                1487192370
1067
            );
1068
        }
1069
1070
        $statement = $queryBuilder
1071
            ->select(...array_values($fieldNames))
1072
            ->from($tableName)
1073
            ->andWhere(...$predicates)
1074
            ->execute();
1075
1076
        $dependentElements = [];
1077
        foreach ($statement as $record) {
1078
            $dependentElements[] = $record;
1079
        }
1080
        return $dependentElements;
1081
    }
1082
1083
    /**
1084
     * Return array of data map items that are of given type
1085
     *
1086
     * @param string $type
1087
     * @param DataMapItem[] $items
1088
     * @return DataMapItem[]
1089
     */
1090
    protected function filterItemsByType(string $type, array $items)
1091
    {
1092
        return array_filter(
1093
            $items,
1094
            function (DataMapItem $item) use ($type) {
1095
                return $item->getType() === $type;
1096
            }
1097
        );
1098
    }
1099
1100
    /**
1101
     * Return only ids that are integer - so no "NEW..." values
1102
     *
1103
     * @param string[]|int[] $ids
1104
     * @param bool $numeric
1105
     * @return int[]|string[]
1106
     */
1107
    protected function filterNumericIds(array $ids, bool $numeric = true)
1108
    {
1109
        return array_filter(
1110
            $ids,
1111
            function ($id) use ($numeric) {
1112
                return MathUtility::canBeInterpretedAsInteger($id) === $numeric;
1113
            }
1114
        );
1115
    }
1116
1117
    /**
1118
     * Return only ids that don't have an item equivalent in $this->allItems.
1119
     *
1120
     * @param string $tableName
1121
     * @param int[] $ids
1122
     * @return array
1123
     */
1124
    protected function filterNewItemIds(string $tableName, array $ids)
1125
    {
1126
        return array_filter(
1127
            $ids,
1128
            function ($id) use ($tableName) {
1129
                return $this->findItem($tableName, $id) === null;
1130
            }
1131
        );
1132
    }
1133
1134
    /**
1135
     * Flatten array
1136
     *
1137
     * @param array $relationItems
1138
     * @return string[]
1139
     */
1140
    protected function mapRelationItemId(array $relationItems)
1141
    {
1142
        return array_map(
1143
            function (array $relationItem) {
1144
                return (int)$relationItem['id'];
1145
            },
1146
            $relationItems
1147
        );
1148
    }
1149
1150
    /**
1151
     * @param array $fieldNames
1152
     * @param array $element
1153
     * @return int|null
1154
     */
1155
    protected function resolveAncestorId(array $fieldNames, array $element)
1156
    {
1157
        // implicit: having source value different to parent value, use source pointer
1158
        if (
1159
            !empty($fieldNames['source'])
1160
            && $element[$fieldNames['source']] !== $element[$fieldNames['parent']]
1161
        ) {
1162
            return (int)$fieldNames['source'];
1163
        }
1164
        if (!empty($fieldNames['parent'])) {
1165
            // implicit: use parent pointer if defined
1166
            return (int)$element[$fieldNames['parent']];
1167
        }
1168
        return null;
1169
    }
1170
1171
    /**
1172
     * Builds a map from ancestor ids to accordant localization dependents.
1173
     *
1174
     * The result of e.g. [5 => [6, 7]] refers to ids 6 and 7 being dependents
1175
     * (either used in parent or source field) of the ancestor with id 5.
1176
     *
1177
     * @param array $fieldNames
1178
     * @param array $elements
1179
     * @return array
1180
     */
1181
    protected function buildElementAncestorIdMap(array $fieldNames, array $elements)
1182
    {
1183
        $ancestorIdMap = [];
1184
        foreach ($elements as $element) {
1185
            $ancestorId = $this->resolveAncestorId($fieldNames, $element);
1186
            if ($ancestorId !== null) {
1187
                $ancestorIdMap[$ancestorId][] = (int)$element['uid'];
1188
            }
1189
        }
1190
        return $ancestorIdMap;
1191
    }
1192
1193
    /**
1194
     * See if an items is in item list and return it
1195
     *
1196
     * @param string $tableName
1197
     * @param string|int $id
1198
     * @return DataMapItem|null
1199
     */
1200
    protected function findItem(string $tableName, $id)
1201
    {
1202
        return $this->allItems[$tableName . ':' . $id] ?? null;
1203
    }
1204
1205
    /**
1206
     * Duplicates an item from data-map and prefixed language title,
1207
     * if applicable for the accordant field name.
1208
     *
1209
     * @param string $tableName
1210
     * @param string|int $fromId
1211
     * @param int $language
1212
     * @param array $fieldNames
1213
     * @param bool $localize
1214
     * @return array
1215
     */
1216
    protected function duplicateFromDataMap(string $tableName, $fromId, int $language, array $fieldNames, bool $localize)
1217
    {
1218
        $data = $this->allDataMap[$tableName][$fromId];
1219
        // just return duplicated item if localization cannot be applied
1220
        if (empty($language) || !$localize) {
1221
            return $data;
1222
        }
1223
1224
        $data[$fieldNames['language']] = $language;
1225
        if (empty($data[$fieldNames['parent']])) {
1226
            // @todo Only $id used in TCA type 'select' is resolved in DataHandler's remapStack
1227
            $data[$fieldNames['parent']] = $fromId;
1228
        }
1229
        if (!empty($fieldNames['source'])) {
1230
            // @todo Not sure, whether $id is resolved in DataHandler's remapStack
1231
            $data[$fieldNames['source']] = $fromId;
1232
        }
1233
        // unset field names that are expected to be handled in this processor
1234
        foreach ($this->getFieldNamesToBeHandled($tableName) as $fieldName) {
1235
            unset($data[$fieldName]);
1236
        }
1237
1238
        $prefixFieldNames = array_intersect(
1239
            array_keys($data),
1240
            $this->getPrefixLanguageTitleFieldNames($tableName)
1241
        );
1242
        if (empty($prefixFieldNames)) {
1243
            return $data;
1244
        }
1245
1246
        $languageService = $this->getLanguageService();
1247
        $languageRecord = BackendUtility::getRecord('sys_language', $language, 'title');
1248
        list($pageId) = BackendUtility::getTSCpid($tableName, $fromId, $data['pid'] ?? null);
0 ignored issues
show
Bug introduced by
It seems like $fromId can also be of type string; however, parameter $uid of TYPO3\CMS\Backend\Utilit...endUtility::getTSCpid() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1248
        list($pageId) = BackendUtility::getTSCpid($tableName, /** @scrutinizer ignore-type */ $fromId, $data['pid'] ?? null);
Loading history...
1249
1250
        $TSconfig =  $this->backendUser->getTSConfig(
1251
            'TCEMAIN',
1252
            BackendUtility::getPagesTSconfig($pageId)
1253
        );
1254
        if (!empty($TSconfig['translateToMessage'])) {
1255
            $prefix = $TSconfig['translateToMessage'];
1256
            if ($languageService !== null) {
1257
                $prefix = $languageService->sL($prefix);
1258
            }
1259
            $prefix = sprintf($prefix, $languageRecord['title']);
1260
        }
1261
        if (empty($prefix)) {
1262
            $prefix = 'Translate to ' . $languageRecord['title'] . ':';
1263
        }
1264
1265
        foreach ($prefixFieldNames as $prefixFieldName) {
1266
            // @todo The hook in DataHandler is not applied here
1267
            $data[$prefixFieldName] = '[' . $prefix . '] ' . $data[$prefixFieldName];
1268
        }
1269
1270
        return $data;
1271
    }
1272
1273
    /**
1274
     * Field names we have to deal with
1275
     *
1276
     * @param DataMapItem $item
1277
     * @param string $scope
1278
     * @param bool $modified
1279
     * @return string[]
1280
     */
1281
    protected function getFieldNamesForItemScope(
1282
        DataMapItem $item,
1283
        string $scope,
1284
        bool $modified
1285
    ) {
1286
        if (
1287
            $scope === DataMapItem::SCOPE_PARENT
1288
            || $scope === DataMapItem::SCOPE_SOURCE
1289
        ) {
1290
            if (!State::isApplicable($item->getTableName())) {
1291
                return [];
1292
            }
1293
            return $item->getState()->filterFieldNames($scope, $modified);
1294
        }
1295
        if ($scope === DataMapItem::SCOPE_EXCLUDE) {
1296
            return $this->getLocalizationModeExcludeFieldNames(
1297
                $item->getTableName()
1298
            );
1299
        }
1300
        return [];
1301
    }
1302
1303
    /**
1304
     * Field names of TCA table with columns having l10n_mode=exclude
1305
     *
1306
     * @param string $tableName
1307
     * @return string[]
1308
     */
1309
    protected function getLocalizationModeExcludeFieldNames(string $tableName)
1310
    {
1311
        $localizationExcludeFieldNames = [];
1312
        if (empty($GLOBALS['TCA'][$tableName]['columns'])) {
1313
            return $localizationExcludeFieldNames;
1314
        }
1315
1316
        foreach ($GLOBALS['TCA'][$tableName]['columns'] as $fieldName => $configuration) {
1317
            if (($configuration['l10n_mode'] ?? null) === 'exclude') {
1318
                $localizationExcludeFieldNames[] = $fieldName;
1319
            }
1320
        }
1321
1322
        return $localizationExcludeFieldNames;
1323
    }
1324
1325
    /**
1326
     * Gets a list of field names which have to be handled. Basically this
1327
     * includes fields using allowLanguageSynchronization or l10n_mode=exclude.
1328
     *
1329
     * @param string $tableName
1330
     * @return string[]
1331
     */
1332
    protected function getFieldNamesToBeHandled(string $tableName)
1333
    {
1334
        return array_merge(
1335
            State::getFieldNames($tableName),
1336
            $this->getLocalizationModeExcludeFieldNames($tableName)
1337
        );
1338
    }
1339
1340
    /**
1341
     * Field names of TCA table with columns having l10n_mode=prefixLangTitle
1342
     *
1343
     * @param string $tableName
1344
     * @return array
1345
     */
1346
    protected function getPrefixLanguageTitleFieldNames(string $tableName)
1347
    {
1348
        $prefixLanguageTitleFieldNames = [];
1349
        if (empty($GLOBALS['TCA'][$tableName]['columns'])) {
1350
            return $prefixLanguageTitleFieldNames;
1351
        }
1352
1353
        foreach ($GLOBALS['TCA'][$tableName]['columns'] as $fieldName => $configuration) {
1354
            $type = $configuration['config']['type'] ?? null;
1355
            if (
1356
                ($configuration['l10n_mode'] ?? null) === 'prefixLangTitle'
1357
                && ($type === 'input' || $type === 'text')
1358
            ) {
1359
                $prefixLanguageTitleFieldNames[] = $fieldName;
1360
            }
1361
        }
1362
1363
        return $prefixLanguageTitleFieldNames;
1364
    }
1365
1366
    /**
1367
     * True if we're dealing with a field that has foreign db relations
1368
     *
1369
     * @param string $tableName
1370
     * @param string $fieldName
1371
     * @return bool True if field is type=group with internalType === db or select with foreign_table
1372
     */
1373
    protected function isRelationField(string $tableName, string $fieldName): bool
1374
    {
1375 View Code Duplication
        if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']['type'])) {
1376
            return false;
1377
        }
1378
1379
        $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1380
1381
        return
1382
            $configuration['type'] === 'group'
1383
                && ($configuration['internal_type'] ?? null) === 'db'
1384
                && !empty($configuration['allowed'])
1385
            || $configuration['type'] === 'select'
1386
                && (
1387
                    !empty($configuration['foreign_table'])
1388
                        && !empty($GLOBALS['TCA'][$configuration['foreign_table']])
1389
                    || ($configuration['special'] ?? null) === 'languages'
1390
                )
1391
            || $this->isInlineRelationField($tableName, $fieldName)
1392
        ;
1393
    }
1394
1395
    /**
1396
     * True if we're dealing with an inline field
1397
     *
1398
     * @param string $tableName
1399
     * @param string $fieldName
1400
     * @return bool TRUE if field is of type inline with foreign_table set
1401
     */
1402
    protected function isInlineRelationField(string $tableName, string $fieldName): bool
1403
    {
1404 View Code Duplication
        if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']['type'])) {
1405
            return false;
1406
        }
1407
1408
        $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1409
1410
        return
1411
            $configuration['type'] === 'inline'
1412
            && !empty($configuration['foreign_table'])
1413
            && !empty($GLOBALS['TCA'][$configuration['foreign_table']])
1414
        ;
1415
    }
1416
1417
    /**
1418
     * Determines whether the table can be localized and either has fields
1419
     * with allowLanguageSynchronization enabled or l10n_mode set to exclude.
1420
     *
1421
     * @param string $tableName
1422
     * @return bool
1423
     */
1424
    protected function isApplicable(string $tableName): bool
1425
    {
1426
        return
1427
            State::isApplicable($tableName)
1428
            || BackendUtility::isTableLocalizable($tableName)
1429
                && count($this->getLocalizationModeExcludeFieldNames($tableName)) > 0
1430
        ;
1431
    }
1432
1433
    /**
1434
     * @return RelationHandler
1435
     */
1436
    protected function createRelationHandler()
1437
    {
1438
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
1439
        $relationHandler->setWorkspaceId($this->backendUser->workspace);
1440
        return $relationHandler;
1441
    }
1442
1443
    /**
1444
     * @return LanguageService|null
1445
     */
1446
    protected function getLanguageService()
1447
    {
1448
        return $GLOBALS['LANG'] ?? null;
1449
    }
1450
}
1451