Passed
Push — master ( 2b6b2d...30755c )
by
unknown
14:27
created

RelationHandler::start()   F

Complexity

Conditions 24
Paths 8064

Size

Total Lines 79
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

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