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

FluidTYPO3 / flux / 12930122773

23 Jan 2025 01:22PM UTC coverage: 92.829% (-0.07%) from 92.901%
12930122773

Pull #2209

github

web-flow
Merge 83c833b70 into cf49f7a79
Pull Request #2209: [WIP] Compatibility with TYPO3 v13

86 of 112 new or added lines in 31 files covered. (76.79%)

5 existing lines in 3 files now uncovered.

7055 of 7600 relevant lines covered (92.83%)

65.02 hits per line

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

47.83
/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\Content\ContentTypeManager;
13
use FluidTYPO3\Flux\Enum\ExtensionOption;
14
use FluidTYPO3\Flux\Provider\Interfaces\GridProviderInterface;
15
use FluidTYPO3\Flux\Provider\Interfaces\RecordProcessingProvider;
16
use FluidTYPO3\Flux\Provider\PageProvider;
17
use FluidTYPO3\Flux\Provider\ProviderResolver;
18
use FluidTYPO3\Flux\Utility\ColumnNumberUtility;
19
use FluidTYPO3\Flux\Utility\DoctrineQueryProxy;
20
use FluidTYPO3\Flux\Utility\ExtensionConfigurationUtility;
21
use TYPO3\CMS\Backend\Utility\BackendUtility;
22
use TYPO3\CMS\Core\Cache\CacheManager;
23
use TYPO3\CMS\Core\Database\Connection;
24
use TYPO3\CMS\Core\Database\ConnectionPool;
25
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
26
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
27
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
28
use TYPO3\CMS\Core\DataHandling\DataHandler;
29
use TYPO3\CMS\Core\Utility\GeneralUtility;
30

31
class DataHandlerSubscriber
32
{
33
    protected static array $copiedRecords = [];
34

35
    public function clearCacheCommand(array $command): void
36
    {
37
        if (($command['cacheCmd'] ?? null) === 'all' || ($command['cacheCmd'] ?? null) === 'system') {
21✔
38
            $this->regenerateContentTypes();
14✔
39
        }
40
    }
41

42
    /**
43
     * @param string $command Command that was executed
44
     * @param string $table The table TCEmain is currently processing
45
     * @param string $id The records id (if any)
46
     * @param array $fieldArray The field names and their values to be processed
47
     * @param DataHandler $reference Reference to the parent object (TCEmain)
48
     * @return void
49
     */
50
    // @phpcs:ignore PSR1.Methods.CamelCapsMethodName
51
    public function processDatamap_afterDatabaseOperations($command, $table, $id, $fieldArray, $reference)
52
    {
53
        if ($table === 'content_types') {
×
54
            // Changing records in table "content_types" has to flush the system cache to regenerate various cached
55
            // definitions of plugins etc. that are based on those "content_types" records.
56
            /** @var CacheManager $cacheManager */
57
            $cacheManager = GeneralUtility::makeInstance(CacheManager::class);
×
58
            $cacheManager->flushCachesInGroup('system');
×
59
            $this->regenerateContentTypes();
×
60
            return;
×
61
        }
62

63
        if ($GLOBALS['BE_USER']->workspace) {
×
64
            $record = BackendUtility::getRecord($table, (integer) $id);
×
65
        } else {
66
            $record = $reference->datamap[$table][$id] ?? null;
×
67
        }
68

69
        if ($record !== null) {
×
70
            /** @var RecordProcessingProvider[] $providers */
71
            $providers = $this->getProviderResolver()->resolveConfigurationProviders(
×
72
                $table,
×
73
                null,
×
74
                $record,
×
75
                null,
×
76
                [RecordProcessingProvider::class]
×
77
            );
×
78

79
            foreach ($providers as $provider) {
×
NEW
80
                if ($provider->postProcessRecord($command, (integer) $id, $record, $reference, [])) {
×
NEW
81
                    break;
×
82
                }
83
            }
84
        }
85

86
        if ($table !== 'tt_content'
×
87
            || $command !== 'new'
×
88
            || !isset($fieldArray['t3_origuid'])
×
89
            || !$fieldArray['t3_origuid']
×
90
        ) {
91
            // The action was not for tt_content, not a "create new" action, or not a "copy" or "copyToLanguage" action.
92
            return;
×
93
        }
94

95
        $originalRecord = $this->getSingleRecordWithoutRestrictions($table, $fieldArray['t3_origuid'], 'colPos');
×
96
        if ($originalRecord === null) {
×
97
            // Original record has been hard-deleted and can no longer be loaded. Processing must stop.
98
            return;
×
99
        }
100
        $originalParentUid = ColumnNumberUtility::calculateParentUid($originalRecord['colPos']);
×
101
        $newColumnPosition = 0;
×
102

103
        if (!empty($fieldArray['l18n_parent'])) {
×
104
            // Command was "localize", read colPos value from the translation parent and use directly
105
            $newColumnPosition = $this->getSingleRecordWithoutRestrictions(
×
106
                $table,
×
107
                $fieldArray['l18n_parent'],
×
108
                'colPos'
×
109
            )['colPos'] ?? null;
×
110
        } elseif (isset(static::$copiedRecords[$originalParentUid])) {
×
111
            // The parent of the original version of the record that was copied, was also copied in the same request;
112
            // this means the record that was copied, was copied as a recursion operation. Look up the most recent copy
113
            // of the original record's parent and create a new column position number based on the new parent.
114
            $newParentRecord = $this->getMostRecentCopyOfRecord($originalParentUid);
×
115
            if ($newParentRecord !== null) {
×
116
                $newColumnPosition = ColumnNumberUtility::calculateColumnNumberForParentAndColumn(
×
117
                    $newParentRecord['uid'],
×
118
                    ColumnNumberUtility::calculateLocalColumnNumber($originalRecord['colPos'])
×
119
                );
×
120
            }
121
        } elseif (($fieldArray['colPos'] ?? 0) >= ColumnNumberUtility::MULTIPLIER) {
×
122
            // Record is a child record, the updated field array still indicates it is a child (was not pasted outside
123
            // of parent, rather, parent was pasted somewhere else).
124
            // If language of child record is different from resolved parent (copyToLanguage occurred), resolve the
125
            // right parent for the language and update the column position accordingly.
126
            $recordUid = (integer) ($record['uid'] ?? $id);
×
127
            $originalParentUid = ColumnNumberUtility::calculateParentUid($fieldArray['colPos']);
×
128
            $originalParent = $this->getSingleRecordWithoutRestrictions($table, $originalParentUid, 'sys_language_uid');
×
129
            $currentRecordLanguageUid = $fieldArray['sys_language_uid']
×
130
                ?? $this->getSingleRecordWithoutRestrictions($table, $recordUid, 'sys_language_uid')['sys_language_uid']
×
131
                ?? 0;
×
132
            if ($originalParent !== null && $originalParent['sys_language_uid'] !== $currentRecordLanguageUid) {
×
133
                // copyToLanguage case. Resolve the most recent translated version of the parent record in language of
134
                // child record, and calculate the new column position number based on it.
135
                $newParentRecord = $this->getTranslatedVersionOfParentInLanguageOnPage(
×
136
                    (int) $currentRecordLanguageUid,
×
137
                    (int) $fieldArray['pid'],
×
138
                    $originalParentUid
×
139
                );
×
140
                if ($newParentRecord !== null) {
×
141
                    $newColumnPosition = ColumnNumberUtility::calculateColumnNumberForParentAndColumn(
×
142
                        $newParentRecord['uid'],
×
143
                        ColumnNumberUtility::calculateLocalColumnNumber($fieldArray['colPos'])
×
144
                    );
×
145
                }
146
            }
147
        }
148

149
        if ($newColumnPosition > 0) {
×
150
            $queryBuilder = $this->createQueryBuilderForTable($table);
×
NEW
151
            $expr = $queryBuilder->expr();
×
NEW
152
            $andMethodName = method_exists($expr, 'andX') ? 'andX' : 'and';
×
153
            $queryBuilder->update($table)->set('colPos', $newColumnPosition, true, Connection::PARAM_INT)->where(
×
NEW
154
                $expr->eq(
×
155
                    'uid',
×
156
                    $queryBuilder->createNamedParameter($reference->substNEWwithIDs[$id], Connection::PARAM_INT)
×
157
                )
×
158
            )->orWhere(
×
NEW
159
                $expr->$andMethodName(
×
NEW
160
                    $expr->eq(
×
161
                        't3ver_oid',
×
162
                        $queryBuilder->createNamedParameter($reference->substNEWwithIDs[$id], Connection::PARAM_INT)
×
163
                    ),
×
NEW
164
                    $expr->eq(
×
165
                        't3ver_wsid',
×
166
                        $queryBuilder->createNamedParameter($GLOBALS['BE_USER']->workspace, Connection::PARAM_INT)
×
167
                    )
×
168
                )
×
169
            );
×
170
            DoctrineQueryProxy::executeStatementOnQueryBuilder($queryBuilder);
×
171
        }
172

173
        static::$copiedRecords[$fieldArray['t3_origuid']] = true;
×
174
    }
175

176
    /**
177
     * @param array $fieldArray
178
     * @param string $table
179
     * @param int|string $id
180
     * @param DataHandler $dataHandler
181
     * @return void
182
     */
183
    // @phpcs:ignore PSR1.Methods.CamelCapsMethodName
184
    public function processDatamap_preProcessFieldArray(array &$fieldArray, $table, $id, DataHandler $dataHandler)
185
    {
186
        $isNewRecord = strpos((string) $id, 'NEW') === 0;
21✔
187
        $isTranslatedRecord = ($fieldArray['l10n_source'] ?? 0) > 0;
21✔
188
        $pageIntegrationEnabled = ExtensionConfigurationUtility::getOption(ExtensionOption::OPTION_PAGE_INTEGRATION);
21✔
189
        $isPageRecord = $table === 'pages';
21✔
190
        if ($pageIntegrationEnabled && $isPageRecord && $isNewRecord && $isTranslatedRecord) {
21✔
191
            // Record is a newly created page and is a translation of a page. In all likelyhood (but we can't actually
192
            // know for sure since TYPO3 uses a nested DataHandler for this...) this record is the result of a blank
193
            // initial copy of the original language's record. We may want to copy the "Page Configuration" fields'
194
            // values from the original record.
195
            if (!isset($fieldArray[PageProvider::FIELD_NAME_MAIN], $fieldArray[PageProvider::FIELD_NAME_SUB])) {
7✔
196
                // To make completely sure, we only want to copy those values if both "Page Configuration" fields are
197
                // completely omitted from the incoming field array.
198
                $originalLanguageRecord = $this->getSingleRecordWithoutRestrictions(
7✔
199
                    'pages',
7✔
200
                    $fieldArray['l10n_source'],
7✔
201
                    PageProvider::FIELD_NAME_MAIN . ',' . PageProvider::FIELD_NAME_SUB
7✔
202
                );
7✔
203
                if ($originalLanguageRecord) {
7✔
204
                    $fieldArray[PageProvider::FIELD_NAME_MAIN] = $originalLanguageRecord[PageProvider::FIELD_NAME_MAIN];
7✔
205
                    $fieldArray[PageProvider::FIELD_NAME_SUB] = $originalLanguageRecord[PageProvider::FIELD_NAME_SUB];
7✔
206
                }
207
            }
208
        }
209

210
        // Handle "$table.$field" named fields where $table is the valid TCA table name and $field is an existing TCA
211
        // field. Updated value will still be subject to permission checks.
212
        $resolver = $this->getProviderResolver();
21✔
213
        foreach ($fieldArray as $fieldName => $fieldValue) {
21✔
214
            if (($GLOBALS["TCA"][$table]["columns"][$fieldName]["config"]["type"] ?? '') === 'flex') {
21✔
215
                $primaryConfigurationProvider = $resolver->resolvePrimaryConfigurationProvider(
7✔
216
                    $table,
7✔
217
                    $fieldName,
7✔
218
                    $fieldArray
7✔
219
                );
7✔
220

221
                if ($primaryConfigurationProvider
7✔
222
                    && is_array($fieldArray[$fieldName])
7✔
223
                    && array_key_exists('data', $fieldArray[$fieldName])
7✔
224
                ) {
225
                    foreach ($fieldArray[$fieldName]['data'] as $sheet) {
7✔
226
                        foreach ($sheet['lDEF'] as $key => $value) {
7✔
227
                            if (strpos($key, '.') !== false) {
7✔
228
                                [$possibleTableName, $columnName] = explode('.', $key, 2);
7✔
229
                                if ($possibleTableName === $table
7✔
230
                                    && isset($GLOBALS['TCA'][$table]['columns'][$columnName])
7✔
231
                                ) {
232
                                    $fieldArray[$columnName] = $value['vDEF'];
7✔
233
                                }
234
                            }
235
                        }
236
                    }
237
                }
238
            }
239
        }
240

241
        if ($table !== 'tt_content' || !is_integer($id)) {
21✔
242
            return;
14✔
243
        }
244

245
        // TYPO3 issue https://forge.typo3.org/issues/85013 "colPos not part of $fieldArray when dropping in top column"
246
        // TODO: remove when expected solution, the inclusion of colPos in $fieldArray, is merged and released in TYPO3
247
        if (!array_key_exists('colPos', $fieldArray)) {
7✔
248
            $record = $this->getSingleRecordWithoutRestrictions($table, (int) $id, 'pid, colPos, l18n_parent');
7✔
249
            $uidInDefaultLanguage = $record['l18n_parent'] ?? null;
7✔
250
            if ($uidInDefaultLanguage && isset($dataHandler->datamap[$table][$uidInDefaultLanguage]['colPos'])) {
7✔
251
                $fieldArray['colPos'] = (integer) $dataHandler->datamap[$table][$uidInDefaultLanguage]['colPos'];
7✔
252
            }
253
        }
254
    }
255

256
    /**
257
     * @param string $table
258
     * @param int $id
259
     * @param string $command
260
     * @param mixed $value
261
     * @param DataHandler $dataHandler
262
     * @return void
263
     */
264
    protected function cascadeCommandToChildRecords(
265
        string $table,
266
        int $id,
267
        string $command,
268
        $value,
269
        DataHandler $dataHandler
270
    ) {
271
        [, $childRecords] = $this->getParentAndRecordsNestedInGrid(
35✔
272
            $table,
35✔
273
            (int)$id,
35✔
274
            'uid, pid',
35✔
275
            false,
35✔
276
            $command
35✔
277
        );
35✔
278

279
        if (empty($childRecords)) {
35✔
280
            return;
35✔
281
        }
282

283
        foreach ($childRecords as $childRecord) {
35✔
284
            $childRecordUid = $childRecord['uid'];
35✔
285
            $dataHandler->cmdmap[$table][$childRecordUid][$command] = $value;
35✔
286
            $this->cascadeCommandToChildRecords($table, $childRecordUid, $command, $value, $dataHandler);
35✔
287
        }
288
    }
289

290
    /**
291
     * @param DataHandler $dataHandler
292
     * @return void
293
     */
294
    // @phpcs:ignore PSR1.Methods.CamelCapsMethodName
295
    public function processCmdmap_beforeStart(DataHandler $dataHandler)
296
    {
297
        foreach ($dataHandler->cmdmap as $table => $commandSets) {
63✔
298
            if ($table === 'content_types') {
63✔
299
                $this->regenerateContentTypes();
7✔
300
                continue;
7✔
301
            }
302

303
            if ($table !== 'tt_content') {
56✔
304
                continue;
7✔
305
            }
306

307
            foreach ($commandSets as $id => $commands) {
49✔
308
                foreach ($commands as $command => $value) {
49✔
309
                    switch ($command) {
310
                        case 'move':
49✔
311
                            // Verify that the target column is not within the element or any child hereof.
312
                            if (is_array($value) && isset($value['update']['colPos'])) {
7✔
313
                                $invalidColumnNumbers = $this->fetchAllColumnNumbersBeneathParent((integer) $id);
7✔
314
                                // Only react to move commands which contain a target colPos
315
                                if (in_array((int) $value['update']['colPos'], $invalidColumnNumbers, true)) {
7✔
316
                                    // Invalid target detected - delete the "move" command so it does not happen, and
317
                                    // dispatch an error message.
318
                                    unset($dataHandler->cmdmap[$table][$id]);
7✔
319
                                    $dataHandler->log(
7✔
320
                                        $table,
7✔
321
                                        (integer) $id,
7✔
322
                                        4,
7✔
323
                                        0,
7✔
324
                                        1,
7✔
325
                                        'Record not moved, would become child of self'
7✔
326
                                    );
7✔
327
                                }
328
                            }
329
                            break;
7✔
330
                        case 'delete':
42✔
331
                        case 'undelete':
35✔
332
                        case 'copyToLanguage':
28✔
333
                        case 'localize':
21✔
334
                            $this->cascadeCommandToChildRecords($table, (int)$id, $command, $value, $dataHandler);
28✔
335
                            break;
28✔
336
                        case 'copy':
14✔
337
                            if (is_array($value)) {
7✔
338
                                unset($value['update']['colPos']);
7✔
339
                            }
340
                            $this->cascadeCommandToChildRecords($table, (int)$id, $command, $value, $dataHandler);
7✔
341
                            break;
7✔
342
                        default:
343
                            break;
7✔
344
                    }
345
                }
346
            }
347
        }
348
    }
349

350
    /**
351
     * Command post processing method
352
     *
353
     * Like other pre/post methods this method calls the corresponding
354
     * method on Providers which match the table/id(record) being passed.
355
     *
356
     * In addition, this method also listens for paste commands executed
357
     * via the TYPO3 clipboard, since such methods do not necessarily
358
     * trigger the "normal" record move hooks (which we also subscribe
359
     * to and react to in moveRecord_* methods).
360
     *
361
     * @param string $command The TCEmain operation status, fx. 'update'
362
     * @param string $table The table TCEmain is currently processing
363
     * @param string $id The records id (if any)
364
     * @param string $relativeTo Filled if command is relative to another element
365
     * @param DataHandler $reference Reference to the parent object (TCEmain)
366
     * @param array $pasteUpdate
367
     * @param array $pasteDataMap
368
     * @return void
369
     */
370
    // @phpcs:ignore PSR1.Methods.CamelCapsMethodName
371
    public function processCmdmap_postProcess(
372
        &$command,
373
        $table,
374
        $id,
375
        &$relativeTo,
376
        &$reference,
377
        &$pasteUpdate,
378
        &$pasteDataMap
379
    ) {
380

381
        /*
382
        if ($table === 'pages' && $command === 'copy') {
383
            foreach ($reference->copyMappingArray['tt_content'] ?? [] as $originalRecordUid => $copiedRecordUid) {
384
                $copiedRecord = $this->getSingleRecordWithoutRestrictions('tt_content', $copiedRecordUid, 'colPos');
385
                if ($copiedRecord['colPos'] < ColumnNumberUtility::MULTIPLIER) {
386
                    continue;
387
                }
388

389
                $oldParentUid = ColumnNumberUtility::calculateParentUid($copiedRecord['colPos']);
390
                $newParentUid = $reference->copyMappingArray['tt_content'][$oldParentUid];
391

392
                $overrideArray['colPos'] = ColumnNumberUtility::calculateColumnNumberForParentAndColumn(
393
                    $newParentUid,
394
                    ColumnNumberUtility::calculateLocalColumnNumber((int) $copiedRecord['colPos'])
395
                );
396

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

401
                // But if we also have a workspace version of the record recorded, it too must be updated:
402
                if (isset($reference->autoVersionIdMap['tt_content'][$copiedRecordUid])) {
403
                    $reference->updateDB(
404
                        'tt_content',
405
                        $reference->autoVersionIdMap['tt_content'][$copiedRecordUid],
406
                        $overrideArray
407
                    );
408
                }
409
            }
410
        }
411
        */
412

413
        if ($table !== 'tt_content' || $command !== 'move') {
28✔
414
            return;
7✔
415
        }
416

417
        [$originalRecord, $recordsToProcess] = $this->getParentAndRecordsNestedInGrid(
21✔
418
            $table,
21✔
419
            (integer) $id,
21✔
420
            'uid, pid, colPos',
21✔
421
            false,
21✔
422
            $command
21✔
423
        );
21✔
424

425
        if (empty($recordsToProcess)) {
21✔
426
            return;
7✔
427
        }
428

429
        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
14✔
430
        $languageUid = (int)($reference->cmdmap[$table][$id][$command]['update'][$languageField]
14✔
431
            ?? $originalRecord[$languageField]);
14✔
432

433
        if ($relativeTo > 0) {
14✔
434
            $destinationPid = $relativeTo;
7✔
435
        } else {
436
            $relativeRecord = $this->getSingleRecordWithoutRestrictions(
7✔
437
                $table,
7✔
438
                (integer) abs((integer) $relativeTo),
7✔
439
                'pid'
7✔
440
            );
7✔
441
            $destinationPid = $relativeRecord['pid'] ?? $relativeTo;
7✔
442
        }
443

444
        $this->recursivelyMoveChildRecords(
14✔
445
            $table,
14✔
446
            $recordsToProcess,
14✔
447
            (integer) $destinationPid,
14✔
448
            $languageUid,
14✔
449
            $reference
14✔
450
        );
14✔
451
    }
452

453
    protected function fetchAllColumnNumbersBeneathParent(int $parentUid): array
454
    {
455
        [, $recordsToProcess, $bannedColumnNumbers] = $this->getParentAndRecordsNestedInGrid(
×
456
            'tt_content',
×
457
            $parentUid,
×
458
            'uid, colPos'
×
459
        );
×
460
        $invalidColumnPositions = $bannedColumnNumbers;
×
461
        foreach ($recordsToProcess as $childRecord) {
×
462
            $invalidColumnPositions += $this->fetchAllColumnNumbersBeneathParent($childRecord['uid']);
×
463
        }
464
        return (array) $invalidColumnPositions;
×
465
    }
466

467
    protected function recursivelyMoveChildRecords(
468
        string $table,
469
        array $recordsToProcess,
470
        int $pageUid,
471
        int $languageUid,
472
        DataHandler $dataHandler
473
    ): void {
474
        $subCommandMap = [];
14✔
475

476
        foreach ($recordsToProcess as $recordToProcess) {
14✔
477
            $recordUid = $recordToProcess['uid'];
14✔
478
            $subCommandMap[$table][$recordUid]['move'] = [
14✔
479
                'action' => 'paste',
14✔
480
                'target' => $pageUid,
14✔
481
                'update' => [
14✔
482
                    $GLOBALS['TCA']['tt_content']['ctrl']['languageField'] => $languageUid,
14✔
483
                ],
14✔
484
            ];
14✔
485
        }
486

487
        if (!empty($subCommandMap)) {
14✔
488
            /** @var DataHandler $dataHandler */
489
            $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
14✔
490
            $dataHandler->copyMappingArray = $dataHandler->copyMappingArray;
14✔
491
            $dataHandler->start([], $subCommandMap);
14✔
492
            $dataHandler->process_cmdmap();
14✔
493
        }
494
    }
495

496
    /**
497
     * @codeCoverageIgnore
498
     */
499
    protected function getSingleRecordWithRestrictions(string $table, int $uid, string $fieldsToSelect): ?array
500
    {
501
        /** @var DeletedRestriction $deletedRestriction */
502
        $deletedRestriction = GeneralUtility::makeInstance(DeletedRestriction::class);
503
        $queryBuilder = $this->createQueryBuilderForTable($table);
504
        $queryBuilder->getRestrictions()->removeAll()->add($deletedRestriction);
505
        $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
506
            ->from($table)
507
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)));
508
        /** @var array|false $firstResult */
509
        $firstResult = DoctrineQueryProxy::fetchAssociative(
510
            DoctrineQueryProxy::executeQueryOnQueryBuilder($queryBuilder)
511
        );
512
        return $firstResult ?: null;
513
    }
514

515
    /**
516
     * @codeCoverageIgnore
517
     */
518
    protected function getSingleRecordWithoutRestrictions(string $table, int $uid, string $fieldsToSelect): ?array
519
    {
520
        $queryBuilder = $this->createQueryBuilderForTable($table);
521
        $queryBuilder->getRestrictions()->removeAll();
522
        $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
523
            ->from($table)
524
            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)));
525
        /** @var array|false $firstResult */
526
        $firstResult = DoctrineQueryProxy::fetchAssociative(
527
            DoctrineQueryProxy::executeQueryOnQueryBuilder($queryBuilder)
528
        );
529
        return $firstResult ?: null;
530
    }
531

532
    /**
533
     * @codeCoverageIgnore
534
     */
535
    protected function getMostRecentCopyOfRecord(int $uid, string $fieldsToSelect = 'uid'): ?array
536
    {
537
        $queryBuilder = $this->createQueryBuilderForTable('tt_content');
538
        $queryBuilder->getRestrictions()->removeAll();
539
        $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
540
            ->from('tt_content')
541
            ->orderBy('uid', 'DESC')
542
            ->setMaxResults(1)
543
            ->where(
544
                $queryBuilder->expr()->eq('t3_origuid', $uid),
545
                $queryBuilder->expr()->neq('t3ver_state', -1)
546
            );
547
        /** @var array|false $firstResult */
548
        $firstResult = DoctrineQueryProxy::fetchAssociative(
549
            DoctrineQueryProxy::executeQueryOnQueryBuilder($queryBuilder)
550
        );
551
        return $firstResult ?: null;
552
    }
553

554
    /**
555
     * @codeCoverageIgnore
556
     */
557
    protected function getTranslatedVersionOfParentInLanguageOnPage(
558
        int $languageUid,
559
        int $pageUid,
560
        int $originalParentUid,
561
        string $fieldsToSelect = '*'
562
    ): ?array {
563
        /** @var DeletedRestriction $deletedRestriction */
564
        $deletedRestriction = GeneralUtility::makeInstance(DeletedRestriction::class);
565
        $queryBuilder = $this->createQueryBuilderForTable('tt_content');
566
        $queryBuilder->getRestrictions()->removeAll()->add($deletedRestriction);
567
        $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
568
            ->from('tt_content')
569
            ->setMaxResults(1)
570
            ->orderBy('uid', 'DESC')
571
            ->where(
572
                $queryBuilder->expr()->eq(
573
                    'sys_language_uid',
574
                    $queryBuilder->createNamedParameter($languageUid, Connection::PARAM_INT)
575
                ),
576
                $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pageUid, Connection::PARAM_INT)),
577
                $queryBuilder->expr()->eq(
578
                    'l10n_source',
579
                    $queryBuilder->createNamedParameter($originalParentUid, Connection::PARAM_INT)
580
                )
581
            );
582
        /** @var array|false $firstResult */
583
        $firstResult = DoctrineQueryProxy::fetchAssociative(
584
            DoctrineQueryProxy::executeQueryOnQueryBuilder($queryBuilder)
585
        );
586
        return $firstResult ?: null;
587
    }
588

589
    protected function getParentAndRecordsNestedInGrid(
590
        string $table,
591
        int $parentUid,
592
        string $fieldsToSelect,
593
        bool $respectPid = false,
594
        ?string $command = null
595
    ):array {
596
        // A Provider must be resolved which implements the GridProviderInterface
597
        $resolver = $this->getProviderResolver();
×
598
        $originalRecord = (array) $this->getSingleRecordWithoutRestrictions($table, $parentUid, '*');
×
599

600
        $primaryProvider = $resolver->resolvePrimaryConfigurationProvider(
×
601
            $table,
×
602
            null,
×
603
            $originalRecord,
×
604
            null,
×
605
            [GridProviderInterface::class]
×
606
        );
×
607

608
        if (!$primaryProvider) {
×
609
            return [
×
610
                $originalRecord,
×
611
                [],
×
612
                [],
×
613
            ];
×
614
        }
615

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

619
        if (empty($childColPosValues)) {
×
620
            return [
×
621
                $originalRecord,
×
622
                [],
×
623
                [],
×
624
            ];
×
625
        }
626

627
        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
×
628

629
        $queryBuilder = $this->createQueryBuilderForTable($table);
×
630
        if ($command === 'undelete') {
×
631
            $queryBuilder->getRestrictions()->removeAll();
×
632
        } else {
633
            $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
×
634
        }
635

636
        $query = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldsToSelect))
×
637
            ->from($table)
×
638
            ->andWhere(
×
639
                $queryBuilder->expr()->in(
×
640
                    'colPos',
×
641
                    $queryBuilder->createNamedParameter($childColPosValues, Connection::PARAM_INT_ARRAY)
×
642
                ),
×
643
                $queryBuilder->expr()->eq($languageField, (int)$originalRecord[$languageField]),
×
644
                $queryBuilder->expr()->in(
×
645
                    't3ver_wsid',
×
646
                    $queryBuilder->createNamedParameter(
×
647
                        [0, $GLOBALS['BE_USER']->workspace],
×
648
                        Connection::PARAM_INT_ARRAY
×
649
                    )
×
650
                )
×
651
            )->orderBy('sorting', 'DESC');
×
652

653
        if ($respectPid) {
×
654
            $query->andWhere($queryBuilder->expr()->eq('pid', $originalRecord['pid']));
×
655
        } else {
656
            $query->andWhere($queryBuilder->expr()->neq('pid', -1));
×
657
        }
658

659
        $records = DoctrineQueryProxy::fetchAllAssociative(DoctrineQueryProxy::executeQueryOnQueryBuilder($query));
×
660

661
        // Selecting records to return. The "sorting DESC" is very intentional; copy operations will place records
662
        // into the top of columns which means reading records in reverse order causes the correct final order.
663
        return [
×
664
            $originalRecord,
×
665
            $records,
×
666
            $childColPosValues
×
667
        ];
×
668
    }
669

670
    /**
671
     * @codeCoverageIgnore
672
     */
673
    protected function getProviderResolver(): ProviderResolver
674
    {
675
        /** @var ProviderResolver $providerResolver */
676
        $providerResolver = GeneralUtility::makeInstance(ProviderResolver::class);
677
        return $providerResolver;
678
    }
679

680
    /**
681
     * @codeCoverageIgnore
682
     */
683
    protected function createQueryBuilderForTable(string $table): QueryBuilder
684
    {
685
        /** @var ConnectionPool $connectionPool */
686
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
687
        return $connectionPool->getQueryBuilderForTable($table);
688
    }
689

690
    /**
691
     * @codeCoverageIgnore
692
     */
693
    protected function regenerateContentTypes(): void
694
    {
695
        /** @var ContentTypeManager $contentTypeManager */
696
        $contentTypeManager = GeneralUtility::makeInstance(ContentTypeManager::class);
697
        $contentTypeManager->regenerate();
698
    }
699
}
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