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