Passed
Push — master ( e47dd8...3ef27a )
by
unknown
13:30
created

Typo3DbBackend::getRowByIdentifier()   A

Complexity

Conditions 4
Paths 13

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 13
nop 2
dl 0
loc 21
rs 9.8333
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Extbase\Persistence\Generic\Storage;
19
20
use Doctrine\DBAL\Exception as DBALException;
21
use Doctrine\DBAL\Platforms\SQLServer2012Platform as SQLServerPlatform;
22
use TYPO3\CMS\Backend\Utility\BackendUtility;
23
use TYPO3\CMS\Core\Context\Context;
24
use TYPO3\CMS\Core\Context\WorkspaceAspect;
25
use TYPO3\CMS\Core\Database\ConnectionPool;
26
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
27
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
28
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
29
use TYPO3\CMS\Core\SingletonInterface;
30
use TYPO3\CMS\Core\Utility\GeneralUtility;
31
use TYPO3\CMS\Core\Utility\MathUtility;
32
use TYPO3\CMS\Core\Versioning\VersionState;
33
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
34
use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
35
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
36
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
37
use TYPO3\CMS\Extbase\Persistence\Generic\Qom;
38
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\JoinInterface;
39
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\SelectorInterface;
40
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface;
41
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\Statement;
42
use TYPO3\CMS\Extbase\Persistence\Generic\Query;
43
use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\BadConstraintException;
44
use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\SqlErrorException;
45
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
46
use TYPO3\CMS\Extbase\Service\CacheService;
47
use TYPO3\CMS\Extbase\Service\EnvironmentService;
48
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
49
50
/**
51
 * A Storage backend
52
 * @internal only to be used within Extbase, not part of TYPO3 Core API.
53
 */
54
class Typo3DbBackend implements BackendInterface, SingletonInterface
55
{
56
    /**
57
     * @var ConnectionPool
58
     */
59
    protected $connectionPool;
60
61
    /**
62
     * @var ConfigurationManagerInterface
63
     */
64
    protected $configurationManager;
65
66
    /**
67
     * @var CacheService
68
     */
69
    protected $cacheService;
70
71
    /**
72
     * @var EnvironmentService
73
     */
74
    protected $environmentService;
75
76
    /**
77
     * @var ObjectManagerInterface
78
     */
79
    protected $objectManager;
80
81
    /**
82
     * As determining the table columns is a costly operation this is done only once per table during runtime and cached then
83
     *
84
     * @var array
85
     * @see clearPageCache()
86
     */
87
    protected $hasPidColumn = [];
88
89
    /**
90
     * @param ConfigurationManagerInterface $configurationManager
91
     */
92
    public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void
93
    {
94
        $this->configurationManager = $configurationManager;
95
    }
96
97
    /**
98
     * @param CacheService $cacheService
99
     */
100
    public function injectCacheService(CacheService $cacheService): void
101
    {
102
        $this->cacheService = $cacheService;
103
    }
104
105
    /**
106
     * @param EnvironmentService $environmentService
107
     */
108
    public function injectEnvironmentService(EnvironmentService $environmentService): void
109
    {
110
        $this->environmentService = $environmentService;
111
    }
112
113
    /**
114
     * @param ObjectManagerInterface $objectManager
115
     */
116
    public function injectObjectManager(ObjectManagerInterface $objectManager): void
117
    {
118
        $this->objectManager = $objectManager;
119
    }
120
121
    /**
122
     * Constructor.
123
     */
124
    public function __construct()
125
    {
126
        $this->connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
127
    }
128
129
    /**
130
     * Adds a row to the storage
131
     *
132
     * @param string $tableName The database table name
133
     * @param array $fieldValues The row to be inserted
134
     * @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
135
     * @return int The uid of the inserted row
136
     * @throws SqlErrorException
137
     */
138
    public function addRow(string $tableName, array $fieldValues, bool $isRelation = false): int
139
    {
140
        if (isset($fieldValues['uid'])) {
141
            unset($fieldValues['uid']);
142
        }
143
        try {
144
            $connection = $this->connectionPool->getConnectionForTable($tableName);
145
146
            $types = [];
147
            $platform = $connection->getDatabasePlatform();
148
            if ($platform instanceof SQLServerPlatform) {
149
                // mssql needs to set proper PARAM_LOB and others to update fields
150
                $tableDetails = $connection->getSchemaManager()->listTableDetails($tableName);
151
                foreach ($fieldValues as $columnName => $columnValue) {
152
                    $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
153
                }
154
            }
155
156
            $connection->insert($tableName, $fieldValues, $types);
157
        } catch (DBALException $e) {
158
            throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230766, $e);
159
        }
160
161
        $uid = 0;
162
        if (!$isRelation) {
163
            // Relation tables have no auto_increment column, so no retrieval must be tried.
164
            $uid = (int)$connection->lastInsertId($tableName);
165
            $this->clearPageCache($tableName, $uid);
166
        }
167
        return $uid;
168
    }
169
170
    /**
171
     * Updates a row in the storage
172
     *
173
     * @param string $tableName The database table name
174
     * @param array $fieldValues The row to be updated
175
     * @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default
176
     * @throws \InvalidArgumentException
177
     * @throws SqlErrorException
178
     */
179
    public function updateRow(string $tableName, array $fieldValues, bool $isRelation = false): void
180
    {
181
        if (!isset($fieldValues['uid'])) {
182
            throw new \InvalidArgumentException('The given row must contain a value for "uid".', 1476045164);
183
        }
184
185
        $uid = (int)$fieldValues['uid'];
186
        unset($fieldValues['uid']);
187
188
        try {
189
            $connection = $this->connectionPool->getConnectionForTable($tableName);
190
191
            $types = [];
192
            $platform = $connection->getDatabasePlatform();
193
            if ($platform instanceof SQLServerPlatform) {
194
                // mssql needs to set proper PARAM_LOB and others to update fields
195
                $tableDetails = $connection->getSchemaManager()->listTableDetails($tableName);
196
                foreach ($fieldValues as $columnName => $columnValue) {
197
                    $columnName = (string)$columnName;
198
                    $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
199
                }
200
            }
201
202
            $connection->update($tableName, $fieldValues, ['uid' => $uid], $types);
203
        } catch (DBALException $e) {
204
            throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230767, $e);
205
        }
206
207
        if (!$isRelation) {
208
            $this->clearPageCache($tableName, $uid);
209
        }
210
    }
211
212
    /**
213
     * Updates a relation row in the storage.
214
     *
215
     * @param string $tableName The database relation table name
216
     * @param array $fieldValues The row to be updated
217
     * @throws SqlErrorException
218
     * @throws \InvalidArgumentException
219
     */
220
    public function updateRelationTableRow(string $tableName, array $fieldValues): void
221
    {
222
        if (!isset($fieldValues['uid_local']) && !isset($fieldValues['uid_foreign'])) {
223
            throw new \InvalidArgumentException(
224
                'The given fieldValues must contain a value for "uid_local" and "uid_foreign".',
225
                1360500126
226
            );
227
        }
228
229
        $where = [];
230
        $where['uid_local'] = (int)$fieldValues['uid_local'];
231
        $where['uid_foreign'] = (int)$fieldValues['uid_foreign'];
232
        unset($fieldValues['uid_local']);
233
        unset($fieldValues['uid_foreign']);
234
235
        if (!empty($fieldValues['tablenames'])) {
236
            $where['tablenames'] = $fieldValues['tablenames'];
237
            unset($fieldValues['tablenames']);
238
        }
239
        if (!empty($fieldValues['fieldname'])) {
240
            $where['fieldname'] = $fieldValues['fieldname'];
241
            unset($fieldValues['fieldname']);
242
        }
243
244
        try {
245
            $this->connectionPool->getConnectionForTable($tableName)->update($tableName, $fieldValues, $where);
246
        } catch (DBALException $e) {
247
            throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230768, $e);
248
        }
249
    }
250
251
    /**
252
     * Deletes a row in the storage
253
     *
254
     * @param string $tableName The database table name
255
     * @param array $where An array of where array('fieldname' => value).
256
     * @param bool $isRelation TRUE if we are currently manipulating a relation table, FALSE by default
257
     * @throws SqlErrorException
258
     */
259
    public function removeRow(string $tableName, array $where, bool $isRelation = false): void
260
    {
261
        try {
262
            $this->connectionPool->getConnectionForTable($tableName)->delete($tableName, $where);
263
        } catch (DBALException $e) {
264
            throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230769, $e);
265
        }
266
267
        if (!$isRelation && isset($where['uid'])) {
268
            $this->clearPageCache($tableName, (int)$where['uid']);
269
        }
270
    }
271
272
    /**
273
     * Returns the object data matching the $query.
274
     *
275
     * @param QueryInterface $query
276
     * @return array
277
     * @throws SqlErrorException
278
     */
279
    public function getObjectDataByQuery(QueryInterface $query): array
280
    {
281
        $statement = $query->getStatement();
282
        // todo: remove instanceof checks as soon as getStatement() strictly returns Qom\Statement only
283
        if ($statement instanceof Statement
284
            && !$statement->getStatement() instanceof QueryBuilder
285
        ) {
286
            $rows = $this->getObjectDataByRawQuery($statement);
287
        } else {
288
            /** @var Typo3DbQueryParser $queryParser */
289
            $queryParser = $this->objectManager->get(Typo3DbQueryParser::class);
290
            if ($statement instanceof Statement
291
                && $statement->getStatement() instanceof QueryBuilder
292
            ) {
293
                $queryBuilder = $statement->getStatement();
294
            } else {
295
                $queryBuilder = $queryParser->convertQueryToDoctrineQueryBuilder($query);
296
            }
297
            $selectParts = $queryBuilder->getQueryPart('select');
298
            if ($queryParser->isDistinctQuerySuggested() && !empty($selectParts)) {
299
                $selectParts[0] = 'DISTINCT ' . $selectParts[0];
300
                $queryBuilder->selectLiteral(...$selectParts);
301
            }
302
            if ($query->getOffset()) {
303
                $queryBuilder->setFirstResult($query->getOffset());
304
            }
305
            if ($query->getLimit()) {
306
                $queryBuilder->setMaxResults($query->getLimit());
307
            }
308
            try {
309
                $rows = $queryBuilder->execute()->fetchAll();
310
            } catch (DBALException $e) {
311
                throw new SqlErrorException($e->getPrevious()->getMessage(), 1472074485, $e);
312
            }
313
        }
314
315
        if (!empty($rows)) {
316
            $rows = $this->overlayLanguageAndWorkspace($query->getSource(), $rows, $query);
317
        }
318
319
        return $rows;
320
    }
321
322
    /**
323
     * Returns the object data using a custom statement
324
     *
325
     * @param Qom\Statement $statement
326
     * @return array
327
     * @throws SqlErrorException when the raw SQL statement fails in the database
328
     */
329
    protected function getObjectDataByRawQuery(Statement $statement): array
330
    {
331
        $realStatement = $statement->getStatement();
332
        $parameters = $statement->getBoundVariables();
333
334
        // The real statement is an instance of the Doctrine DBAL QueryBuilder, so fetching
335
        // this directly is possible
336
        if ($realStatement instanceof QueryBuilder) {
337
            try {
338
                $result = $realStatement->execute();
339
            } catch (DBALException $e) {
340
                throw new SqlErrorException($e->getPrevious()->getMessage(), 1472064721, $e);
341
            }
342
            $rows = $result->fetchAll();
343
        } elseif ($realStatement instanceof \Doctrine\DBAL\Statement) {
344
            try {
345
                $realStatement->execute($parameters);
346
            } catch (DBALException $e) {
347
                throw new SqlErrorException($e->getPrevious()->getMessage(), 1481281404, $e);
348
            }
349
            $rows = $realStatement->fetchAll();
350
        } else {
351
            // Do a real raw query. This is very stupid, as it does not allow to use DBAL's real power if
352
            // several tables are on different databases, so this is used with caution and could be removed
353
            // in the future
354
            try {
355
                $connection = $this->connectionPool->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
356
                $statement = $connection->executeQuery($realStatement, $parameters);
357
            } catch (DBALException $e) {
358
                throw new SqlErrorException($e->getPrevious()->getMessage(), 1472064775, $e);
359
            }
360
361
            $rows = $statement->fetchAll();
362
        }
363
364
        return $rows;
365
    }
366
367
    /**
368
     * Returns the number of tuples matching the query.
369
     *
370
     * @param QueryInterface $query
371
     * @return int The number of matching tuples
372
     * @throws BadConstraintException
373
     * @throws SqlErrorException
374
     */
375
    public function getObjectCountByQuery(QueryInterface $query): int
376
    {
377
        if ($query->getConstraint() instanceof Statement) {
378
            throw new BadConstraintException('Could not execute count on queries with a constraint of type TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Qom\\Statement', 1256661045);
379
        }
380
381
        $statement = $query->getStatement();
382
        if ($statement instanceof Statement
383
            && !$statement->getStatement() instanceof QueryBuilder
384
        ) {
385
            $rows = $this->getObjectDataByQuery($query);
386
            $count = count($rows);
387
        } else {
388
            /** @var Typo3DbQueryParser $queryParser */
389
            $queryParser  = $this->objectManager->get(Typo3DbQueryParser::class);
390
            $queryBuilder = $queryParser
391
                ->convertQueryToDoctrineQueryBuilder($query)
392
                ->resetQueryPart('orderBy');
393
394
            if ($queryParser->isDistinctQuerySuggested()) {
395
                $source = $queryBuilder->getQueryPart('from')[0];
396
                // Tablename is already quoted for the DBMS, we need to treat table and field names separately
397
                $tableName = $source['alias'] ?: $source['table'];
398
                $fieldName = $queryBuilder->quoteIdentifier('uid');
399
                $queryBuilder->resetQueryPart('groupBy')
400
                    ->selectLiteral(sprintf('COUNT(DISTINCT %s.%s)', $tableName, $fieldName));
401
            } else {
402
                $queryBuilder->count('*');
403
            }
404
405
            try {
406
                $count = $queryBuilder->execute()->fetchColumn(0);
407
            } catch (DBALException $e) {
408
                throw new SqlErrorException($e->getPrevious()->getMessage(), 1472074379, $e);
409
            }
410
            if ($query->getOffset()) {
411
                $count -= $query->getOffset();
412
            }
413
            if ($query->getLimit()) {
414
                $count = min($count, $query->getLimit());
415
            }
416
        }
417
        return (int)max(0, $count);
418
    }
419
420
    /**
421
     * Checks if a Value Object equal to the given Object exists in the database
422
     *
423
     * @param AbstractValueObject $object The Value Object
424
     * @return int|null The matching uid if an object was found, else FALSE
425
     * @throws SqlErrorException
426
     */
427
    public function getUidOfAlreadyPersistedValueObject(AbstractValueObject $object): ?int
428
    {
429
        /** @var DataMapper $dataMapper */
430
        $dataMapper = $this->objectManager->get(DataMapper::class);
431
        $dataMap = $dataMapper->getDataMap(get_class($object));
432
        $tableName = $dataMap->getTableName();
433
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
434
        if ($this->environmentService->isEnvironmentInFrontendMode()) {
435
            $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
436
        }
437
        $whereClause = [];
438
        // loop over all properties of the object to exactly set the values of each database field
439
        $properties = $object->_getProperties();
440
        foreach ($properties as $propertyName => $propertyValue) {
441
            $propertyName = (string)$propertyName;
442
443
            // @todo We couple the Backend to the Entity implementation (uid, isClone); changes there breaks this method
444
            if ($dataMap->isPersistableProperty($propertyName) && $propertyName !== 'uid' && $propertyName !== 'pid' && $propertyName !== 'isClone') {
445
                $fieldName = $dataMap->getColumnMap($propertyName)->getColumnName();
446
                if ($propertyValue === null) {
447
                    $whereClause[] = $queryBuilder->expr()->isNull($fieldName);
448
                } else {
449
                    $whereClause[] = $queryBuilder->expr()->eq($fieldName, $queryBuilder->createNamedParameter($dataMapper->getPlainValue($propertyValue)));
450
                }
451
            }
452
        }
453
        $queryBuilder
454
            ->select('uid')
455
            ->from($tableName)
456
            ->where(...$whereClause);
457
458
        try {
459
            $uid = (int)$queryBuilder
460
                ->execute()
461
                ->fetchColumn(0);
462
            if ($uid > 0) {
463
                return $uid;
464
            }
465
            return null;
466
        } catch (DBALException $e) {
467
            throw new SqlErrorException($e->getPrevious()->getMessage(), 1470231748, $e);
468
        }
469
    }
470
471
    /**
472
     * Performs workspace and language overlay on the given row array. The language and workspace id is automatically
473
     * detected (depending on FE or BE context). You can also explicitly set the language/workspace id.
474
     *
475
     * @param Qom\SourceInterface $source The source (selector or join)
476
     * @param array $rows
477
     * @param QueryInterface $query
478
     * @param int|null $workspaceUid
479
     * @return array
480
     * @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
481
     */
482
    protected function overlayLanguageAndWorkspace(SourceInterface $source, array $rows, QueryInterface $query, int $workspaceUid = null): array
483
    {
484
        if ($source instanceof SelectorInterface) {
485
            $tableName = $source->getSelectorName();
486
        } elseif ($source instanceof JoinInterface) {
487
            $tableName = $source->getRight()->getSelectorName();
488
        } else {
489
            // No proper source, so we do not have a table name here
490
            // we cannot do an overlay and return the original rows instead.
491
            return $rows;
492
        }
493
494
        $context = GeneralUtility::makeInstance(Context::class);
495
        if ($workspaceUid === null) {
496
            $workspaceUid = (int)$context->getPropertyFromAspect('workspace', 'id');
497
        } else {
498
            // A custom query is needed, so a custom context is cloned
499
            $context = clone $context;
500
            $context->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $workspaceUid));
501
        }
502
        $pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context);
503
504
        // Fetches the moved record in case it is supported
505
        // by the table and if there's only one row in the result set
506
        // (applying this to all rows does not work, since the sorting
507
        // order would be destroyed and possible limits not met anymore)
508
        // The move pointers are later unset (see versionOL() last argument)
509
        if (!empty($workspaceUid)
510
            && BackendUtility::isTableWorkspaceEnabled($tableName)
511
            && count($rows) === 1
512
        ) {
513
            $versionId = $workspaceUid;
514
            $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
515
            $queryBuilder->getRestrictions()->removeAll();
516
            $movedRecords = $queryBuilder
517
                ->select('*')
518
                ->from($tableName)
519
                ->where(
520
                    $queryBuilder->expr()->eq('t3ver_state', $queryBuilder->createNamedParameter(VersionState::MOVE_POINTER, \PDO::PARAM_INT)),
521
                    $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter($versionId, \PDO::PARAM_INT)),
522
                    $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($rows[0]['uid'], \PDO::PARAM_INT))
523
                )
524
                ->setMaxResults(1)
525
                ->execute()
526
                ->fetchAll();
527
            if (!empty($movedRecords)) {
528
                $rows = $movedRecords;
529
            }
530
        }
531
        $overlaidRows = [];
532
        $querySettings = $query->getQuerySettings();
533
        foreach ($rows as $row) {
534
            // If current row is a translation select its parent
535
            $languageOfCurrentRecord = 0;
536
            if ($GLOBALS['TCA'][$tableName]['ctrl']['languageField'] ?? null
537
            && $row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']] ?? 0) {
538
                $languageOfCurrentRecord = $row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']];
539
            }
540
            if ($querySettings->getLanguageOverlayMode()
541
                && $languageOfCurrentRecord > 0
542
                && isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
543
                && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0) {
544
                $row = $pageRepository->getRawRecord(
545
                    $tableName,
546
                    (int)$row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']]
547
                );
548
            }
549
            // Handle workspace overlays
550
            $pageRepository->versionOL($tableName, $row, true);
551
            if (is_array($row) && $querySettings->getLanguageOverlayMode()) {
552
                if ($tableName === 'pages') {
553
                    $row = $pageRepository->getPageOverlay($row, $querySettings->getLanguageUid());
554
                } else {
555
                    // todo: remove type cast once getLanguageUid strictly returns an int
556
                    $languageUid = (int)$querySettings->getLanguageUid();
557
                    if (!$querySettings->getRespectSysLanguage()
558
                        && $languageOfCurrentRecord > 0
559
                        && (!$query instanceof Query || !$query->getParentQuery())
560
                    ) {
561
                        //no parent query means we're processing the aggregate root.
562
                        //respectSysLanguage is false which means that records returned by the query
563
                        //might be from different languages (which is desired).
564
                        //So we need to force language used for overlay to the language of the current record.
565
                        $languageUid = $languageOfCurrentRecord;
566
                    }
567
                    if (isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
568
                        && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0
569
                        && $languageOfCurrentRecord > 0) {
570
                        //force overlay by faking default language record, as getRecordOverlay can only handle default language records
571
                        $row['uid'] = $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']];
572
                        $row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']] = 0;
573
                    }
574
                    $row = $pageRepository->getRecordOverlay($tableName, $row, $languageUid, (string)$querySettings->getLanguageOverlayMode());
575
                }
576
            }
577
            if ($row !== null && is_array($row)) {
578
                $overlaidRows[] = $row;
579
            }
580
        }
581
        return $overlaidRows;
582
    }
583
584
    /**
585
     * Clear the TYPO3 page cache for the given record.
586
     * If the record lies on a page, then we clear the cache of this page.
587
     * If the record has no PID column, we clear the cache of the current page as best-effort.
588
     *
589
     * Much of this functionality is taken from DataHandler::clear_cache() which unfortunately only works with logged-in BE user.
590
     *
591
     * @param string $tableName Table name of the record
592
     * @param int $uid UID of the record
593
     */
594
    protected function clearPageCache(string $tableName, int $uid): void
595
    {
596
        $frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
597
        if (empty($frameworkConfiguration['persistence']['enableAutomaticCacheClearing'])) {
598
            return;
599
        }
600
        $pageIdsToClear = [];
601
        $storagePage = null;
602
603
        // As determining the table columns is a costly operation this is done only once per table during runtime and cached then
604
        if (!isset($this->hasPidColumn[$tableName])) {
605
            $columns = $this->connectionPool
606
                ->getConnectionForTable($tableName)
607
                ->getSchemaManager()
608
                ->listTableColumns($tableName);
609
            $this->hasPidColumn[$tableName] = array_key_exists('pid', $columns);
610
        }
611
612
        $tsfe = $this->getTSFE();
613
        if ($this->hasPidColumn[$tableName]) {
614
            $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
615
            $queryBuilder->getRestrictions()->removeAll();
616
            $result = $queryBuilder
617
                ->select('pid')
618
                ->from($tableName)
619
                ->where(
620
                    $queryBuilder->expr()->eq(
621
                        'uid',
622
                        $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
623
                    )
624
                )
625
                ->execute();
626
            if ($row = $result->fetch()) {
627
                $storagePage = $row['pid'];
628
                $pageIdsToClear[] = $storagePage;
629
            }
630
        } elseif (isset($tsfe)) {
631
            // No PID column - we can do a best-effort to clear the cache of the current page if in FE
632
            $storagePage = $tsfe->id;
633
            $pageIdsToClear[] = $storagePage;
634
        }
635
        if ($storagePage === null) {
636
            return;
637
        }
638
639
        $pageTS = BackendUtility::getPagesTSconfig($storagePage);
640
        if (isset($pageTS['TCEMAIN.']['clearCacheCmd'])) {
641
            $clearCacheCommands = GeneralUtility::trimExplode(',', strtolower($pageTS['TCEMAIN.']['clearCacheCmd']), true);
642
            $clearCacheCommands = array_unique($clearCacheCommands);
643
            foreach ($clearCacheCommands as $clearCacheCommand) {
644
                if (MathUtility::canBeInterpretedAsInteger($clearCacheCommand)) {
645
                    $pageIdsToClear[] = $clearCacheCommand;
646
                }
647
            }
648
        }
649
650
        foreach ($pageIdsToClear as $pageIdToClear) {
651
            $this->cacheService->getPageIdStack()->push($pageIdToClear);
652
        }
653
    }
654
655
    /**
656
     * @return TypoScriptFrontendController|null
657
     */
658
    protected function getTSFE(): ?TypoScriptFrontendController
659
    {
660
        return $GLOBALS['TSFE'] ?? null;
661
    }
662
}
663