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