• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FluidTYPO3 / flux / 27757211628

18 Jun 2026 11:46AM UTC coverage: 89.162% (-3.5%) from 92.646%
27757211628

Pull #2288

github

web-flow
Merge 967f03443 into 2614049c6
Pull Request #2288: [FEATURE] Prepare for v14 support

210 of 348 new or added lines in 56 files covered. (60.34%)

121 existing lines in 9 files now uncovered.

6228 of 6985 relevant lines covered (89.16%)

40.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

47.74
/Classes/Integration/HookSubscribers/DataHandlerSubscriber.php
1
<?php
2

3
namespace FluidTYPO3\Flux\Integration\HookSubscribers;
4

5
/*
6
 * This file is part of the FluidTYPO3/Flux project under GPLv2 or later.
7
 *
8
 * For the full copyright and license information, please read the
9
 * LICENSE.md file that was distributed with this source code.
10
 */
11

12
use FluidTYPO3\Flux\Enum\ExtensionOption;
13
use FluidTYPO3\Flux\Provider\Interfaces\GridProviderInterface;
14
use FluidTYPO3\Flux\Provider\Interfaces\RecordProcessingProvider;
15
use FluidTYPO3\Flux\Provider\PageProvider;
16
use FluidTYPO3\Flux\Provider\ProviderResolver;
17
use FluidTYPO3\Flux\Utility\ColumnNumberUtility;
18
use FluidTYPO3\Flux\Utility\DoctrineQueryProxy;
19
use FluidTYPO3\Flux\Utility\ExtensionConfigurationUtility;
20
use TYPO3\CMS\Backend\Utility\BackendUtility;
21
use TYPO3\CMS\Core\Database\Connection;
22
use TYPO3\CMS\Core\Database\ConnectionPool;
23
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
24
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
25
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
26
use TYPO3\CMS\Core\DataHandling\DataHandler;
27
use TYPO3\CMS\Core\Utility\GeneralUtility;
28

29
class DataHandlerSubscriber
30
{
31
    protected static array $copiedRecords = [];
32

33
    /**
34
     * @param string $command Command that was executed
35
     * @param string $table The table TCEmain is currently processing
36
     * @param string $id The records id (if any)
37
     * @param array $fieldArray The field names and their values to be processed
38
     * @param DataHandler $reference Reference to the parent object (TCEmain)
39
     * @return void
40
     */
41
    // @phpcs:ignore PSR1.Methods.CamelCapsMethodName
42
    public function processDatamap_afterDatabaseOperations($command, $table, $id, $fieldArray, $reference)
43
    {
44
        if ($GLOBALS['BE_USER']->workspace) {
×
NEW
45
            $record = BackendUtility::getRecord($table, (int) $id);
×
46
        } else {
47
            $record = $reference->datamap[$table][$id] ?? null;
×
48
        }
49

50
        if ($record !== null) {
×
51
            /** @var RecordProcessingProvider[] $providers */
52
            $providers = $this->getProviderResolver()->resolveConfigurationProviders(
×
53
                $table,
×
54
                null,
×
55
                $record,
×
56
                null,
×
57
                [RecordProcessingProvider::class]
×
58
            );
×
59

60
            foreach ($providers as $provider) {
×
NEW
61
                if ($provider->postProcessRecord($command, (int) $id, $record, $reference, [])) {
×
62
                    break;
×
63
                }
64
            }
65
        }
66

67
        if ($table !== 'tt_content'
×
68
            || $command !== 'new'
×
69
            || !isset($fieldArray['t3_origuid'])
×
70
            || !$fieldArray['t3_origuid']
×
71
        ) {
72
            // The action was not for tt_content, not a "create new" action, or not a "copy" or "copyToLanguage" action.
73
            return;
×
74
        }
75

76
        $originalRecord = $this->getSingleRecordWithoutRestrictions($table, $fieldArray['t3_origuid'], 'colPos');
×
77
        if ($originalRecord === null) {
×
78
            // Original record has been hard-deleted and can no longer be loaded. Processing must stop.
79
            return;
×
80
        }
81
        $originalParentUid = ColumnNumberUtility::calculateParentUid($originalRecord['colPos']);
×
82
        $newColumnPosition = 0;
×
83

84
        if (!empty($fieldArray['l18n_parent'])) {
×
85
            // Command was "localize", read colPos value from the translation parent and use directly
86
            $newColumnPosition = $this->getSingleRecordWithoutRestrictions(
×
87
                $table,
×
88
                $fieldArray['l18n_parent'],
×
89
                'colPos'
×
90
            )['colPos'] ?? null;
×
91
        } elseif (isset(static::$copiedRecords[$originalParentUid])) {
×
92
            // The parent of the original version of the record that was copied, was also copied in the same request;
93
            // this means the record that was copied, was copied as a recursion operation. Look up the most recent copy
94
            // of the original record's parent and create a new column position number based on the new parent.
95
            $newParentRecord = $this->getMostRecentCopyOfRecord($originalParentUid);
×
96
            if ($newParentRecord !== null) {
×
97
                $newColumnPosition = ColumnNumberUtility::calculateColumnNumberForParentAndColumn(
×
98
                    $newParentRecord['uid'],
×
99
                    ColumnNumberUtility::calculateLocalColumnNumber($originalRecord['colPos'])
×
100
                );
×
101
            }
102
        } elseif (($fieldArray['colPos'] ?? 0) >= ColumnNumberUtility::MULTIPLIER) {
×
103
            // Record is a child record, the updated field array still indicates it is a child (was not pasted outside
104
            // of parent, rather, parent was pasted somewhere else).
105
            // If language of child record is different from resolved parent (copyToLanguage occurred), resolve the
106
            // right parent for the language and update the column position accordingly.
NEW
107
            $recordUid = (int) ($record['uid'] ?? $id);
×
108
            $originalParentUid = ColumnNumberUtility::calculateParentUid($fieldArray['colPos']);
×
109
            $originalParent = $this->getSingleRecordWithoutRestrictions($table, $originalParentUid, 'sys_language_uid');
×
110
            $currentRecordLanguageUid = $fieldArray['sys_language_uid']
×
111
                ?? $this->getSingleRecordWithoutRestrictions($table, $recordUid, 'sys_language_uid')['sys_language_uid']
×
112
                ?? 0;
×
113
            if ($originalParent !== null && $originalParent['sys_language_uid'] !== $currentRecordLanguageUid) {
×
114
                // copyToLanguage case. Resolve the most recent translated version of the parent record in language of
115
                // child record, and calculate the new column position number based on it.
116
                $newParentRecord = $this->getTranslatedVersionOfParentInLanguageOnPage(
×
117
                    (int) $currentRecordLanguageUid,
×
118
                    (int) $fieldArray['pid'],
×
119
                    $originalParentUid
×
120
                );
×
121
                if ($newParentRecord !== null) {
×
122
                    $newColumnPosition = ColumnNumberUtility::calculateColumnNumberForParentAndColumn(
×
123
                        $newParentRecord['uid'],
×
124
                        ColumnNumberUtility::calculateLocalColumnNumber($fieldArray['colPos'])
×
125
                    );
×
126
                }
127
            }
128
        }
129

130
        if ($newColumnPosition > 0) {
×
131
            $queryBuilder = $this->createQueryBuilderForTable($table);
×
132
            $expr = $queryBuilder->expr();
×
133
            $andMethodName = method_exists($expr, 'and') ? 'and' : 'andX';
×
134
            $queryBuilder->update($table)->set('colPos', $newColumnPosition, true, Connection::PARAM_INT)->where(
×
135
                $expr->eq(
×
136
                    'uid',
×
137
                    $queryBuilder->createNamedParameter($reference->substNEWwithIDs[$id], Connection::PARAM_INT)
×
138
                )
×
139
            )->orWhere(
×
140
                $expr->$andMethodName(
×
141
                    $expr->eq(
×
142
                        't3ver_oid',
×
143
                        $queryBuilder->createNamedParameter($reference->substNEWwithIDs[$id], Connection::PARAM_INT)
×
144
                    ),
×
145
                    $expr->eq(
×
146
                        't3ver_wsid',
×
147
                        $queryBuilder->createNamedParameter($GLOBALS['BE_USER']->workspace, Connection::PARAM_INT)
×
148
                    )
×
149
                )
×
150
            );
×
151
            DoctrineQueryProxy::executeStatementOnQueryBuilder($queryBuilder);
×
152
        }
153

154
        static::$copiedRecords[$fieldArray['t3_origuid']] = true;
×
155
    }
156

157
    /**
158
     * @param array $fieldArray
159
     * @param string $table
160
     * @param int|string $id
161
     * @param DataHandler $dataHandler
162
     * @return void
163
     */
164
    // @phpcs:ignore PSR1.Methods.CamelCapsMethodName
165
    public function processDatamap_preProcessFieldArray(array &$fieldArray, $table, $id, DataHandler $dataHandler)
166
    {
167
        $isNewRecord = strpos((string) $id, 'NEW') === 0;
12✔
168
        $isTranslatedRecord = ($fieldArray['l10n_source'] ?? 0) > 0;
12✔
169
        $pageIntegrationEnabled = ExtensionConfigurationUtility::getOption(ExtensionOption::OPTION_PAGE_INTEGRATION);
12✔
170
        $isPageRecord = $table === 'pages';
12✔
171
        if ($pageIntegrationEnabled && $isPageRecord && $isNewRecord && $isTranslatedRecord) {
12✔
172
            // Record is a newly created page and is a translation of a page. In all likelyhood (but we can't actually
173
            // know for sure since TYPO3 uses a nested DataHandler for this...) this record is the result of a blank
174
            // initial copy of the original language's record. We may want to copy the "Page Configuration" fields'
175
            // values from the original record.
176
            if (!isset($fieldArray[PageProvider::FIELD_NAME_MAIN], $fieldArray[PageProvider::FIELD_NAME_SUB])) {
4✔
177
                // To make completely sure, we only want to copy those values if both "Page Configuration" fields are
178
                // completely omitted from the incoming field array.
179
                $originalLanguageRecord = $this->getSingleRecordWithoutRestrictions(
4✔
180
                    'pages',
4✔
181
                    $fieldArray['l10n_source'],
4✔
182
                    PageProvider::FIELD_NAME_MAIN . ',' . PageProvider::FIELD_NAME_SUB
4✔
183
                );
4✔
184
                if ($originalLanguageRecord) {
4✔
185
                    $fieldArray[PageProvider::FIELD_NAME_MAIN] = $originalLanguageRecord[PageProvider::FIELD_NAME_MAIN];
4✔
186
                    $fieldArray[PageProvider::FIELD_NAME_SUB] = $originalLanguageRecord[PageProvider::FIELD_NAME_SUB];
4✔
187
                }
188
            }
189
        }
190

191
        // Handle "$table.$field" named fields where $table is the valid TCA table name and $field is an existing TCA
192
        // field. Updated value will still be subject to permission checks.
193
        $resolver = $this->getProviderResolver();
12✔
194
        foreach ($fieldArray as $fieldName => $fieldValue) {
12✔
195
            if (($GLOBALS["TCA"][$table]["columns"][$fieldName]["config"]["type"] ?? '') === 'flex') {
12✔
196
                $primaryConfigurationProvider = $resolver->resolvePrimaryConfigurationProvider(
4✔
197
                    $table,
4✔
198
                    $fieldName,
4✔
199
                    $fieldArray
4✔
200
                );
4✔
201

202
                if ($primaryConfigurationProvider
4✔
203
                    && is_array($fieldArray[$fieldName])
4✔
204
                    && array_key_exists('data', $fieldArray[$fieldName])
4✔
205
                ) {
206
                    foreach ($fieldArray[$fieldName]['data'] as $sheet) {
4✔
207
                        foreach ($sheet['lDEF'] as $key => $value) {
4✔
208
                            if (strpos($key, '.') !== false) {
4✔
209
                                [$possibleTableName, $columnName] = explode('.', $key, 2);
4✔
210
                                if ($possibleTableName === $table
4✔
211
                                    && isset($GLOBALS['TCA'][$table]['columns'][$columnName])
4✔
212
                                ) {
213
                                    $fieldArray[$columnName] = $value['vDEF'];
4✔
214
                                }
215
                            }
216
                        }
217
                    }
218
                }
219
            }
220
        }
221

222
        if ($table !== 'tt_content' || !is_integer($id)) {
12✔
223
            return;
8✔
224
        }
225

226
        // TYPO3 issue https://forge.typo3.org/issues/85013 "colPos not part of $fieldArray when dropping in top column"
227
        // TODO: remove when expected solution, the inclusion of colPos in $fieldArray, is merged and released in TYPO3
228
        if (!array_key_exists('colPos', $fieldArray)) {
4✔
229
            $record = $this->getSingleRecordWithoutRestrictions($table, (int) $id, 'pid, colPos, l18n_parent');
4✔
230
            $uidInDefaultLanguage = $record['l18n_parent'] ?? null;
4✔
231
            if ($uidInDefaultLanguage && isset($dataHandler->datamap[$table][$uidInDefaultLanguage]['colPos'])) {
4✔
232
                $fieldArray['colPos'] = (int) $dataHandler->datamap[$table][$uidInDefaultLanguage]['colPos'];
4✔
233
            }
234
        }
235
    }
236

237
    /**
238
     * @param string $table
239
     * @param int $id
240
     * @param string $command
241
     * @param mixed $value
242
     * @param DataHandler $dataHandler
243
     * @return void
244
     */
245
    protected function cascadeCommandToChildRecords(
246
        string $table,
247
        int $id,
248
        string $command,
249
        $value,
250
        DataHandler $dataHandler
251
    ) {
252
        [, $childRecords] = $this->getParentAndRecordsNestedInGrid(
20✔
253
            $table,
20✔
254
            (int)$id,
20✔
255
            'uid, pid',
20✔
256
            false,
20✔
257
            $command
20✔
258
        );
20✔
259

260
        if (empty($childRecords)) {
20✔
261
            return;
20✔
262
        }
263

264
        foreach ($childRecords as $childRecord) {
20✔
265
            $childRecordUid = $childRecord['uid'];
20✔
266
            $dataHandler->cmdmap[$table][$childRecordUid][$command] = $value;
20✔
267
            $this->cascadeCommandToChildRecords($table, $childRecordUid, $command, $value, $dataHandler);
20✔
268
        }
269
    }
270

271
    /**
272
     * @param DataHandler $dataHandler
273
     * @return void
274
     */
275
    // @phpcs:ignore PSR1.Methods.CamelCapsMethodName
276
    public function processCmdmap_beforeStart(DataHandler $dataHandler)
277
    {
278
        foreach ($dataHandler->cmdmap as $table => $commandSets) {
36✔
279
            if ($table !== 'tt_content') {
36✔
280
                continue;
8✔
281
            }
282

283
            foreach ($commandSets as $id => $commands) {
28✔
284
                foreach ($commands as $command => $value) {
28✔
285
                    switch ($command) {
286
                        case 'move':
28✔
287
                            // Verify that the target column is not within the element or any child hereof.
288
                            if (is_array($value) && isset($value['update']['colPos'])) {
4✔
289
                                $invalidColumnNumbers = $this->fetchAllColumnNumbersBeneathParent((int) $id);
4✔
290
                                // Only react to move commands which contain a target colPos
291
                                if (in_array((int) $value['update']['colPos'], $invalidColumnNumbers, true)) {
4✔
292
                                    // Invalid target detected - delete the "move" command so it does not happen, and
293
                                    // dispatch an error message.
294
                                    unset($dataHandler->cmdmap[$table][$id]);
4✔
295
                                    $dataHandler->log(
4✔
296
                                        $table,
4✔
297
                                        (int) $id,
4✔
298
                                        4,
4✔
299
                                        0,
4✔
300
                                        1,
4✔
301
                                        'Record not moved, would become child of self'
4✔
302
                                    );
4✔
303
                                }
304
                            }
305
                            break;
4✔
306
                        case 'delete':
24✔
307
                        case 'undelete':
20✔
308
                        case 'copyToLanguage':
16✔
309
                        case 'localize':
12✔
310
                            $this->cascadeCommandToChildRecords($table, (int)$id, $command, $value, $dataHandler);
16✔
311
                            break;
16✔
312
                        case 'copy':
8✔
313
                            if (is_array($value)) {
4✔
314
                                unset($value['update']['colPos']);
4✔
315
                            }
316
                            $this->cascadeCommandToChildRecords($table, (int)$id, $command, $value, $dataHandler);
4✔
317
                            break;
4✔
318
                        default:
319
                            break;
4✔
320
                    }
321
                }
322
            }
323
        }
324
    }
325

326
    /**
327
     * Command post processing method
328
     *
329
     * Like other pre/post methods this method calls the corresponding
330
     * method on Providers which match the table/id(record) being passed.
331
     *
332
     * In addition, this method also listens for paste commands executed
333
     * via the TYPO3 clipboard, since such methods do not necessarily
334
     * trigger the "normal" record move hooks (which we also subscribe
335
     * to and react to in moveRecord_* methods).
336
     *
337
     * @param string $command The TCEmain operation status, fx. 'update'
338
     * @param string $table The table TCEmain is currently processing
339
     * @param string $id The records id (if any)
340
     * @param string $relativeTo Filled if command is relative to another element
341
     * @param DataHandler $reference Reference to the parent object (TCEmain)
342
     * @param array $pasteUpdate
343
     * @param array $pasteDataMap
344
     * @return void
345
     */
346
    // @phpcs:ignore PSR1.Methods.CamelCapsMethodName
347
    public function processCmdmap_postProcess(
348
        &$command,
349
        $table,
350
        $id,
351
        &$relativeTo,
352
        &$reference,
353
        &$pasteUpdate,
354
        &$pasteDataMap
355
    ) {
356

357
        /*
358
        if ($table === 'pages' && $command === 'copy') {
359
            foreach ($reference->copyMappingArray['tt_content'] ?? [] as $originalRecordUid => $copiedRecordUid) {
360
                $copiedRecord = $this->getSingleRecordWithoutRestrictions('tt_content', $copiedRecordUid, 'colPos');
361
                if ($copiedRecord['colPos'] < ColumnNumberUtility::MULTIPLIER) {
362
                    continue;
363
                }
364

365
                $oldParentUid = ColumnNumberUtility::calculateParentUid($copiedRecord['colPos']);
366
                $newParentUid = $reference->copyMappingArray['tt_content'][$oldParentUid];
367

368
                $overrideArray['colPos'] = ColumnNumberUtility::calculateColumnNumberForParentAndColumn(
369
                    $newParentUid,
370
                    ColumnNumberUtility::calculateLocalColumnNumber((int) $copiedRecord['colPos'])
371
                );
372

373
                // Note here: it is safe to directly update the DB in this case, since we filtered out any
374
                // non-"copy" actions, and "copy" is the only action which requires adjustment.
375
                $reference->updateDB('tt_content', $copiedRecordUid, $overrideArray);
376

377
                // But if we also have a workspace version of the record recorded, it too must be updated:
378
                if (isset($reference->autoVersionIdMap['tt_content'][$copiedRecordUid])) {
379
                    $reference->updateDB(
380
                        'tt_content',
381
                        $reference->autoVersionIdMap['tt_content'][$copiedRecordUid],
382
                        $overrideArray
383
                    );
384
                }
385
            }
386
        }
387
        */
388

389
        if ($table !== 'tt_content' || $command !== 'move') {
16✔
390
            return;
4✔
391
        }
392

393
        [$originalRecord, $recordsToProcess] = $this->getParentAndRecordsNestedInGrid(
12✔
394
            $table,
12✔
395
            (int) $id,
12✔
396
            'uid, pid, colPos',
12✔
397
            false,
12✔
398
            $command
12✔
399
        );
12✔
400

401
        if (empty($recordsToProcess)) {
12✔
402
            return;
4✔
403
        }
404

405
        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
8✔
406
        $languageUid = (int)($reference->cmdmap[$table][$id][$command]['update'][$languageField]
8✔
407
            ?? $originalRecord[$languageField]);
8✔
408

409
        if ($relativeTo > 0) {
8✔
410
            $destinationPid = $relativeTo;
4✔
411
        } else {
412
            $relativeRecord = $this->getSingleRecordWithoutRestrictions(
4✔
413
                $table,
4✔
414
                (int) abs((int) $relativeTo),
4✔
415
                'pid'
4✔
416
            );
4✔
417
            $destinationPid = $relativeRecord['pid'] ?? $relativeTo;
4✔
418
        }
419

420
        $this->recursivelyMoveChildRecords(
8✔
421
            $table,
8✔
422
            $recordsToProcess,
8✔
423
            (int) $destinationPid,
8✔
424
            $languageUid,
8✔
425
            $reference
8✔
426
        );
8✔
427
    }
428

429
    protected function fetchAllColumnNumbersBeneathParent(int $parentUid): array
430
    {
431
        [, $recordsToProcess, $bannedColumnNumbers] = $this->getParentAndRecordsNestedInGrid(
×
432
            'tt_content',
×
433
            $parentUid,
×
434
            'uid, colPos'
×
435
        );
×
436
        $invalidColumnPositions = $bannedColumnNumbers;
×
437
        foreach ($recordsToProcess as $childRecord) {
×
438
            $invalidColumnPositions += $this->fetchAllColumnNumbersBeneathParent($childRecord['uid']);
×
439
        }
440
        return (array) $invalidColumnPositions;
×
441
    }
442

443
    protected function recursivelyMoveChildRecords(
444
        string $table,
445
        array $recordsToProcess,
446
        int $pageUid,
447
        int $languageUid,
448
        DataHandler $dataHandler
449
    ): void {
450
        $subCommandMap = [];
8✔
451

452
        foreach ($recordsToProcess as $recordToProcess) {
8✔
453
            $recordUid = $recordToProcess['uid'];
8✔
454
            $subCommandMap[$table][$recordUid]['move'] = [
8✔
455
                'action' => 'paste',
8✔
456
                'target' => $pageUid,
8✔
457
                'update' => [
8✔
458
                    $GLOBALS['TCA']['tt_content']['ctrl']['languageField'] => $languageUid,
8✔
459
                ],
8✔
460
            ];
8✔
461
        }
462

463
        if (!empty($subCommandMap)) {
8✔
464
            /** @var DataHandler $dataHandler */
465
            $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
8✔
466
            $dataHandler->copyMappingArray = $dataHandler->copyMappingArray;
8✔
467
            $dataHandler->start([], $subCommandMap);
8✔
468
            $dataHandler->process_cmdmap();
8✔
469
        }
470
    }
471

472
    /**
473
     * @codeCoverageIgnore
474
     */
475
    protected function getSingleRecordWithRestrictions(string $table, int $uid, string $fieldsToSelect): ?array
476
    {
477
        /** @var DeletedRestriction $deletedRestriction */
478
        $deletedRestriction = GeneralUtility::makeInstance(DeletedRestriction::class);
479
        $queryBuilder = $this->createQueryBuilderForTable($table);
480
        $queryBuilder->getRestrictions()->removeAll()->add($deletedRestriction);
481
        $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
482
            ->from($table)
483
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)));
484
        /** @var array|false $firstResult */
485
        $firstResult = DoctrineQueryProxy::fetchAssociative(
486
            DoctrineQueryProxy::executeQueryOnQueryBuilder($queryBuilder)
487
        );
488
        return $firstResult ?: null;
489
    }
490

491
    /**
492
     * @codeCoverageIgnore
493
     */
494
    protected function getSingleRecordWithoutRestrictions(string $table, int $uid, string $fieldsToSelect): ?array
495
    {
496
        $queryBuilder = $this->createQueryBuilderForTable($table);
497
        $queryBuilder->getRestrictions()->removeAll();
498
        $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
499
            ->from($table)
500
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)));
501
        /** @var array|false $firstResult */
502
        $firstResult = DoctrineQueryProxy::fetchAssociative(
503
            DoctrineQueryProxy::executeQueryOnQueryBuilder($queryBuilder)
504
        );
505
        return $firstResult ?: null;
506
    }
507

508
    /**
509
     * @codeCoverageIgnore
510
     */
511
    protected function getMostRecentCopyOfRecord(int $uid, string $fieldsToSelect = 'uid'): ?array
512
    {
513
        $queryBuilder = $this->createQueryBuilderForTable('tt_content');
514
        $queryBuilder->getRestrictions()->removeAll();
515
        $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
516
            ->from('tt_content')
517
            ->orderBy('uid', 'DESC')
518
            ->setMaxResults(1)
519
            ->where(
520
                $queryBuilder->expr()->eq('t3_origuid', $uid),
521
                $queryBuilder->expr()->neq('t3ver_state', -1)
522
            );
523
        /** @var array|false $firstResult */
524
        $firstResult = DoctrineQueryProxy::fetchAssociative(
525
            DoctrineQueryProxy::executeQueryOnQueryBuilder($queryBuilder)
526
        );
527
        return $firstResult ?: null;
528
    }
529

530
    /**
531
     * @codeCoverageIgnore
532
     */
533
    protected function getTranslatedVersionOfParentInLanguageOnPage(
534
        int $languageUid,
535
        int $pageUid,
536
        int $originalParentUid,
537
        string $fieldsToSelect = '*'
538
    ): ?array {
539
        /** @var DeletedRestriction $deletedRestriction */
540
        $deletedRestriction = GeneralUtility::makeInstance(DeletedRestriction::class);
541
        $queryBuilder = $this->createQueryBuilderForTable('tt_content');
542
        $queryBuilder->getRestrictions()->removeAll()->add($deletedRestriction);
543
        $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
544
            ->from('tt_content')
545
            ->setMaxResults(1)
546
            ->orderBy('uid', 'DESC')
547
            ->where(
548
                $queryBuilder->expr()->eq(
549
                    'sys_language_uid',
550
                    $queryBuilder->createNamedParameter($languageUid, Connection::PARAM_INT)
551
                ),
552
                $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageUid, Connection::PARAM_INT)),
553
                $queryBuilder->expr()->eq(
554
                    'l10n_source',
555
                    $queryBuilder->createNamedParameter($originalParentUid, Connection::PARAM_INT)
556
                )
557
            );
558
        /** @var array|false $firstResult */
559
        $firstResult = DoctrineQueryProxy::fetchAssociative(
560
            DoctrineQueryProxy::executeQueryOnQueryBuilder($queryBuilder)
561
        );
562
        return $firstResult ?: null;
563
    }
564

565
    protected function getParentAndRecordsNestedInGrid(
566
        string $table,
567
        int $parentUid,
568
        string $fieldsToSelect,
569
        bool $respectPid = false,
570
        ?string $command = null
571
    ):array {
572
        // A Provider must be resolved which implements the GridProviderInterface
573
        $resolver = $this->getProviderResolver();
×
574
        $originalRecord = (array) $this->getSingleRecordWithoutRestrictions($table, $parentUid, '*');
×
575

576
        $primaryProvider = $resolver->resolvePrimaryConfigurationProvider(
×
577
            $table,
×
578
            null,
×
579
            $originalRecord,
×
580
            null,
×
581
            [GridProviderInterface::class]
×
582
        );
×
583

584
        if (!$primaryProvider) {
×
585
            return [
×
586
                $originalRecord,
×
587
                [],
×
588
                [],
×
589
            ];
×
590
        }
591

592
        // The Grid this Provider returns must contain at least one column
593
        $childColPosValues = $primaryProvider->getGrid($originalRecord)->buildColumnPositionValues($originalRecord);
×
594

595
        if (empty($childColPosValues)) {
×
596
            return [
×
597
                $originalRecord,
×
598
                [],
×
599
                [],
×
600
            ];
×
601
        }
602

603
        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
×
604

605
        $queryBuilder = $this->createQueryBuilderForTable($table);
×
606
        if ($command === 'undelete') {
×
607
            $queryBuilder->getRestrictions()->removeAll();
×
608
        } else {
609
            $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
×
610
        }
611

612
        $query = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
×
613
            ->from($table)
×
614
            ->andWhere(
×
615
                $queryBuilder->expr()->in(
×
616
                    'colPos',
×
617
                    $queryBuilder->createNamedParameter($childColPosValues, Connection::PARAM_INT_ARRAY)
×
618
                ),
×
619
                $queryBuilder->expr()->eq($languageField, (int)$originalRecord[$languageField]),
×
620
                $queryBuilder->expr()->in(
×
621
                    't3ver_wsid',
×
622
                    $queryBuilder->createNamedParameter(
×
623
                        [0, $GLOBALS['BE_USER']->workspace],
×
624
                        Connection::PARAM_INT_ARRAY
×
625
                    )
×
626
                )
×
627
            )->orderBy('sorting', 'DESC');
×
628

629
        if ($respectPid) {
×
630
            $query->andWhere($queryBuilder->expr()->eq('pid', $originalRecord['pid']));
×
631
        } else {
632
            $query->andWhere($queryBuilder->expr()->neq('pid', -1));
×
633
        }
634

635
        $records = DoctrineQueryProxy::fetchAllAssociative(DoctrineQueryProxy::executeQueryOnQueryBuilder($query));
×
636

637
        // Selecting records to return. The "sorting DESC" is very intentional; copy operations will place records
638
        // into the top of columns which means reading records in reverse order causes the correct final order.
639
        return [
×
640
            $originalRecord,
×
641
            $records,
×
642
            $childColPosValues
×
643
        ];
×
644
    }
645

646
    /**
647
     * @codeCoverageIgnore
648
     */
649
    protected function getProviderResolver(): ProviderResolver
650
    {
651
        /** @var ProviderResolver $providerResolver */
652
        $providerResolver = GeneralUtility::makeInstance(ProviderResolver::class);
653
        return $providerResolver;
654
    }
655

656
    /**
657
     * @codeCoverageIgnore
658
     */
659
    protected function createQueryBuilderForTable(string $table): QueryBuilder
660
    {
661
        /** @var ConnectionPool $connectionPool */
662
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
663
        return $connectionPool->getQueryBuilderForTable($table);
664
    }
665
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc