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