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