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