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