[TASK] Update php-cs-fixer to 2.5.0
[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.
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 $rows = $this->getObjectDataByRawQuery($statement);
358 } else {
359 $queryBuilder = $this->objectManager->get(Typo3DbQueryParser::class)
360 ->convertQueryToDoctrineQueryBuilder($query);
361 if ($query->getOffset()) {
362 $queryBuilder->setFirstResult($query->getOffset());
363 }
364 if ($query->getLimit()) {
365 $queryBuilder->setMaxResults($query->getLimit());
366 }
367 try {
368 $rows = $queryBuilder->execute()->fetchAll();
369 } catch (DBALException $e) {
370 throw new SqlErrorException($e->getPrevious()->getMessage(), 1472074485);
371 }
372 }
373
374 $rows = $this->doLanguageAndWorkspaceOverlay($query->getSource(), $rows, $query->getQuerySettings());
375 return $rows;
376 }
377
378 /**
379 * Returns the object data using a custom statement
380 *
381 * @param Qom\Statement $statement
382 * @return array
383 * @throws SqlErrorException when the raw SQL statement fails in the database
384 */
385 protected function getObjectDataByRawQuery(Qom\Statement $statement)
386 {
387 $realStatement = $statement->getStatement();
388 $parameters = $statement->getBoundVariables();
389
390 // The real statement is an instance of the Doctrine DBAL QueryBuilder, so fetching
391 // this directly is possible
392 if ($realStatement instanceof QueryBuilder) {
393 try {
394 $result = $realStatement->execute();
395 } catch (DBALException $e) {
396 throw new SqlErrorException($e->getPrevious()->getMessage(), 1472064721);
397 }
398 $rows = $result->fetchAll();
399 } elseif ($realStatement instanceof \Doctrine\DBAL\Statement) {
400 try {
401 $realStatement->execute($parameters);
402 } catch (DBALException $e) {
403 throw new SqlErrorException($e->getPrevious()->getMessage(), 1481281404);
404 }
405 $rows = $realStatement->fetchAll();
406 } else {
407 // Do a real raw query. This is very stupid, as it does not allow to use DBAL's real power if
408 // several tables are on different databases, so this is used with caution and could be removed
409 // in the future
410 try {
411 $connection = $this->connectionPool->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
412 $statement = $connection->executeQuery($realStatement, $parameters);
413 } catch (DBALException $e) {
414 throw new SqlErrorException($e->getPrevious()->getMessage(), 1472064775);
415 }
416
417 $rows = $statement->fetchAll();
418 }
419
420 return $rows;
421 }
422
423 /**
424 * Returns the number of tuples matching the query.
425 *
426 * @param QueryInterface $query
427 * @throws Exception\BadConstraintException
428 * @return int The number of matching tuples
429 * @throws SqlErrorException
430 */
431 public function getObjectCountByQuery(QueryInterface $query)
432 {
433 if ($query->getConstraint() instanceof Qom\Statement) {
434 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);
435 }
436
437 $queryBuilder = $this->objectManager->get(Typo3DbQueryParser::class)
438 ->convertQueryToDoctrineQueryBuilder($query)
439 ->resetQueryPart('orderBy');
440
441 if (count($queryBuilder->getQueryPart('groupBy')) !== 0) {
442 $source = $queryBuilder->getQueryPart('from')[0];
443 // Tablename is already quoted for the DBMS, we need to treat table and field names separately
444 $tableName = $source['alias'] ?: $source['table'];
445 $fieldName = $queryBuilder->quoteIdentifier('uid');
446 $queryBuilder->resetQueryPart('groupBy')
447 ->selectLiteral(sprintf('COUNT(DISTINCT %s.%s)', $tableName, $fieldName));
448 } else {
449 $queryBuilder->count('*');
450 }
451
452 try {
453 $count = $queryBuilder->execute()->fetchColumn(0);
454 } catch (DBALException $e) {
455 throw new SqlErrorException($e->getPrevious()->getMessage(), 1472074379);
456 }
457 if ($query->getOffset()) {
458 $count -= $query->getOffset();
459 }
460 if ($query->getLimit()) {
461 $count = min($count, $query->getLimit());
462 }
463 return (int)max(0, $count);
464 }
465
466 /**
467 * Checks if a Value Object equal to the given Object exists in the database
468 *
469 * @param AbstractValueObject $object The Value Object
470 * @return mixed The matching uid if an object was found, else FALSE
471 * @throws SqlErrorException
472 */
473 public function getUidOfAlreadyPersistedValueObject(AbstractValueObject $object)
474 {
475 $dataMap = $this->dataMapper->getDataMap(get_class($object));
476 $tableName = $dataMap->getTableName();
477 $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
478 if ($this->environmentService->isEnvironmentInFrontendMode()) {
479 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
480 }
481 $whereClause = [];
482 // loop over all properties of the object to exactly set the values of each database field
483 $properties = $object->_getProperties();
484 foreach ($properties as $propertyName => $propertyValue) {
485 // @todo We couple the Backend to the Entity implementation (uid, isClone); changes there breaks this method
486 if ($dataMap->isPersistableProperty($propertyName) && $propertyName !== 'uid' && $propertyName !== 'pid' && $propertyName !== 'isClone') {
487 $fieldName = $dataMap->getColumnMap($propertyName)->getColumnName();
488 if ($propertyValue === null) {
489 $whereClause[] = $queryBuilder->expr()->isNull($fieldName);
490 } else {
491 $whereClause[] = $queryBuilder->expr()->eq($fieldName, $queryBuilder->createNamedParameter($this->dataMapper->getPlainValue($propertyValue)));
492 }
493 }
494 }
495 $queryBuilder
496 ->select('uid')
497 ->from($tableName)
498 ->where(...$whereClause);
499
500 try {
501 $uid = (int)$queryBuilder
502 ->execute()
503 ->fetchColumn(0);
504 if ($uid > 0) {
505 return $uid;
506 }
507 return false;
508 } catch (DBALException $e) {
509 throw new SqlErrorException($e->getPrevious()->getMessage(), 1470231748);
510 }
511 }
512
513 /**
514 * Performs workspace and language overlay on the given row array. The language and workspace id is automatically
515 * detected (depending on FE or BE context). You can also explicitly set the language/workspace id.
516 *
517 * @param Qom\SourceInterface $source The source (selector od join)
518 * @param array $rows
519 * @param \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
520 * @param null|int $workspaceUid
521 * @return array
522 */
523 protected function doLanguageAndWorkspaceOverlay(Qom\SourceInterface $source, array $rows, \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings, $workspaceUid = null)
524 {
525 if ($source instanceof Qom\SelectorInterface) {
526 $tableName = $source->getSelectorName();
527 } elseif ($source instanceof Qom\JoinInterface) {
528 $tableName = $source->getRight()->getSelectorName();
529 } else {
530 // No proper source, so we do not have a table name here
531 // we cannot do an overlay and return the original rows instead.
532 return $rows;
533 }
534
535 $pageRepository = $this->getPageRepository();
536 if (is_object($GLOBALS['TSFE'])) {
537 if ($workspaceUid !== null) {
538 $pageRepository->versioningWorkspaceId = $workspaceUid;
539 }
540 } else {
541 if ($workspaceUid === null) {
542 $workspaceUid = $GLOBALS['BE_USER']->workspace;
543 }
544 $pageRepository->versioningWorkspaceId = $workspaceUid;
545 }
546
547 // Fetches the move-placeholder in case it is supported
548 // by the table and if there's only one row in the result set
549 // (applying this to all rows does not work, since the sorting
550 // order would be destroyed and possible limits not met anymore)
551 if (!empty($pageRepository->versioningWorkspaceId)
552 && BackendUtility::isTableWorkspaceEnabled($tableName)
553 && count($rows) === 1
554 ) {
555 $versionId = $pageRepository->versioningWorkspaceId;
556 $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
557 $queryBuilder->getRestrictions()->removeAll();
558 $movePlaceholder = $queryBuilder->select($tableName . '.*')
559 ->from($tableName)
560 ->where(
561 $queryBuilder->expr()->eq('t3ver_state', $queryBuilder->createNamedParameter(3, \PDO::PARAM_INT)),
562 $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter($versionId, \PDO::PARAM_INT)),
563 $queryBuilder->expr()->eq('t3ver_move_id', $queryBuilder->createNamedParameter($rows[0]['uid'], \PDO::PARAM_INT))
564 )
565 ->setMaxResults(1)
566 ->execute()
567 ->fetch();
568 if (!empty($movePlaceholder)) {
569 $rows = [$movePlaceholder];
570 }
571 }
572
573 $overlaidRows = [];
574 foreach ($rows as $row) {
575 // If current row is a translation select its parent
576 if (isset($tableName) && isset($GLOBALS['TCA'][$tableName])
577 && isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
578 && isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
579 && $tableName !== 'pages_language_overlay'
580 ) {
581 if (isset($row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']])
582 && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0
583 ) {
584 $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
585 $queryBuilder->getRestrictions()->removeAll();
586 $row = $queryBuilder->select($tableName . '.*')
587 ->from($tableName)
588 ->where(
589 $queryBuilder->expr()->eq(
590 $tableName . '.uid',
591 $queryBuilder->createNamedParameter(
592 $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']],
593 \PDO::PARAM_INT
594 )
595 ),
596 $queryBuilder->expr()->eq(
597 $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'],
598 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
599 )
600 )
601 ->setMaxResults(1)
602 ->execute()
603 ->fetch();
604 }
605 }
606 $pageRepository->versionOL($tableName, $row, true);
607 if ($tableName === 'pages') {
608 $row = $pageRepository->getPageOverlay($row, $querySettings->getLanguageUid());
609 } elseif (isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
610 && $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] !== ''
611 && $tableName !== 'pages_language_overlay'
612 ) {
613 if (in_array($row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']], [-1, 0])) {
614 $overlayMode = $querySettings->getLanguageMode() === 'strict' ? 'hideNonTranslated' : '';
615 $row = $pageRepository->getRecordOverlay($tableName, $row, $querySettings->getLanguageUid(), $overlayMode);
616 }
617 }
618 if ($row !== null && is_array($row)) {
619 $overlaidRows[] = $row;
620 }
621 }
622 return $overlaidRows;
623 }
624
625 /**
626 * @return \TYPO3\CMS\Frontend\Page\PageRepository
627 */
628 protected function getPageRepository()
629 {
630 if (!$this->pageRepository instanceof \TYPO3\CMS\Frontend\Page\PageRepository) {
631 if ($this->environmentService->isEnvironmentInFrontendMode() && is_object($GLOBALS['TSFE'])) {
632 $this->pageRepository = $GLOBALS['TSFE']->sys_page;
633 } else {
634 $this->pageRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\Page\PageRepository::class);
635 }
636 }
637
638 return $this->pageRepository;
639 }
640
641 /**
642 * Clear the TYPO3 page cache for the given record.
643 * If the record lies on a page, then we clear the cache of this page.
644 * If the record has no PID column, we clear the cache of the current page as best-effort.
645 *
646 * Much of this functionality is taken from DataHandler::clear_cache() which unfortunately only works with logged-in BE user.
647 *
648 * @param string $tableName Table name of the record
649 * @param int $uid UID of the record
650 */
651 protected function clearPageCache($tableName, $uid)
652 {
653 $frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
654 if (isset($frameworkConfiguration['persistence']['enableAutomaticCacheClearing']) && $frameworkConfiguration['persistence']['enableAutomaticCacheClearing'] === '1') {
655 } else {
656 // if disabled, return
657 return;
658 }
659 $pageIdsToClear = [];
660 $storagePage = null;
661 $columns = GeneralUtility::makeInstance(ConnectionPool::class)
662 ->getConnectionForTable($tableName)
663 ->getSchemaManager()
664 ->listTableColumns($tableName);
665 if (array_key_exists('pid', $columns)) {
666 $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
667 $queryBuilder->getRestrictions()->removeAll();
668 $result = $queryBuilder
669 ->select('pid')
670 ->from($tableName)
671 ->where(
672 $queryBuilder->expr()->eq(
673 'uid',
674 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
675 )
676 )
677 ->execute();
678 if ($row = $result->fetch()) {
679 $storagePage = $row['pid'];
680 $pageIdsToClear[] = $storagePage;
681 }
682 } elseif (isset($GLOBALS['TSFE'])) {
683 // No PID column - we can do a best-effort to clear the cache of the current page if in FE
684 $storagePage = $GLOBALS['TSFE']->id;
685 $pageIdsToClear[] = $storagePage;
686 }
687 if ($storagePage === null) {
688 return;
689 }
690 if (!isset($this->pageTSConfigCache[$storagePage])) {
691 $this->pageTSConfigCache[$storagePage] = BackendUtility::getPagesTSconfig($storagePage);
692 }
693 if (isset($this->pageTSConfigCache[$storagePage]['TCEMAIN.']['clearCacheCmd'])) {
694 $clearCacheCommands = GeneralUtility::trimExplode(',', strtolower($this->pageTSConfigCache[$storagePage]['TCEMAIN.']['clearCacheCmd']), true);
695 $clearCacheCommands = array_unique($clearCacheCommands);
696 foreach ($clearCacheCommands as $clearCacheCommand) {
697 if (MathUtility::canBeInterpretedAsInteger($clearCacheCommand)) {
698 $pageIdsToClear[] = $clearCacheCommand;
699 }
700 }
701 }
702
703 foreach ($pageIdsToClear as $pageIdToClear) {
704 $this->cacheService->getPageIdStack()->push($pageIdToClear);
705 }
706 }
707 }