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