RelationHandler::writeMM()   F
last analyzed

Complexity

Conditions 38
Paths > 20000

Size

Total Lines 231
Code Lines 145

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 38
eloc 145
nc 840962
nop 3
dl 0
loc 231
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Core\Database;
17
18
use TYPO3\CMS\Backend\Utility\BackendUtility;
19
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
20
use TYPO3\CMS\Core\Database\Platform\PlatformInformation;
21
use TYPO3\CMS\Core\Database\Query\QueryHelper;
22
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
23
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
24
use TYPO3\CMS\Core\DataHandling\PlainDataResolver;
25
use TYPO3\CMS\Core\DataHandling\ReferenceIndexUpdater;
26
use TYPO3\CMS\Core\Utility\GeneralUtility;
27
use TYPO3\CMS\Core\Utility\MathUtility;
28
use TYPO3\CMS\Core\Versioning\VersionState;
29
30
/**
31
 * Load database groups (relations)
32
 * Used to process the relations created by the TCA element types "group" and "select" for database records.
33
 * Manages MM-relations as well.
34
 */
35
class RelationHandler
36
{
37
    /**
38
     * $fetchAllFields if false getFromDB() fetches only uid, pid, thumbnail and label fields (as defined in TCA)
39
     *
40
     * @var bool
41
     */
42
    protected $fetchAllFields = false;
43
44
    /**
45
     * If set, values that are not ids in tables are normally discarded. By this options they will be preserved.
46
     *
47
     * @var bool
48
     */
49
    public $registerNonTableValues = false;
50
51
    /**
52
     * Contains the table names as keys. The values are the id-values for each table.
53
     * Should ONLY contain proper table names.
54
     *
55
     * @var array
56
     */
57
    public $tableArray = [];
58
59
    /**
60
     * Contains items in a numeric array (table/id for each). Tablenames here might be "_NO_TABLE"
61
     *
62
     * @var array<int, array<string, mixed>>
63
     */
64
    public $itemArray = [];
65
66
    /**
67
     * Array for NON-table elements
68
     *
69
     * @var array
70
     */
71
    public $nonTableArray = [];
72
73
    /**
74
     * @var array
75
     */
76
    public $additionalWhere = [];
77
78
    /**
79
     * Deleted-column is added to additionalWhere... if this is set...
80
     *
81
     * @var bool
82
     */
83
    public $checkIfDeleted = true;
84
85
    /**
86
     * Will contain the first table name in the $tablelist (for positive ids)
87
     *
88
     * @var string
89
     */
90
    protected $firstTable = '';
91
92
    /**
93
     * If TRUE, uid_local and uid_foreign are switched, and the current table
94
     * is inserted as tablename - this means you display a foreign relation "from the opposite side"
95
     *
96
     * @var bool
97
     */
98
    protected $MM_is_foreign = false;
99
100
    /**
101
     * Is empty by default; if MM_is_foreign is set and there is more than one table
102
     * allowed (on the "local" side), then it contains the first table (as a fallback)
103
     * @var string
104
     */
105
    protected $MM_isMultiTableRelationship = '';
106
107
    /**
108
     * Current table => Only needed for reverse relations
109
     *
110
     * @var string
111
     */
112
    protected $currentTable;
113
114
    /**
115
     * If a record should be undeleted
116
     * (so do not use the $useDeleteClause on \TYPO3\CMS\Backend\Utility\BackendUtility)
117
     *
118
     * @var bool
119
     */
120
    public $undeleteRecord;
121
122
    /**
123
     * Array of fields value pairs that should match while SELECT
124
     * and will be written into MM table if $MM_insert_fields is not set
125
     *
126
     * @var array
127
     */
128
    protected $MM_match_fields = [];
129
130
    /**
131
     * This is set to TRUE if the MM table has a UID field.
132
     *
133
     * @var bool
134
     */
135
    protected $MM_hasUidField;
136
137
    /**
138
     * Array of fields and value pairs used for insert in MM table
139
     *
140
     * @var array
141
     */
142
    protected $MM_insert_fields = [];
143
144
    /**
145
     * Extra MM table where
146
     *
147
     * @var string
148
     */
149
    protected $MM_table_where = '';
150
151
    /**
152
     * Usage of a MM field on the opposite relation.
153
     *
154
     * @var array
155
     */
156
    protected $MM_oppositeUsage;
157
158
    /**
159
     * If false, reference index is not updated.
160
     *
161
     * @var bool
162
     * @deprecated since v11, will be removed in v12
163
     */
164
    protected $updateReferenceIndex = true;
165
166
    /**
167
     * @var ReferenceIndexUpdater|null
168
     */
169
    protected $referenceIndexUpdater;
170
171
    /**
172
     * @var bool
173
     */
174
    protected $useLiveParentIds = true;
175
176
    /**
177
     * @var bool
178
     */
179
    protected $useLiveReferenceIds = true;
180
181
    /**
182
     * @var int|null
183
     */
184
    protected $workspaceId;
185
186
    /**
187
     * @var bool
188
     */
189
    protected $purged = false;
190
191
    /**
192
     * This array will be filled by getFromDB().
193
     *
194
     * @var array
195
     */
196
    public $results = [];
197
198
    /**
199
     * Gets the current workspace id.
200
     *
201
     * @return int
202
     */
203
    protected function getWorkspaceId(): int
204
    {
205
        $backendUser = $GLOBALS['BE_USER'] ?? null;
206
        if (!isset($this->workspaceId)) {
207
            $this->workspaceId = $backendUser instanceof BackendUserAuthentication ? (int)($backendUser->workspace) : 0;
208
        }
209
        return $this->workspaceId;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->workspaceId could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
210
    }
211
212
    /**
213
     * Sets the current workspace id.
214
     *
215
     * @param int $workspaceId
216
     */
217
    public function setWorkspaceId($workspaceId): void
218
    {
219
        $this->workspaceId = (int)$workspaceId;
220
    }
221
222
    /**
223
     * Setter to carry the 'deferred' reference index updater registry around.
224
     *
225
     * @param ReferenceIndexUpdater $updater
226
     * @internal Used internally within DataHandler only
227
     */
228
    public function setReferenceIndexUpdater(ReferenceIndexUpdater $updater): void
229
    {
230
        $this->referenceIndexUpdater = $updater;
231
    }
232
233
    /**
234
     * Whether item array has been purged in this instance.
235
     *
236
     * @return bool
237
     */
238
    public function isPurged()
239
    {
240
        return $this->purged;
241
    }
242
243
    /**
244
     * Initialization of the class.
245
     *
246
     * @param string $itemlist List of group/select items
247
     * @param string $tablelist Comma list of tables, first table takes priority if no table is set for an entry in the list.
248
     * @param string $MMtable Name of a MM table.
249
     * @param int|string $MMuid Local UID for MM lookup. May be a string for newly created elements.
250
     * @param string $currentTable Current table name
251
     * @param array $conf TCA configuration for current field
252
     */
253
    public function start($itemlist, $tablelist, $MMtable = '', $MMuid = 0, $currentTable = '', $conf = [])
254
    {
255
        $conf = (array)$conf;
256
        // SECTION: MM reverse relations
257
        $this->MM_is_foreign = (bool)($conf['MM_opposite_field'] ?? false);
258
        $this->MM_table_where = $conf['MM_table_where'] ?? null;
259
        $this->MM_hasUidField = $conf['MM_hasUidField'] ?? null;
260
        $this->MM_match_fields = (isset($conf['MM_match_fields']) && is_array($conf['MM_match_fields'])) ? $conf['MM_match_fields'] : [];
261
        $this->MM_insert_fields = (isset($conf['MM_insert_fields']) && is_array($conf['MM_insert_fields'])) ? $conf['MM_insert_fields'] : $this->MM_match_fields;
262
        $this->currentTable = $currentTable;
263
        if (!empty($conf['MM_oppositeUsage']) && is_array($conf['MM_oppositeUsage'])) {
264
            $this->MM_oppositeUsage = $conf['MM_oppositeUsage'];
265
        }
266
        $mmOppositeTable = '';
267
        if ($this->MM_is_foreign) {
268
            $allowedTableList = $conf['type'] === 'group' ? $conf['allowed'] : $conf['foreign_table'];
269
            // Normally, $conf['allowed'] can contain a list of tables,
270
            // but as we are looking at a MM relation from the foreign side,
271
            // it only makes sense to allow one table in $conf['allowed'].
272
            [$mmOppositeTable] = GeneralUtility::trimExplode(',', $allowedTableList);
273
            // Only add the current table name if there is more than one allowed
274
            // field. We must be sure this has been done at least once before accessing
275
            // the "columns" part of TCA for a table.
276
            $mmOppositeAllowed = (string)($GLOBALS['TCA'][$mmOppositeTable]['columns'][$conf['MM_opposite_field'] ?? '']['config']['allowed'] ?? '');
277
            if ($mmOppositeAllowed !== '') {
278
                $mmOppositeAllowedTables = explode(',', $mmOppositeAllowed);
279
                if ($mmOppositeAllowed === '*' || count($mmOppositeAllowedTables) > 1) {
280
                    $this->MM_isMultiTableRelationship = $mmOppositeAllowedTables[0];
281
                }
282
            }
283
        }
284
        // SECTION:	normal MM relations
285
        // If the table list is "*" then all tables are used in the list:
286
        if (trim($tablelist) === '*') {
287
            $tablelist = implode(',', array_keys($GLOBALS['TCA']));
288
        }
289
        // The tables are traversed and internal arrays are initialized:
290
        $tempTableArray = GeneralUtility::trimExplode(',', $tablelist, true);
291
        foreach ($tempTableArray as $val) {
292
            $tName = trim($val);
293
            $this->tableArray[$tName] = [];
294
            $deleteField = $GLOBALS['TCA'][$tName]['ctrl']['delete'] ?? false;
295
            if ($this->checkIfDeleted && $deleteField) {
296
                $fieldN = $tName . '.' . $deleteField;
297
                if (!isset($this->additionalWhere[$tName])) {
298
                    $this->additionalWhere[$tName] = '';
299
                }
300
                $this->additionalWhere[$tName] .= ' AND ' . $fieldN . '=0';
301
            }
302
        }
303
        if (is_array($this->tableArray)) {
0 ignored issues
show
introduced by
The condition is_array($this->tableArray) is always true.
Loading history...
304
            reset($this->tableArray);
305
        } else {
306
            // No tables
307
            return;
308
        }
309
        // Set first and second tables:
310
        // Is the first table
311
        $this->firstTable = (string)key($this->tableArray);
312
        next($this->tableArray);
313
        // Now, populate the internal itemArray and tableArray arrays:
314
        // If MM, then call this function to do that:
315
        if ($MMtable) {
316
            if ($MMuid) {
317
                $this->readMM($MMtable, $MMuid, $mmOppositeTable);
318
                $this->purgeItemArray();
319
            } else {
320
                // Revert to readList() for new records in order to load possible default values from $itemlist
321
                $this->readList($itemlist, $conf);
322
                $this->purgeItemArray();
323
            }
324
        } elseif ($MMuid && ($conf['foreign_field'] ?? false)) {
325
            // If not MM but foreign_field, the read the records by the foreign_field
326
            $this->readForeignField($MMuid, $conf);
327
        } else {
328
            // If not MM, then explode the itemlist by "," and traverse the list:
329
            $this->readList($itemlist, $conf);
330
            // Do automatic default_sortby, if any
331
            if (isset($conf['foreign_default_sortby']) && $conf['foreign_default_sortby']) {
332
                $this->sortList($conf['foreign_default_sortby']);
333
            }
334
        }
335
    }
336
337
    /**
338
     * Sets $fetchAllFields
339
     *
340
     * @param bool $allFields enables fetching of all fields in getFromDB()
341
     */
342
    public function setFetchAllFields($allFields)
343
    {
344
        $this->fetchAllFields = (bool)$allFields;
345
    }
346
347
    /**
348
     * Sets whether the reference index shall be updated.
349
     *
350
     * @param bool $updateReferenceIndex Whether the reference index shall be updated
351
     * @deprecated since v11, will be removed in v12
352
     */
353
    public function setUpdateReferenceIndex($updateReferenceIndex)
354
    {
355
        trigger_error(
356
            'Calling RelationHandler->setUpdateReferenceIndex() is deprecated. Use setReferenceIndexUpdater() instead.',
357
            E_USER_DEPRECATED
358
        );
359
        $this->updateReferenceIndex = (bool)$updateReferenceIndex;
360
    }
361
362
    /**
363
     * @param bool $useLiveParentIds
364
     */
365
    public function setUseLiveParentIds($useLiveParentIds)
366
    {
367
        $this->useLiveParentIds = (bool)$useLiveParentIds;
368
    }
369
370
    /**
371
     * @param bool $useLiveReferenceIds
372
     */
373
    public function setUseLiveReferenceIds($useLiveReferenceIds)
374
    {
375
        $this->useLiveReferenceIds = (bool)$useLiveReferenceIds;
376
    }
377
378
    /**
379
     * Explodes the item list and stores the parts in the internal arrays itemArray and tableArray from MM records.
380
     *
381
     * @param string $itemlist Item list
382
     * @param array $configuration Parent field configuration
383
     */
384
    protected function readList($itemlist, array $configuration)
385
    {
386
        if ((string)trim($itemlist) != '') {
387
            // Changed to trimExplode 31/3 04; HMENU special type "list" didn't work
388
            // if there were spaces in the list... I suppose this is better overall...
389
            $tempItemArray = GeneralUtility::trimExplode(',', $itemlist);
390
            // If the second table is set and the ID number is less than zero (later)
391
            // then the record is regarded to come from the second table...
392
            $secondTable = (string)(key($this->tableArray) ?? '');
393
            foreach ($tempItemArray as $key => $val) {
394
                // Will be set to "true" if the entry was a real table/id
395
                $isSet = false;
396
                // Extract table name and id. This is in the formula [tablename]_[id]
397
                // where table name MIGHT contain "_", hence the reversion of the string!
398
                $val = strrev($val);
399
                $parts = explode('_', $val, 2);
400
                $theID = strrev($parts[0]);
401
                // Check that the id IS an integer:
402
                if (MathUtility::canBeInterpretedAsInteger($theID)) {
403
                    // Get the table name: If a part of the exploded string, use that.
404
                    // Otherwise if the id number is LESS than zero, use the second table, otherwise the first table
405
                    $theTable = trim($parts[1] ?? '')
406
                        ? strrev(trim($parts[1] ?? ''))
407
                        : ($secondTable && $theID < 0 ? $secondTable : $this->firstTable);
408
                    // If the ID is not blank and the table name is among the names in the inputted tableList
409
                    if ((string)$theID != '' && $theID && $theTable && isset($this->tableArray[$theTable])) {
410
                        // Get ID as the right value:
411
                        $theID = $secondTable ? abs((int)$theID) : (int)$theID;
412
                        // Register ID/table name in internal arrays:
413
                        $this->itemArray[$key]['id'] = $theID;
414
                        $this->itemArray[$key]['table'] = $theTable;
415
                        $this->tableArray[$theTable][] = $theID;
416
                        // Set update-flag
417
                        $isSet = true;
418
                    }
419
                }
420
                // If it turns out that the value from the list was NOT a valid reference to a table-record,
421
                // then we might still set it as a NO_TABLE value:
422
                if (!$isSet && $this->registerNonTableValues) {
423
                    $this->itemArray[$key]['id'] = $tempItemArray[$key];
424
                    $this->itemArray[$key]['table'] = '_NO_TABLE';
425
                    $this->nonTableArray[] = $tempItemArray[$key];
426
                }
427
            }
428
429
            // Skip if not dealing with IRRE in a CSV list on a workspace
430
            if (!isset($configuration['type']) || $configuration['type'] !== 'inline'
431
                || empty($configuration['foreign_table']) || !empty($configuration['foreign_field'])
432
                || !empty($configuration['MM']) || count($this->tableArray) !== 1 || empty($this->tableArray[$configuration['foreign_table']])
433
                || $this->getWorkspaceId() === 0 || !BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])
434
            ) {
435
                return;
436
            }
437
438
            // Fetch live record data
439
            if ($this->useLiveReferenceIds) {
440
                foreach ($this->itemArray as &$item) {
441
                    $item['id'] = $this->getLiveDefaultId($item['table'], $item['id']);
442
                }
443
            } else {
444
                // Directly overlay workspace data
445
                $this->itemArray = [];
446
                $foreignTable = $configuration['foreign_table'];
447
                $ids = $this->getResolver($foreignTable, $this->tableArray[$foreignTable])->get();
448
                foreach ($ids as $id) {
449
                    $this->itemArray[] = [
450
                        'id' => $id,
451
                        'table' => $foreignTable,
452
                    ];
453
                }
454
            }
455
        }
456
    }
457
458
    /**
459
     * Does a sorting on $this->itemArray depending on a default sortby field.
460
     * This is only used for automatic sorting of comma separated lists.
461
     * This function is only relevant for data that is stored in comma separated lists!
462
     *
463
     * @param string $sortby The default_sortby field/command (e.g. 'price DESC')
464
     */
465
    protected function sortList($sortby)
466
    {
467
        // Sort directly without fetching additional data
468
        if ($sortby === 'uid') {
469
            usort(
470
                $this->itemArray,
471
                function ($a, $b) {
472
                    return $a['id'] < $b['id'] ? -1 : 1;
473
                }
474
            );
475
        } elseif (count($this->tableArray) === 1) {
476
            reset($this->tableArray);
477
            $table = (string)key($this->tableArray);
478
            $connection = $this->getConnectionForTableName($table);
479
            $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
480
481
            foreach (array_chunk(current($this->tableArray), $maxBindParameters - 10, true) as $chunk) {
482
                if (empty($chunk)) {
483
                    continue;
484
                }
485
                $this->itemArray = [];
486
                $this->tableArray = [];
487
                $queryBuilder = $connection->createQueryBuilder();
488
                $queryBuilder->getRestrictions()->removeAll();
489
                $queryBuilder->select('uid')
490
                    ->from($table)
491
                    ->where(
492
                        $queryBuilder->expr()->in(
493
                            'uid',
494
                            $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
495
                        )
496
                    );
497
                foreach (QueryHelper::parseOrderBy((string)$sortby) as $orderPair) {
498
                    [$fieldName, $order] = $orderPair;
499
                    $queryBuilder->addOrderBy($fieldName, $order);
500
                }
501
                $statement = $queryBuilder->execute();
502
                while ($row = $statement->fetch()) {
503
                    $this->itemArray[] = ['id' => $row['uid'], 'table' => $table];
504
                    $this->tableArray[$table][] = $row['uid'];
505
                }
506
            }
507
        }
508
    }
509
510
    /**
511
     * Reads the record tablename/id into the internal arrays itemArray and tableArray from MM records.
512
     *
513
     * @param string $tableName MM Tablename
514
     * @param int|string $uid Local UID
515
     * @param string $mmOppositeTable Opposite table name
516
     */
517
    protected function readMM($tableName, $uid, $mmOppositeTable)
518
    {
519
        $key = 0;
520
        $theTable = null;
521
        $queryBuilder = $this->getConnectionForTableName($tableName)
522
            ->createQueryBuilder();
523
        $queryBuilder->getRestrictions()->removeAll();
524
        $queryBuilder->select('*')->from($tableName);
525
        // In case of a reverse relation
526
        if ($this->MM_is_foreign) {
527
            $uidLocal_field = 'uid_foreign';
528
            $uidForeign_field = 'uid_local';
529
            $sorting_field = 'sorting_foreign';
530
            if ($this->MM_isMultiTableRelationship) {
531
                // Be backwards compatible! When allowing more than one table after
532
                // having previously allowed only one table, this case applies.
533
                if ($this->currentTable == $this->MM_isMultiTableRelationship) {
534
                    $expression = $queryBuilder->expr()->orX(
535
                        $queryBuilder->expr()->eq(
536
                            'tablenames',
537
                            $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
538
                        ),
539
                        $queryBuilder->expr()->eq(
540
                            'tablenames',
541
                            $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
542
                        )
543
                    );
544
                } else {
545
                    $expression = $queryBuilder->expr()->eq(
546
                        'tablenames',
547
                        $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
548
                    );
549
                }
550
                $queryBuilder->andWhere($expression);
551
            }
552
            $theTable = $mmOppositeTable;
553
        } else {
554
            // Default
555
            $uidLocal_field = 'uid_local';
556
            $uidForeign_field = 'uid_foreign';
557
            $sorting_field = 'sorting';
558
        }
559
        if ($this->MM_table_where) {
560
            $queryBuilder->andWhere(
561
                QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (string)$uid, $this->MM_table_where))
562
            );
563
        }
564
        foreach ($this->MM_match_fields as $field => $value) {
565
            $queryBuilder->andWhere(
566
                $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
567
            );
568
        }
569
        $queryBuilder->andWhere(
570
            $queryBuilder->expr()->eq(
571
                $uidLocal_field,
572
                $queryBuilder->createNamedParameter((int)$uid, \PDO::PARAM_INT)
573
            )
574
        );
575
        $queryBuilder->orderBy($sorting_field);
576
        $queryBuilder->addOrderBy($uidForeign_field);
577
        $statement = $queryBuilder->execute();
578
        while ($row = $statement->fetch()) {
579
            // Default
580
            if (!$this->MM_is_foreign) {
581
                // If tablesnames columns exists and contain a name, then this value is the table, else it's the firstTable...
582
                $theTable = $row['tablenames'] ?: $this->firstTable;
583
            }
584
            if (($row[$uidForeign_field] || $theTable === 'pages') && $theTable && isset($this->tableArray[$theTable])) {
585
                $this->itemArray[$key]['id'] = $row[$uidForeign_field];
586
                $this->itemArray[$key]['table'] = $theTable;
587
                $this->tableArray[$theTable][] = $row[$uidForeign_field];
588
            } elseif ($this->registerNonTableValues) {
589
                $this->itemArray[$key]['id'] = $row[$uidForeign_field];
590
                $this->itemArray[$key]['table'] = '_NO_TABLE';
591
                $this->nonTableArray[] = $row[$uidForeign_field];
592
            }
593
            $key++;
594
        }
595
    }
596
597
    /**
598
     * Writes the internal itemArray to MM table:
599
     *
600
     * @param string $MM_tableName MM table name
601
     * @param int $uid Local UID
602
     * @param bool $prependTableName If set, then table names will always be written.
603
     */
604
    public function writeMM($MM_tableName, $uid, $prependTableName = false)
605
    {
606
        $connection = $this->getConnectionForTableName($MM_tableName);
607
        $expressionBuilder = $connection->createQueryBuilder()->expr();
608
609
        // In case of a reverse relation
610
        if ($this->MM_is_foreign) {
611
            $uidLocal_field = 'uid_foreign';
612
            $uidForeign_field = 'uid_local';
613
            $sorting_field = 'sorting_foreign';
614
        } else {
615
            // default
616
            $uidLocal_field = 'uid_local';
617
            $uidForeign_field = 'uid_foreign';
618
            $sorting_field = 'sorting';
619
        }
620
        // If there are tables...
621
        $tableC = count($this->tableArray);
622
        if ($tableC) {
623
            // Boolean: does the field "tablename" need to be filled?
624
            $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship;
625
            $c = 0;
626
            $additionalWhere_tablenames = '';
627
            if ($this->MM_is_foreign && $prep) {
628
                $additionalWhere_tablenames = $expressionBuilder->eq(
629
                    'tablenames',
630
                    $expressionBuilder->literal($this->currentTable)
631
                );
632
            }
633
            $additionalWhere = $expressionBuilder->andX();
634
            // Add WHERE clause if configured
635
            if ($this->MM_table_where) {
636
                $additionalWhere->add(
637
                    QueryHelper::stripLogicalOperatorPrefix(
638
                        str_replace('###THIS_UID###', (string)$uid, $this->MM_table_where)
639
                    )
640
                );
641
            }
642
            // Select, update or delete only those relations that match the configured fields
643
            foreach ($this->MM_match_fields as $field => $value) {
644
                $additionalWhere->add($expressionBuilder->eq($field, $expressionBuilder->literal($value)));
645
            }
646
647
            $queryBuilder = $connection->createQueryBuilder();
648
            $queryBuilder->getRestrictions()->removeAll();
649
            $queryBuilder->select($uidForeign_field)
650
                ->from($MM_tableName)
651
                ->where($queryBuilder->expr()->eq(
652
                    $uidLocal_field,
653
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
654
                ))
655
                ->orderBy($sorting_field);
656
657
            if ($prep) {
658
                $queryBuilder->addSelect('tablenames');
659
            }
660
            if ($this->MM_hasUidField) {
661
                $queryBuilder->addSelect('uid');
662
            }
663
            if ($additionalWhere_tablenames) {
664
                $queryBuilder->andWhere($additionalWhere_tablenames);
665
            }
666
            if ($additionalWhere->count()) {
667
                $queryBuilder->andWhere($additionalWhere);
668
            }
669
670
            $result = $queryBuilder->execute();
671
            $oldMMs = [];
672
            // This array is similar to $oldMMs but also holds the uid of the MM-records, if any (configured by MM_hasUidField).
673
            // If the UID is present it will be used to update sorting and delete MM-records.
674
            // This is necessary if the "multiple" feature is used for the MM relations.
675
            // $oldMMs is still needed for the in_array() search used to look if an item from $this->itemArray is in $oldMMs
676
            $oldMMs_inclUid = [];
677
            while ($row = $result->fetch()) {
678
                if (!$this->MM_is_foreign && $prep) {
679
                    $oldMMs[] = [$row['tablenames'], $row[$uidForeign_field]];
680
                } else {
681
                    $oldMMs[] = $row[$uidForeign_field];
682
                }
683
                $oldMMs_inclUid[] = [$row['tablenames'], $row[$uidForeign_field], $row['uid'] ?? 0];
684
            }
685
            // For each item, insert it:
686
            foreach ($this->itemArray as $val) {
687
                $c++;
688
                if ($prep || $val['table'] === '_NO_TABLE') {
689
                    // Insert current table if needed
690
                    if ($this->MM_is_foreign) {
691
                        $tablename = $this->currentTable;
692
                    } else {
693
                        $tablename = $val['table'];
694
                    }
695
                } else {
696
                    $tablename = '';
697
                }
698
                if (!$this->MM_is_foreign && $prep) {
699
                    $item = [$val['table'], $val['id']];
700
                } else {
701
                    $item = $val['id'];
702
                }
703
                if (in_array($item, $oldMMs)) {
704
                    $oldMMs_index = array_search($item, $oldMMs);
705
                    // In principle, selecting on the UID is all we need to do
706
                    // if a uid field is available since that is unique!
707
                    // But as long as it "doesn't hurt" we just add it to the where clause. It should all match up.
708
                    $queryBuilder = $connection->createQueryBuilder();
709
                    $queryBuilder->update($MM_tableName)
710
                        ->set($sorting_field, $c)
711
                        ->where(
712
                            $expressionBuilder->eq(
713
                                $uidLocal_field,
714
                                $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
715
                            ),
716
                            $expressionBuilder->eq(
717
                                $uidForeign_field,
718
                                $queryBuilder->createNamedParameter($val['id'], \PDO::PARAM_INT)
719
                            )
720
                        );
721
722
                    if ($additionalWhere->count()) {
723
                        $queryBuilder->andWhere($additionalWhere);
724
                    }
725
                    if ($this->MM_hasUidField) {
726
                        $queryBuilder->andWhere(
727
                            $expressionBuilder->eq(
728
                                'uid',
729
                                $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMMs_index][2], \PDO::PARAM_INT)
730
                            )
731
                        );
732
                    }
733
                    if ($tablename) {
734
                        $queryBuilder->andWhere(
735
                            $expressionBuilder->eq(
736
                                'tablenames',
737
                                $queryBuilder->createNamedParameter($tablename, \PDO::PARAM_STR)
738
                            )
739
                        );
740
                    }
741
742
                    $queryBuilder->execute();
743
                    // Remove the item from the $oldMMs array so after this
744
                    // foreach loop only the ones that need to be deleted are in there.
745
                    unset($oldMMs[$oldMMs_index]);
746
                    // Remove the item from the $oldMMs_inclUid array so after this
747
                    // foreach loop only the ones that need to be deleted are in there.
748
                    unset($oldMMs_inclUid[$oldMMs_index]);
749
                } else {
750
                    $insertFields = $this->MM_insert_fields;
751
                    $insertFields[$uidLocal_field] = $uid;
752
                    $insertFields[$uidForeign_field] = $val['id'];
753
                    $insertFields[$sorting_field] = $c;
754
                    if ($tablename) {
755
                        $insertFields['tablenames'] = $tablename;
756
                        $insertFields = $this->completeOppositeUsageValues($tablename, $insertFields);
757
                    }
758
                    $connection->insert($MM_tableName, $insertFields);
759
                    if ($this->MM_is_foreign) {
760
                        $this->updateRefIndex($val['table'], $val['id']);
761
                    }
762
                }
763
            }
764
            // Delete all not-used relations:
765
            if (is_array($oldMMs) && !empty($oldMMs)) {
766
                $queryBuilder = $connection->createQueryBuilder();
767
                $removeClauses = $queryBuilder->expr()->orX();
768
                $updateRefIndex_records = [];
769
                foreach ($oldMMs as $oldMM_key => $mmItem) {
770
                    // If UID field is present, of course we need only use that for deleting.
771
                    if ($this->MM_hasUidField) {
772
                        $removeClauses->add($queryBuilder->expr()->eq(
773
                            'uid',
774
                            $queryBuilder->createNamedParameter($oldMMs_inclUid[$oldMM_key][2], \PDO::PARAM_INT)
775
                        ));
776
                    } else {
777
                        if (is_array($mmItem)) {
778
                            $removeClauses->add(
779
                                $queryBuilder->expr()->andX(
780
                                    $queryBuilder->expr()->eq(
781
                                        'tablenames',
782
                                        $queryBuilder->createNamedParameter($mmItem[0], \PDO::PARAM_STR)
783
                                    ),
784
                                    $queryBuilder->expr()->eq(
785
                                        $uidForeign_field,
786
                                        $queryBuilder->createNamedParameter($mmItem[1], \PDO::PARAM_INT)
787
                                    )
788
                                )
789
                            );
790
                        } else {
791
                            $removeClauses->add(
792
                                $queryBuilder->expr()->eq(
793
                                    $uidForeign_field,
794
                                    $queryBuilder->createNamedParameter($mmItem, \PDO::PARAM_INT)
795
                                )
796
                            );
797
                        }
798
                    }
799
                    if ($this->MM_is_foreign) {
800
                        if (is_array($mmItem)) {
801
                            $updateRefIndex_records[] = [$mmItem[0], $mmItem[1]];
802
                        } else {
803
                            $updateRefIndex_records[] = [$this->firstTable, $mmItem];
804
                        }
805
                    }
806
                }
807
808
                $queryBuilder->delete($MM_tableName)
809
                    ->where(
810
                        $queryBuilder->expr()->eq(
811
                            $uidLocal_field,
812
                            $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
813
                        ),
814
                        $removeClauses
815
                    );
816
817
                if ($additionalWhere_tablenames) {
818
                    $queryBuilder->andWhere($additionalWhere_tablenames);
819
                }
820
                if ($additionalWhere->count()) {
821
                    $queryBuilder->andWhere($additionalWhere);
822
                }
823
824
                $queryBuilder->execute();
825
826
                // Update ref index:
827
                foreach ($updateRefIndex_records as $pair) {
828
                    $this->updateRefIndex($pair[0], $pair[1]);
829
                }
830
            }
831
            // Update ref index; In DataHandler it is not certain that this will happen because
832
            // if only the MM field is changed the record itself is not updated and so the ref-index is not either.
833
            // This could also have been fixed in updateDB in DataHandler, however I decided to do it here ...
834
            $this->updateRefIndex($this->currentTable, $uid);
835
        }
836
    }
837
838
    /**
839
     * Remaps MM table elements from one local uid to another
840
     * Does NOT update the reference index for you, must be called subsequently to do that!
841
     *
842
     * @param string $MM_tableName MM table name
843
     * @param int $uid Local, current UID
844
     * @param int $newUid Local, new UID
845
     * @param bool $prependTableName If set, then table names will always be written.
846
     */
847
    public function remapMM($MM_tableName, $uid, $newUid, $prependTableName = false)
848
    {
849
        // In case of a reverse relation
850
        if ($this->MM_is_foreign) {
851
            $uidLocal_field = 'uid_foreign';
852
        } else {
853
            // default
854
            $uidLocal_field = 'uid_local';
855
        }
856
        // If there are tables...
857
        $tableC = count($this->tableArray);
858
        if ($tableC) {
859
            $queryBuilder = $this->getConnectionForTableName($MM_tableName)
860
                ->createQueryBuilder();
861
            $queryBuilder->update($MM_tableName)
862
                ->set($uidLocal_field, (int)$newUid)
863
                ->where($queryBuilder->expr()->eq(
864
                    $uidLocal_field,
865
                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
866
                ));
867
            // Boolean: does the field "tablename" need to be filled?
868
            $prep = $tableC > 1 || $prependTableName || $this->MM_isMultiTableRelationship;
869
            if ($this->MM_is_foreign && $prep) {
870
                $queryBuilder->andWhere(
871
                    $queryBuilder->expr()->eq(
872
                        'tablenames',
873
                        $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
874
                    )
875
                );
876
            }
877
            // Add WHERE clause if configured
878
            if ($this->MM_table_where) {
879
                $queryBuilder->andWhere(
880
                    QueryHelper::stripLogicalOperatorPrefix(str_replace('###THIS_UID###', (string)$uid, $this->MM_table_where))
881
                );
882
            }
883
            // Select, update or delete only those relations that match the configured fields
884
            foreach ($this->MM_match_fields as $field => $value) {
885
                $queryBuilder->andWhere(
886
                    $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
887
                );
888
            }
889
            $queryBuilder->execute();
890
        }
891
    }
892
893
    /**
894
     * Reads items from a foreign_table, that has a foreign_field (uid of the parent record) and
895
     * stores the parts in the internal array itemArray and tableArray.
896
     *
897
     * @param int|string $uid The uid of the parent record (this value is also on the foreign_table in the foreign_field)
898
     * @param array $conf TCA configuration for current field
899
     */
900
    protected function readForeignField($uid, $conf)
901
    {
902
        if ($this->useLiveParentIds) {
903
            $uid = $this->getLiveDefaultId($this->currentTable, $uid);
904
        }
905
906
        $key = 0;
907
        $uid = (int)$uid;
908
        // skip further processing if $uid does not
909
        // point to a valid parent record
910
        if ($uid === 0) {
911
            return;
912
        }
913
914
        $foreign_table = $conf['foreign_table'];
915
        $foreign_table_field = $conf['foreign_table_field'] ?? '';
916
        $useDeleteClause = !$this->undeleteRecord;
917
        $foreign_match_fields = is_array($conf['foreign_match_fields'] ?? false) ? $conf['foreign_match_fields'] : [];
918
        $queryBuilder = $this->getConnectionForTableName($foreign_table)
919
            ->createQueryBuilder();
920
        $queryBuilder->getRestrictions()
921
            ->removeAll();
922
        // Use the deleteClause (e.g. "deleted=0") on this table
923
        if ($useDeleteClause) {
924
            $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
925
        }
926
927
        $queryBuilder->select('uid')
928
            ->from($foreign_table);
929
930
        // Search for $uid in foreign_field, and if we have symmetric relations, do this also on symmetric_field
931
        if (!empty($conf['symmetric_field'])) {
932
            $queryBuilder->where(
933
                $queryBuilder->expr()->orX(
934
                    $queryBuilder->expr()->eq(
935
                        $conf['foreign_field'],
936
                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
937
                    ),
938
                    $queryBuilder->expr()->eq(
939
                        $conf['symmetric_field'],
940
                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
941
                    )
942
                )
943
            );
944
        } else {
945
            $queryBuilder->where($queryBuilder->expr()->eq(
946
                $conf['foreign_field'],
947
                $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
948
            ));
949
        }
950
        // If it's requested to look for the parent uid AND the parent table,
951
        // add an additional SQL-WHERE clause
952
        if ($foreign_table_field && $this->currentTable) {
953
            $queryBuilder->andWhere(
954
                $queryBuilder->expr()->eq(
955
                    $foreign_table_field,
956
                    $queryBuilder->createNamedParameter($this->currentTable, \PDO::PARAM_STR)
957
                )
958
            );
959
        }
960
        // Add additional where clause if foreign_match_fields are defined
961
        foreach ($foreign_match_fields as $field => $value) {
962
            $queryBuilder->andWhere(
963
                $queryBuilder->expr()->eq($field, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
964
            );
965
        }
966
        // Select children from the live(!) workspace only
967
        if (BackendUtility::isTableWorkspaceEnabled($foreign_table)) {
968
            $queryBuilder->getRestrictions()->add(
969
                GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getWorkspaceId())
970
            );
971
        }
972
        // Get the correct sorting field
973
        // Specific manual sortby for data handled by this field
974
        $sortby = '';
975
        if (!empty($conf['foreign_sortby'])) {
976
            if (!empty($conf['symmetric_sortby']) && !empty($conf['symmetric_field'])) {
977
                // Sorting depends on, from which side of the relation we're looking at it
978
                // This requires bypassing automatic quoting and setting of the default sort direction
979
                // @TODO: Doctrine: generalize to standard SQL to guarantee database independency
980
                $queryBuilder->add(
981
                    'orderBy',
982
                    'CASE
983
						WHEN ' . $queryBuilder->expr()->eq($conf['foreign_field'], $uid) . '
984
						THEN ' . $queryBuilder->quoteIdentifier($conf['foreign_sortby']) . '
985
						ELSE ' . $queryBuilder->quoteIdentifier($conf['symmetric_sortby']) . '
986
					END'
987
                );
988
            } else {
989
                // Regular single-side behaviour
990
                $sortby = $conf['foreign_sortby'];
991
            }
992
        } elseif (!empty($conf['foreign_default_sortby'])) {
993
            // Specific default sortby for data handled by this field
994
            $sortby = $conf['foreign_default_sortby'];
995
        } elseif (!empty($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'])) {
996
            // Manual sortby for all table records
997
            $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
998
        } elseif (!empty($GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'])) {
999
            // Default sortby for all table records
1000
            $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['default_sortby'];
1001
        }
1002
1003
        if (!empty($sortby)) {
1004
            foreach (QueryHelper::parseOrderBy($sortby) as $orderPair) {
1005
                [$fieldName, $sorting] = $orderPair;
1006
                $queryBuilder->addOrderBy($fieldName, $sorting);
1007
            }
1008
        }
1009
1010
        // Get the rows from storage
1011
        $rows = [];
1012
        $result = $queryBuilder->execute();
1013
        while ($row = $result->fetch()) {
1014
            $rows[(int)$row['uid']] = $row;
1015
        }
1016
        if (!empty($rows)) {
1017
            // Retrieve the parsed and prepared ORDER BY configuration for the resolver
1018
            $sortby = $queryBuilder->getQueryPart('orderBy');
1019
            $ids = $this->getResolver($foreign_table, array_keys($rows), $sortby)->get();
1020
            foreach ($ids as $id) {
1021
                $this->itemArray[$key]['id'] = $id;
1022
                $this->itemArray[$key]['table'] = $foreign_table;
1023
                $this->tableArray[$foreign_table][] = $id;
1024
                $key++;
1025
            }
1026
        }
1027
    }
1028
1029
    /**
1030
     * Write the sorting values to a foreign_table, that has a foreign_field (uid of the parent record)
1031
     *
1032
     * @param array $conf TCA configuration for current field
1033
     * @param int $parentUid The uid of the parent record
1034
     * @param int $updateToUid If this is larger than zero it will be used as foreign UID instead of the given $parentUid (on Copy)
1035
     * @param bool $skipSorting Do not update the sorting columns, this could happen for imported values
1036
     */
1037
    public function writeForeignField($conf, $parentUid, $updateToUid = 0, $skipSorting = false)
1038
    {
1039
        if ($this->useLiveParentIds) {
1040
            $parentUid = $this->getLiveDefaultId($this->currentTable, $parentUid);
1041
            if (!empty($updateToUid)) {
1042
                $updateToUid = $this->getLiveDefaultId($this->currentTable, $updateToUid);
1043
            }
1044
        }
1045
1046
        // Ensure all values are set.
1047
        $conf += [
1048
            'foreign_table' => '',
1049
            'foreign_field' => '',
1050
            'symmetric_field' => '',
1051
            'foreign_table_field' => '',
1052
            'foreign_match_fields' => [],
1053
        ];
1054
1055
        $c = 0;
1056
        $foreign_table = $conf['foreign_table'];
1057
        $foreign_field = $conf['foreign_field'];
1058
        $symmetric_field = $conf['symmetric_field'] ?? '';
1059
        $foreign_table_field = $conf['foreign_table_field'];
1060
        $foreign_match_fields = $conf['foreign_match_fields'];
1061
        // If there are table items and we have a proper $parentUid
1062
        if (MathUtility::canBeInterpretedAsInteger($parentUid) && !empty($this->tableArray)) {
1063
            // If updateToUid is not a positive integer, set it to '0', so it will be ignored
1064
            if (!(MathUtility::canBeInterpretedAsInteger($updateToUid) && $updateToUid > 0)) {
1065
                $updateToUid = 0;
1066
            }
1067
            $considerWorkspaces = BackendUtility::isTableWorkspaceEnabled($foreign_table);
1068
            $fields = 'uid,pid,' . $foreign_field;
1069
            // Consider the symmetric field if defined:
1070
            if ($symmetric_field) {
1071
                $fields .= ',' . $symmetric_field;
1072
            }
1073
            // Consider workspaces if defined and currently used:
1074
            if ($considerWorkspaces) {
1075
                $fields .= ',t3ver_wsid,t3ver_state,t3ver_oid';
1076
            }
1077
            // Update all items
1078
            foreach ($this->itemArray as $val) {
1079
                $uid = $val['id'];
1080
                $table = $val['table'];
1081
                $row = [];
1082
                // Fetch the current (not overwritten) relation record if we should handle symmetric relations
1083
                if ($symmetric_field || $considerWorkspaces) {
1084
                    $row = BackendUtility::getRecord($table, $uid, $fields, '', true);
1085
                    if (empty($row)) {
1086
                        continue;
1087
                    }
1088
                }
1089
                $isOnSymmetricSide = false;
1090
                if ($symmetric_field) {
1091
                    $isOnSymmetricSide = self::isOnSymmetricSide((string)$parentUid, $conf, $row);
1092
                }
1093
                $updateValues = $foreign_match_fields;
1094
                // No update to the uid is requested, so this is the normal behaviour
1095
                // just update the fields and care about sorting
1096
                if (!$updateToUid) {
1097
                    // Always add the pointer to the parent uid
1098
                    if ($isOnSymmetricSide) {
1099
                        $updateValues[$symmetric_field] = $parentUid;
1100
                    } else {
1101
                        $updateValues[$foreign_field] = $parentUid;
1102
                    }
1103
                    // If it is configured in TCA also to store the parent table in the child record, just do it
1104
                    if ($foreign_table_field && $this->currentTable) {
1105
                        $updateValues[$foreign_table_field] = $this->currentTable;
1106
                    }
1107
                    // Update sorting columns if not to be skipped
1108
                    if (!$skipSorting) {
1109
                        // Get the correct sorting field
1110
                        // Specific manual sortby for data handled by this field
1111
                        $sortby = '';
1112
                        if ($conf['foreign_sortby'] ?? false) {
1113
                            $sortby = $conf['foreign_sortby'];
1114
                        } elseif ($GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'] ?? false) {
1115
                            // manual sortby for all table records
1116
                            $sortby = $GLOBALS['TCA'][$foreign_table]['ctrl']['sortby'];
1117
                        }
1118
                        // Apply sorting on the symmetric side
1119
                        // (it depends on who created the relation, so what uid is in the symmetric_field):
1120
                        if ($isOnSymmetricSide && isset($conf['symmetric_sortby']) && $conf['symmetric_sortby']) {
1121
                            $sortby = $conf['symmetric_sortby'];
1122
                        } else {
1123
                            $tempSortBy = [];
1124
                            foreach (QueryHelper::parseOrderBy($sortby) as $orderPair) {
1125
                                [$fieldName, $order] = $orderPair;
1126
                                if ($order !== null) {
1127
                                    $tempSortBy[] = implode(' ', $orderPair);
1128
                                } else {
1129
                                    $tempSortBy[] = $fieldName;
1130
                                }
1131
                            }
1132
                            $sortby = implode(',', $tempSortBy);
1133
                        }
1134
                        if ($sortby) {
1135
                            $updateValues[$sortby] = ++$c;
1136
                        }
1137
                    }
1138
                } else {
1139
                    if ($isOnSymmetricSide) {
1140
                        $updateValues[$symmetric_field] = $updateToUid;
1141
                    } else {
1142
                        $updateValues[$foreign_field] = $updateToUid;
1143
                    }
1144
                }
1145
                // Update accordant fields in the database:
1146
                if (!empty($updateValues)) {
1147
                    // Update tstamp if any foreign field value has changed
1148
                    if (!empty($GLOBALS['TCA'][$table]['ctrl']['tstamp'])) {
1149
                        $updateValues[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1150
                    }
1151
                    $this->getConnectionForTableName($table)
1152
                        ->update(
1153
                            $table,
1154
                            $updateValues,
1155
                            ['uid' => (int)$uid]
1156
                        );
1157
                    $this->updateRefIndex($table, $uid);
1158
                }
1159
            }
1160
        }
1161
    }
1162
1163
    /**
1164
     * After initialization you can extract an array of the elements from the object. Use this function for that.
1165
     *
1166
     * @param bool $prependTableName If set, then table names will ALWAYS be prepended (unless its a _NO_TABLE value)
1167
     * @return array A numeric array.
1168
     */
1169
    public function getValueArray($prependTableName = false)
1170
    {
1171
        // INIT:
1172
        $valueArray = [];
1173
        $tableC = count($this->tableArray);
1174
        // If there are tables in the table array:
1175
        if ($tableC) {
1176
            // If there are more than ONE table in the table array, then always prepend table names:
1177
            $prep = $tableC > 1 || $prependTableName;
1178
            // Traverse the array of items:
1179
            foreach ($this->itemArray as $val) {
1180
                $valueArray[] = ($prep && $val['table'] !== '_NO_TABLE' ? $val['table'] . '_' : '') . $val['id'];
1181
            }
1182
        }
1183
        // Return the array
1184
        return $valueArray;
1185
    }
1186
1187
    /**
1188
     * Reads all records from internal tableArray into the internal ->results array
1189
     * where keys are table names and for each table, records are stored with uids as their keys.
1190
     * If $this->fetchAllFields is false you can save a little memory
1191
     * since only uid,pid and a few other fields are selected.
1192
     *
1193
     * @return array
1194
     */
1195
    public function getFromDB()
1196
    {
1197
        // Traverses the tables listed:
1198
        foreach ($this->tableArray as $table => $ids) {
1199
            if (is_array($ids) && !empty($ids)) {
1200
                $connection = $this->getConnectionForTableName($table);
1201
                $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1202
1203
                foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1204
                    if ($this->fetchAllFields) {
1205
                        $fields = '*';
1206
                    } else {
1207
                        $fields = 'uid,pid';
1208
                        if ($GLOBALS['TCA'][$table]['ctrl']['label']) {
1209
                            // Title
1210
                            $fields .= ',' . $GLOBALS['TCA'][$table]['ctrl']['label'];
1211
                        }
1212
                        if ($GLOBALS['TCA'][$table]['ctrl']['label_alt'] ?? false) {
1213
                            // Alternative Title-Fields
1214
                            $fields .= ',' . $GLOBALS['TCA'][$table]['ctrl']['label_alt'];
1215
                        }
1216
                    }
1217
                    $queryBuilder = $connection->createQueryBuilder();
1218
                    $queryBuilder->getRestrictions()->removeAll();
1219
                    $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields, true))
1220
                        ->from($table)
1221
                        ->where($queryBuilder->expr()->in(
1222
                            'uid',
1223
                            $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1224
                        ));
1225
                    if ($this->additionalWhere[$table] ?? false) {
1226
                        $queryBuilder->andWhere(
1227
                            QueryHelper::stripLogicalOperatorPrefix($this->additionalWhere[$table])
1228
                        );
1229
                    }
1230
                    $statement = $queryBuilder->execute();
1231
                    while ($row = $statement->fetch()) {
1232
                        $this->results[$table][$row['uid']] = $row;
1233
                    }
1234
                }
1235
            }
1236
        }
1237
        return $this->results;
1238
    }
1239
1240
    /**
1241
     * This method is typically called after getFromDB().
1242
     * $this->results holds a list of resolved and valid relations,
1243
     * $this->itemArray hold a list of "selected" relations from the incoming selection array.
1244
     * The difference is that "itemArray" may hold a single table/uid combination multiple times,
1245
     * for instance in a type=group relation having multiple=true, while "results" hold each
1246
     * resolved relation only once.
1247
     * The methods creates a sanitized "itemArray" from resolved "results" list, normalized
1248
     * the return array to always contain both table name and uid, and keep incoming
1249
     * "itemArray" sort order and keeps "multiple" selections.
1250
     *
1251
     * @return array
1252
     */
1253
    public function getResolvedItemArray(): array
1254
    {
1255
        $itemArray = [];
1256
        foreach ($this->itemArray as $item) {
1257
            if (isset($this->results[$item['table']][$item['id']])) {
1258
                $itemArray[] = [
1259
                    'table' => $item['table'],
1260
                    'uid' => $item['id'],
1261
                ];
1262
            }
1263
        }
1264
        return $itemArray;
1265
    }
1266
1267
    /**
1268
     * Counts the items in $this->itemArray and puts this value in an array by default.
1269
     *
1270
     * @param bool $returnAsArray Whether to put the count value in an array
1271
     * @return mixed The plain count as integer or the same inside an array
1272
     */
1273
    public function countItems($returnAsArray = true)
1274
    {
1275
        $count = count($this->itemArray);
1276
        if ($returnAsArray) {
1277
            $count = [$count];
1278
        }
1279
        return $count;
1280
    }
1281
1282
    /**
1283
     * Update Reference Index (sys_refindex) for a record.
1284
     * Should be called any almost any update to a record which could affect references inside the record.
1285
     * If used from within DataHandler, only registers a row for update for later processing.
1286
     *
1287
     * @param string $table Table name
1288
     * @param int $uid Record uid
1289
     * @return array Result from ReferenceIndex->updateRefIndexTable() updated directly, else empty array
1290
     */
1291
    protected function updateRefIndex($table, $uid): array
1292
    {
1293
        if (!$this->updateReferenceIndex) {
1294
            return [];
1295
        }
1296
        if ($this->referenceIndexUpdater) {
1297
            // Add to update registry if given
1298
            $this->referenceIndexUpdater->registerForUpdate((string)$table, (int)$uid, $this->getWorkspaceId());
1299
            $statisticsArray = [];
1300
        } else {
1301
            // @deprecated else branch can be dropped when setUpdateReferenceIndex() is dropped.
1302
            // Update reference index directly if enabled
1303
            $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
1304
            if (BackendUtility::isTableWorkspaceEnabled($table)) {
1305
                $referenceIndex->setWorkspaceId($this->getWorkspaceId());
1306
            }
1307
            $statisticsArray = $referenceIndex->updateRefIndexTable($table, $uid);
1308
        }
1309
        return $statisticsArray;
1310
    }
1311
1312
    /**
1313
     * Converts elements in the local item array to use version ids instead of
1314
     * live ids, if possible. The most common use case is, to call that prior
1315
     * to processing with MM relations in a workspace context. For tha special
1316
     * case, ids on both side of the MM relation must use version ids if
1317
     * available.
1318
     *
1319
     * @return bool Whether items have been converted
1320
     */
1321
    public function convertItemArray()
1322
    {
1323
        // conversion is only required in a workspace context
1324
        // (the case that version ids are submitted in a live context are rare)
1325
        if ($this->getWorkspaceId() === 0) {
1326
            return false;
1327
        }
1328
1329
        $hasBeenConverted = false;
1330
        foreach ($this->tableArray as $tableName => $ids) {
1331
            if (empty($ids) || !BackendUtility::isTableWorkspaceEnabled($tableName)) {
1332
                continue;
1333
            }
1334
1335
            // convert live ids to version ids if available
1336
            $convertedIds = $this->getResolver($tableName, $ids)
1337
                ->setKeepDeletePlaceholder(false)
1338
                ->setKeepMovePlaceholder(false)
1339
                ->processVersionOverlays($ids);
1340
            foreach ($this->itemArray as $index => $item) {
1341
                if ($item['table'] !== $tableName) {
1342
                    continue;
1343
                }
1344
                $currentItemId = $item['id'];
1345
                if (
1346
                    !isset($convertedIds[$currentItemId])
1347
                    || $currentItemId === $convertedIds[$currentItemId]
1348
                ) {
1349
                    continue;
1350
                }
1351
                // adjust local item to use resolved version id
1352
                $this->itemArray[$index]['id'] = $convertedIds[$currentItemId];
1353
                $hasBeenConverted = true;
1354
            }
1355
            // update per-table reference for ids
1356
            if ($hasBeenConverted) {
1357
                $this->tableArray[$tableName] = array_values($convertedIds);
1358
            }
1359
        }
1360
1361
        return $hasBeenConverted;
1362
    }
1363
1364
    /**
1365
     * @param int|null $workspaceId
1366
     * @return bool Whether items have been purged
1367
     */
1368
    public function purgeItemArray($workspaceId = null)
1369
    {
1370
        if ($workspaceId === null) {
1371
            $workspaceId = $this->getWorkspaceId();
1372
        } else {
1373
            $workspaceId = (int)$workspaceId;
1374
        }
1375
1376
        // Ensure, only live relations are in the items Array
1377
        if ($workspaceId === 0) {
1378
            $purgeCallback = 'purgeVersionedIds';
1379
        } else {
1380
            // Otherwise, ensure that live relations are purged if version exists
1381
            $purgeCallback = 'purgeLiveVersionedIds';
1382
        }
1383
1384
        $itemArrayHasBeenPurged = $this->purgeItemArrayHandler($purgeCallback);
1385
        $this->purged = ($this->purged || $itemArrayHasBeenPurged);
1386
        return $itemArrayHasBeenPurged;
1387
    }
1388
1389
    /**
1390
     * Removes items having a delete placeholder from $this->itemArray
1391
     *
1392
     * @return bool Whether items have been purged
1393
     */
1394
    public function processDeletePlaceholder()
1395
    {
1396
        if (!$this->useLiveReferenceIds || $this->getWorkspaceId() === 0) {
1397
            return false;
1398
        }
1399
1400
        return $this->purgeItemArrayHandler('purgeDeletePlaceholder');
1401
    }
1402
1403
    /**
1404
     * Handles a purge callback on $this->itemArray
1405
     *
1406
     * @param string $purgeCallback
1407
     * @return bool Whether items have been purged
1408
     */
1409
    protected function purgeItemArrayHandler($purgeCallback)
1410
    {
1411
        $itemArrayHasBeenPurged = false;
1412
1413
        foreach ($this->tableArray as $itemTableName => $itemIds) {
1414
            if (empty($itemIds) || !BackendUtility::isTableWorkspaceEnabled($itemTableName)) {
1415
                continue;
1416
            }
1417
1418
            $purgedItemIds = [];
1419
            $callable =[$this, $purgeCallback];
1420
            if (is_callable($callable)) {
1421
                $purgedItemIds = $callable($itemTableName, $itemIds);
1422
            }
1423
1424
            $removedItemIds = array_diff($itemIds, $purgedItemIds);
1425
            foreach ($removedItemIds as $removedItemId) {
1426
                $this->removeFromItemArray($itemTableName, $removedItemId);
1427
            }
1428
            $this->tableArray[$itemTableName] = $purgedItemIds;
1429
            if (!empty($removedItemIds)) {
1430
                $itemArrayHasBeenPurged = true;
1431
            }
1432
        }
1433
1434
        return $itemArrayHasBeenPurged;
1435
    }
1436
1437
    /**
1438
     * Purges ids that are versioned.
1439
     *
1440
     * @param string $tableName
1441
     * @param array $ids
1442
     * @return array
1443
     */
1444
    protected function purgeVersionedIds($tableName, array $ids)
1445
    {
1446
        $ids = $this->sanitizeIds($ids);
1447
        $ids = (array)array_combine($ids, $ids);
1448
        $connection = $this->getConnectionForTableName($tableName);
1449
        $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1450
1451
        foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1452
            $queryBuilder = $connection->createQueryBuilder();
1453
            $queryBuilder->getRestrictions()->removeAll();
1454
            $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1455
                ->from($tableName)
1456
                ->where(
1457
                    $queryBuilder->expr()->in(
1458
                        't3ver_oid',
1459
                        $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1460
                    ),
1461
                    $queryBuilder->expr()->neq(
1462
                        't3ver_wsid',
1463
                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1464
                    )
1465
                )
1466
                ->orderBy('t3ver_state', 'DESC')
1467
                ->execute();
1468
1469
            while ($version = $result->fetch()) {
1470
                $versionId = $version['uid'];
1471
                if (isset($ids[$versionId])) {
1472
                    unset($ids[$versionId]);
1473
                }
1474
            }
1475
        }
1476
1477
        return array_values($ids);
1478
    }
1479
1480
    /**
1481
     * Purges ids that are live but have an accordant version.
1482
     *
1483
     * @param string $tableName
1484
     * @param array $ids
1485
     * @return array
1486
     */
1487
    protected function purgeLiveVersionedIds($tableName, array $ids)
1488
    {
1489
        $ids = $this->sanitizeIds($ids);
1490
        $ids = (array)array_combine($ids, $ids);
1491
        $connection = $this->getConnectionForTableName($tableName);
1492
        $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1493
1494
        foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1495
            $queryBuilder = $connection->createQueryBuilder();
1496
            $queryBuilder->getRestrictions()->removeAll();
1497
            $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1498
                ->from($tableName)
1499
                ->where(
1500
                    $queryBuilder->expr()->in(
1501
                        't3ver_oid',
1502
                        $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1503
                    ),
1504
                    $queryBuilder->expr()->neq(
1505
                        't3ver_wsid',
1506
                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1507
                    )
1508
                )
1509
                ->orderBy('t3ver_state', 'DESC')
1510
                ->execute();
1511
1512
            while ($version = $result->fetch()) {
1513
                $versionId = $version['uid'];
1514
                $liveId = $version['t3ver_oid'];
1515
                if (isset($ids[$liveId]) && isset($ids[$versionId])) {
1516
                    unset($ids[$liveId]);
1517
                }
1518
            }
1519
        }
1520
1521
        return array_values($ids);
1522
    }
1523
1524
    /**
1525
     * Purges ids that have a delete placeholder
1526
     *
1527
     * @param string $tableName
1528
     * @param array $ids
1529
     * @return array
1530
     */
1531
    protected function purgeDeletePlaceholder($tableName, array $ids)
1532
    {
1533
        $ids = $this->sanitizeIds($ids);
1534
        $ids = array_combine($ids, $ids) ?: [];
1535
        $connection = $this->getConnectionForTableName($tableName);
1536
        $maxBindParameters = PlatformInformation::getMaxBindParameters($connection->getDatabasePlatform());
1537
1538
        foreach (array_chunk($ids, $maxBindParameters - 10, true) as $chunk) {
1539
            $queryBuilder = $connection->createQueryBuilder();
1540
            $queryBuilder->getRestrictions()->removeAll();
1541
            $result = $queryBuilder->select('uid', 't3ver_oid', 't3ver_state')
1542
                ->from($tableName)
1543
                ->where(
1544
                    $queryBuilder->expr()->in(
1545
                        't3ver_oid',
1546
                        $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
1547
                    ),
1548
                    $queryBuilder->expr()->eq(
1549
                        't3ver_wsid',
1550
                        $queryBuilder->createNamedParameter(
1551
                            $this->getWorkspaceId(),
1552
                            \PDO::PARAM_INT
1553
                        )
1554
                    ),
1555
                    $queryBuilder->expr()->eq(
1556
                        't3ver_state',
1557
                        $queryBuilder->createNamedParameter(
1558
                            (string)VersionState::cast(VersionState::DELETE_PLACEHOLDER),
1559
                            \PDO::PARAM_INT
1560
                        )
1561
                    )
1562
                )
1563
                ->execute();
1564
1565
            while ($version = $result->fetch()) {
1566
                $liveId = $version['t3ver_oid'];
1567
                if (isset($ids[$liveId])) {
1568
                    unset($ids[$liveId]);
1569
                }
1570
            }
1571
        }
1572
1573
        return array_values($ids);
1574
    }
1575
1576
    protected function removeFromItemArray($tableName, $id)
1577
    {
1578
        foreach ($this->itemArray as $index => $item) {
1579
            if ($item['table'] === $tableName && (string)$item['id'] === (string)$id) {
1580
                unset($this->itemArray[$index]);
1581
                return true;
1582
            }
1583
        }
1584
        return false;
1585
    }
1586
1587
    /**
1588
     * Checks, if we're looking from the "other" side, the symmetric side, to a symmetric relation.
1589
     *
1590
     * @param string $parentUid The uid of the parent record
1591
     * @param array $parentConf The TCA configuration of the parent field embedding the child records
1592
     * @param array $childRec The record row of the child record
1593
     * @return bool Returns TRUE if looking from the symmetric ("other") side to the relation.
1594
     */
1595
    protected static function isOnSymmetricSide($parentUid, $parentConf, $childRec)
1596
    {
1597
        return MathUtility::canBeInterpretedAsInteger($childRec['uid'])
1598
            && $parentConf['symmetric_field']
1599
            && $parentUid == $childRec[$parentConf['symmetric_field']];
1600
    }
1601
1602
    /**
1603
     * Completes MM values to be written by values from the opposite relation.
1604
     * This method used MM insert field or MM match fields if defined.
1605
     *
1606
     * @param string $tableName Name of the opposite table
1607
     * @param array $referenceValues Values to be written
1608
     * @return array Values to be written, possibly modified
1609
     */
1610
    protected function completeOppositeUsageValues($tableName, array $referenceValues)
1611
    {
1612
        if (empty($this->MM_oppositeUsage[$tableName]) || count($this->MM_oppositeUsage[$tableName]) > 1) {
1613
            return $referenceValues;
1614
        }
1615
1616
        $fieldName = $this->MM_oppositeUsage[$tableName][0];
1617
        if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) {
1618
            return $referenceValues;
1619
        }
1620
1621
        $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1622
        if (!empty($configuration['MM_insert_fields'])) {
1623
            $referenceValues = array_merge($configuration['MM_insert_fields'], $referenceValues);
1624
        } elseif (!empty($configuration['MM_match_fields'])) {
1625
            $referenceValues = array_merge($configuration['MM_match_fields'], $referenceValues);
1626
        }
1627
1628
        return $referenceValues;
1629
    }
1630
1631
    /**
1632
     * Gets the record uid of the live default record. If already
1633
     * pointing to the live record, the submitted record uid is returned.
1634
     *
1635
     * @param string $tableName
1636
     * @param int|string $id
1637
     * @return int
1638
     */
1639
    protected function getLiveDefaultId($tableName, $id)
1640
    {
1641
        $liveDefaultId = BackendUtility::getLiveVersionIdOfRecord($tableName, $id);
1642
        if ($liveDefaultId === null) {
1643
            $liveDefaultId = $id;
1644
        }
1645
        return (int)$liveDefaultId;
1646
    }
1647
1648
    /**
1649
     * Removes empty values (null, '0', 0, false).
1650
     *
1651
     * @param int[] $ids
1652
     * @return array
1653
     */
1654
    protected function sanitizeIds(array $ids): array
1655
    {
1656
        return array_filter($ids);
1657
    }
1658
1659
    /**
1660
     * @param string $tableName
1661
     * @param int[] $ids
1662
     * @param array $sortingStatement
1663
     * @return PlainDataResolver
1664
     */
1665
    protected function getResolver($tableName, array $ids, array $sortingStatement = null)
1666
    {
1667
        /** @var PlainDataResolver $resolver */
1668
        $resolver = GeneralUtility::makeInstance(
1669
            PlainDataResolver::class,
1670
            $tableName,
1671
            $ids,
1672
            $sortingStatement
1673
        );
1674
        $resolver->setWorkspaceId($this->getWorkspaceId());
1675
        $resolver->setKeepDeletePlaceholder(true);
1676
        $resolver->setKeepLiveIds($this->useLiveReferenceIds);
1677
        return $resolver;
1678
    }
1679
1680
    /**
1681
     * @param string $tableName
1682
     * @return Connection
1683
     */
1684
    protected function getConnectionForTableName(string $tableName)
1685
    {
1686
        return GeneralUtility::makeInstance(ConnectionPool::class)
1687
            ->getConnectionForTable($tableName);
1688
    }
1689
}
1690