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

FluidTYPO3 / flux / 12237686280

09 Dec 2024 02:27PM UTC coverage: 92.9% (-0.5%) from 93.421%
12237686280

push

github

NamelessCoder
[TER] 10.1.0

7013 of 7549 relevant lines covered (92.9%)

56.22 hits per line

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

48.35
/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') {
18✔
38
            $this->regenerateContentTypes();
12✔
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) {
×
80
                $provider->postProcessRecord($command, (integer) $id, $record, $reference, []);
×
81
            }
82
        }
83

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

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

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

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

169
        static::$copiedRecords[$fieldArray['t3_origuid']] = true;
×
170
    }
171

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

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

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

237
        if ($table !== 'tt_content' || !is_integer($id)) {
18✔
238
            return;
12✔
239
        }
240

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

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

275
        if (empty($childRecords)) {
30✔
276
            return;
30✔
277
        }
278

279
        foreach ($childRecords as $childRecord) {
30✔
280
            $childRecordUid = $childRecord['uid'];
30✔
281
            $dataHandler->cmdmap[$table][$childRecordUid][$command] = $value;
30✔
282
            $this->cascadeCommandToChildRecords($table, $childRecordUid, $command, $value, $dataHandler);
30✔
283
        }
284
    }
285

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

299
            if ($table !== 'tt_content') {
48✔
300
                continue;
6✔
301
            }
302

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

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

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

385
                $oldParentUid = ColumnNumberUtility::calculateParentUid($copiedRecord['colPos']);
386
                $newParentUid = $reference->copyMappingArray['tt_content'][$oldParentUid];
387

388
                $overrideArray['colPos'] = ColumnNumberUtility::calculateColumnNumberForParentAndColumn(
389
                    $newParentUid,
390
                    ColumnNumberUtility::calculateLocalColumnNumber((int) $copiedRecord['colPos'])
391
                );
392

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

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

409
        if ($table !== 'tt_content' || $command !== 'move') {
24✔
410
            return;
6✔
411
        }
412

413
        [$originalRecord, $recordsToProcess] = $this->getParentAndRecordsNestedInGrid(
18✔
414
            $table,
18✔
415
            (integer) $id,
18✔
416
            'uid, pid, colPos',
18✔
417
            false,
18✔
418
            $command
18✔
419
        );
18✔
420

421
        if (empty($recordsToProcess)) {
18✔
422
            return;
6✔
423
        }
424

425
        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
12✔
426
        $languageUid = (int)($reference->cmdmap[$table][$id][$command]['update'][$languageField]
12✔
427
            ?? $originalRecord[$languageField]);
12✔
428

429
        if ($relativeTo > 0) {
12✔
430
            $destinationPid = $relativeTo;
6✔
431
        } else {
432
            $relativeRecord = $this->getSingleRecordWithoutRestrictions(
6✔
433
                $table,
6✔
434
                (integer) abs((integer) $relativeTo),
6✔
435
                'pid'
6✔
436
            );
6✔
437
            $destinationPid = $relativeRecord['pid'] ?? $relativeTo;
6✔
438
        }
439

440
        $this->recursivelyMoveChildRecords(
12✔
441
            $table,
12✔
442
            $recordsToProcess,
12✔
443
            (integer) $destinationPid,
12✔
444
            $languageUid,
12✔
445
            $reference
12✔
446
        );
12✔
447
    }
448

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

463
    protected function recursivelyMoveChildRecords(
464
        string $table,
465
        array $recordsToProcess,
466
        int $pageUid,
467
        int $languageUid,
468
        DataHandler $dataHandler
469
    ): void {
470
        $subCommandMap = [];
12✔
471

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

483
        if (!empty($subCommandMap)) {
12✔
484
            /** @var DataHandler $dataHandler */
485
            $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
12✔
486
            $dataHandler->copyMappingArray = $dataHandler->copyMappingArray;
12✔
487
            $dataHandler->start([], $subCommandMap);
12✔
488
            $dataHandler->process_cmdmap();
12✔
489
        }
490
    }
491

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

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

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

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

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

596
        $primaryProvider = $resolver->resolvePrimaryConfigurationProvider(
×
597
            $table,
×
598
            null,
×
599
            $originalRecord,
×
600
            null,
×
601
            [GridProviderInterface::class]
×
602
        );
×
603

604
        if (!$primaryProvider) {
×
605
            return [
×
606
                $originalRecord,
×
607
                [],
×
608
                [],
×
609
            ];
×
610
        }
611

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

615
        if (empty($childColPosValues)) {
×
616
            return [
×
617
                $originalRecord,
×
618
                [],
×
619
                [],
×
620
            ];
×
621
        }
622

623
        $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
×
624

625
        $queryBuilder = $this->createQueryBuilderForTable($table);
×
626
        if ($command === 'undelete') {
×
627
            $queryBuilder->getRestrictions()->removeAll();
×
628
        } else {
629
            $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
×
630
        }
631

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

649
        if ($respectPid) {
×
650
            $query->andWhere($queryBuilder->expr()->eq('pid', $originalRecord['pid']));
×
651
        } else {
652
            $query->andWhere($queryBuilder->expr()->neq('pid', -1));
×
653
        }
654

655
        $records = DoctrineQueryProxy::fetchAllAssociative(DoctrineQueryProxy::executeQueryOnQueryBuilder($query));
×
656

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

666
    /**
667
     * @codeCoverageIgnore
668
     */
669
    protected function getProviderResolver(): ProviderResolver
670
    {
671
        /** @var ProviderResolver $providerResolver */
672
        $providerResolver = GeneralUtility::makeInstance(ProviderResolver::class);
673
        return $providerResolver;
674
    }
675

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

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