TYPO3 CMS  TYPO3_8-7
Typo3DbBackend.php
Go to the documentation of this file.
1 <?php
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 
41 
46 {
50  protected $connectionPool;
51 
55  protected $dataMapper;
56 
62  protected $pageRepository;
63 
68 
72  protected $cacheService;
73 
78 
82  protected $objectManager;
83 
90  protected $hasPidColumn = [];
91 
96  {
97  $this->dataMapper = $dataMapper;
98  }
99 
104  {
105  $this->configurationManager = $configurationManager;
106  }
107 
112  {
113  $this->cacheService = $cacheService;
114  }
115 
120  {
121  $this->environmentService = $environmentService;
122  }
123 
128  {
129  $this->objectManager = $objectManager;
130  }
131 
135  public function __construct()
136  {
137  $this->connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
138  }
139 
149  public function addRow($tableName, array $fieldValues, $isRelation = false)
150  {
151  if (isset($fieldValues['uid'])) {
152  unset($fieldValues['uid']);
153  }
154  try {
155  $connection = $this->connectionPool->getConnectionForTable($tableName);
156 
157  $types = [];
158  $platform = $connection->getDatabasePlatform();
159  if ($platform instanceof SQLServerPlatform) {
160  // mssql needs to set proper PARAM_LOB and others to update fields
161  $tableDetails = $connection->getSchemaManager()->listTableDetails($tableName);
162  foreach ($fieldValues as $columnName => $columnValue) {
163  $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
164  }
165  }
166 
167  $connection->insert($tableName, $fieldValues, $types);
168  } catch (DBALException $e) {
169  throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230766, $e);
170  }
171 
172  $uid = 0;
173  if (!$isRelation) {
174  // Relation tables have no auto_increment column, so no retrieval must be tried.
175  $uid = $connection->lastInsertId($tableName);
176  $this->clearPageCache($tableName, $uid);
177  }
178  return (int)$uid;
179  }
180 
191  public function updateRow($tableName, array $fieldValues, $isRelation = false)
192  {
193  if (!isset($fieldValues['uid'])) {
194  throw new \InvalidArgumentException('The given row must contain a value for "uid".', 1476045164);
195  }
196 
197  $uid = (int)$fieldValues['uid'];
198  unset($fieldValues['uid']);
199 
200  try {
201  $connection = $this->connectionPool->getConnectionForTable($tableName);
202 
203  $types = [];
204  $platform = $connection->getDatabasePlatform();
205  if ($platform instanceof SQLServerPlatform) {
206  // mssql needs to set proper PARAM_LOB and others to update fields
207  $tableDetails = $connection->getSchemaManager()->listTableDetails($tableName);
208  foreach ($fieldValues as $columnName => $columnValue) {
209  $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType();
210  }
211  }
212 
213  $connection->update($tableName, $fieldValues, ['uid' => $uid], $types);
214  } catch (DBALException $e) {
215  throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230767, $e);
216  }
217 
218  if (!$isRelation) {
219  $this->clearPageCache($tableName, $uid);
220  }
221 
222  // always returns true
223  return true;
224  }
225 
235  public function updateRelationTableRow($tableName, array $fieldValues)
236  {
237  if (!isset($fieldValues['uid_local']) && !isset($fieldValues['uid_foreign'])) {
238  throw new \InvalidArgumentException(
239  'The given fieldValues must contain a value for "uid_local" and "uid_foreign".',
240  1360500126
241  );
242  }
243 
244  $where['uid_local'] = (int)$fieldValues['uid_local'];
245  $where['uid_foreign'] = (int)$fieldValues['uid_foreign'];
246  unset($fieldValues['uid_local']);
247  unset($fieldValues['uid_foreign']);
248 
249  if (!empty($fieldValues['tablenames'])) {
250  $where['tablenames'] = $fieldValues['tablenames'];
251  unset($fieldValues['tablenames']);
252  }
253  if (!empty($fieldValues['fieldname'])) {
254  $where['fieldname'] = $fieldValues['fieldname'];
255  unset($fieldValues['fieldname']);
256  }
257 
258  try {
259  $this->connectionPool->getConnectionForTable($tableName)->update($tableName, $fieldValues, $where);
260  } catch (DBALException $e) {
261  throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230768, $e);
262  }
263 
264  // always returns true
265  return true;
266  }
267 
277  public function removeRow($tableName, array $where, $isRelation = false)
278  {
279  try {
280  $this->connectionPool->getConnectionForTable($tableName)->delete($tableName, $where);
281  } catch (DBALException $e) {
282  throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230769, $e);
283  }
284 
285  if (!$isRelation && isset($where['uid'])) {
286  $this->clearPageCache($tableName, $where['uid']);
287  }
288 
289  // always returns true
290  return true;
291  }
292 
302  public function getMaxValueFromTable($tableName, array $where, $columnName)
303  {
304  try {
305  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
306  $queryBuilder->getRestrictions()->removeAll();
307  $queryBuilder
308  ->select($columnName)
309  ->from($tableName)
310  ->orderBy($columnName, 'DESC')
311  ->setMaxResults(1);
312 
313  foreach ($where as $fieldName => $value) {
314  $queryBuilder->andWhere(
315  $queryBuilder->expr()->eq($fieldName, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
316  );
317  }
318 
319  $result = $queryBuilder->execute()->fetchColumn(0);
320  } catch (DBALException $e) {
321  throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230770, $e);
322  }
323  return $result;
324  }
325 
334  public function getRowByIdentifier($tableName, array $where)
335  {
336  try {
337  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
338  $queryBuilder->getRestrictions()->removeAll();
339  $queryBuilder
340  ->select('*')
341  ->from($tableName);
342 
343  foreach ($where as $fieldName => $value) {
344  $queryBuilder->andWhere(
345  $queryBuilder->expr()->eq($fieldName, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR))
346  );
347  }
348 
349  $row = $queryBuilder->execute()->fetch();
350  } catch (DBALException $e) {
351  throw new SqlErrorException($e->getPrevious()->getMessage(), 1470230771, $e);
352  }
353  return $row ?: false;
354  }
355 
363  public function getObjectDataByQuery(QueryInterface $query)
364  {
365  $statement = $query->getStatement();
366  if ($statement instanceof Qom\Statement
367  && !$statement->getStatement() instanceof QueryBuilder
368  ) {
369  $rows = $this->getObjectDataByRawQuery($statement);
370  } else {
371  $queryParser = $this->objectManager->get(Typo3DbQueryParser::class);
372  if ($statement instanceof Qom\Statement
373  && $statement->getStatement() instanceof QueryBuilder
374  ) {
375  $queryBuilder = $statement->getStatement();
376  } else {
377  $queryBuilder = $queryParser->convertQueryToDoctrineQueryBuilder($query);
378  }
379  $selectParts = $queryBuilder->getQueryPart('select');
380  if ($queryParser->isDistinctQuerySuggested() && !empty($selectParts)) {
381  $selectParts[0] = 'DISTINCT ' . $selectParts[0];
382  $queryBuilder->selectLiteral(...$selectParts);
383  }
384  if ($query->getOffset()) {
385  $queryBuilder->setFirstResult($query->getOffset());
386  }
387  if ($query->getLimit()) {
388  $queryBuilder->setMaxResults($query->getLimit());
389  }
390  try {
391  $rows = $queryBuilder->execute()->fetchAll();
392  } catch (DBALException $e) {
393  throw new SqlErrorException($e->getPrevious()->getMessage(), 1472074485, $e);
394  }
395  }
396 
397  $rows = $this->doLanguageAndWorkspaceOverlay($query->getSource(), $rows, $query->getQuerySettings());
398  return $rows;
399  }
400 
408  protected function getObjectDataByRawQuery(Qom\Statement $statement)
409  {
410  $realStatement = $statement->getStatement();
411  $parameters = $statement->getBoundVariables();
412 
413  // The real statement is an instance of the Doctrine DBAL QueryBuilder, so fetching
414  // this directly is possible
415  if ($realStatement instanceof QueryBuilder) {
416  try {
417  $result = $realStatement->execute();
418  } catch (DBALException $e) {
419  throw new SqlErrorException($e->getPrevious()->getMessage(), 1472064721, $e);
420  }
421  $rows = $result->fetchAll();
422  } elseif ($realStatement instanceof \Doctrine\DBAL\Statement) {
423  try {
424  $realStatement->execute($parameters);
425  } catch (DBALException $e) {
426  throw new SqlErrorException($e->getPrevious()->getMessage(), 1481281404, $e);
427  }
428  $rows = $realStatement->fetchAll();
429  } elseif ($realStatement instanceof PreparedStatement) {
430  GeneralUtility::deprecationLog('Extbase support for Prepared Statements has been deprecated in TYPO3 v8, and will be removed in TYPO3 v9. Use native Doctrine DBAL Statements or QueryBuilder objects.');
431  $realStatement->execute($parameters);
432  $rows = $realStatement->fetchAll();
433 
434  $realStatement->free();
435  } else {
436  // Do a real raw query. This is very stupid, as it does not allow to use DBAL's real power if
437  // several tables are on different databases, so this is used with caution and could be removed
438  // in the future
439  try {
440  $connection = $this->connectionPool->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
441  $statement = $connection->executeQuery($realStatement, $parameters);
442  } catch (DBALException $e) {
443  throw new SqlErrorException($e->getPrevious()->getMessage(), 1472064775, $e);
444  }
445 
446  $rows = $statement->fetchAll();
447  }
448 
449  return $rows;
450  }
451 
460  public function getObjectCountByQuery(QueryInterface $query)
461  {
462  if ($query->getConstraint() instanceof Qom\Statement) {
463  throw new BadConstraintException('Could not execute count on queries with a constraint of type TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Qom\\Statement', 1256661045);
464  }
465 
466  $statement = $query->getStatement();
467  if ($statement instanceof Qom\Statement
468  && !$statement->getStatement() instanceof QueryBuilder
469  ) {
470  $rows = $this->getObjectDataByQuery($query);
471  $count = count($rows);
472  } else {
473  $queryParser = $this->objectManager->get(Typo3DbQueryParser::class);
474  $queryBuilder = $queryParser
475  ->convertQueryToDoctrineQueryBuilder($query)
476  ->resetQueryPart('orderBy');
477 
478  if ($queryParser->isDistinctQuerySuggested()) {
479  $source = $queryBuilder->getQueryPart('from')[0];
480  // Tablename is already quoted for the DBMS, we need to treat table and field names separately
481  $tableName = $source['alias'] ?: $source['table'];
482  $fieldName = $queryBuilder->quoteIdentifier('uid');
483  $queryBuilder->resetQueryPart('groupBy')
484  ->selectLiteral(sprintf('COUNT(DISTINCT %s.%s)', $tableName, $fieldName));
485  } else {
486  $queryBuilder->count('*');
487  }
488 
489  try {
490  $count = $queryBuilder->execute()->fetchColumn(0);
491  } catch (DBALException $e) {
492  throw new SqlErrorException($e->getPrevious()->getMessage(), 1472074379, $e);
493  }
494  if ($query->getOffset()) {
495  $count -= $query->getOffset();
496  }
497  if ($query->getLimit()) {
498  $count = min($count, $query->getLimit());
499  }
500  }
501  return (int)max(0, $count);
502  }
503 
512  {
513  $dataMap = $this->dataMapper->getDataMap(get_class($object));
514  $tableName = $dataMap->getTableName();
515  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
516  if ($this->environmentService->isEnvironmentInFrontendMode()) {
517  $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
518  }
519  $whereClause = [];
520  // loop over all properties of the object to exactly set the values of each database field
521  $properties = $object->_getProperties();
522  foreach ($properties as $propertyName => $propertyValue) {
523  // @todo We couple the Backend to the Entity implementation (uid, isClone); changes there breaks this method
524  if ($dataMap->isPersistableProperty($propertyName) && $propertyName !== 'uid' && $propertyName !== 'pid' && $propertyName !== 'isClone') {
525  $fieldName = $dataMap->getColumnMap($propertyName)->getColumnName();
526  if ($propertyValue === null) {
527  $whereClause[] = $queryBuilder->expr()->isNull($fieldName);
528  } else {
529  $whereClause[] = $queryBuilder->expr()->eq($fieldName, $queryBuilder->createNamedParameter($this->dataMapper->getPlainValue($propertyValue)));
530  }
531  }
532  }
533  $queryBuilder
534  ->select('uid')
535  ->from($tableName)
536  ->where(...$whereClause);
537 
538  try {
539  $uid = (int)$queryBuilder
540  ->execute()
541  ->fetchColumn(0);
542  if ($uid > 0) {
543  return $uid;
544  }
545  return false;
546  } catch (DBALException $e) {
547  throw new SqlErrorException($e->getPrevious()->getMessage(), 1470231748, $e);
548  }
549  }
550 
561  protected function doLanguageAndWorkspaceOverlay(Qom\SourceInterface $source, array $rows, QuerySettingsInterface $querySettings, $workspaceUid = null)
562  {
563  if ($source instanceof Qom\SelectorInterface) {
564  $tableName = $source->getSelectorName();
565  } elseif ($source instanceof Qom\JoinInterface) {
566  $tableName = $source->getRight()->getSelectorName();
567  } else {
568  // No proper source, so we do not have a table name here
569  // we cannot do an overlay and return the original rows instead.
570  return $rows;
571  }
572 
574  if (is_object($this->getTSFE())) {
575  if ($workspaceUid !== null) {
576  $pageRepository->versioningWorkspaceId = $workspaceUid;
577  }
578  } else {
579  if ($workspaceUid === null) {
580  $workspaceUid = $this->getBeUser()->workspace;
581  }
582  $pageRepository->versioningWorkspaceId = $workspaceUid;
583  }
584 
585  // Fetches the move-placeholder in case it is supported
586  // by the table and if there's only one row in the result set
587  // (applying this to all rows does not work, since the sorting
588  // order would be destroyed and possible limits not met anymore)
589  if (!empty($pageRepository->versioningWorkspaceId)
591  && count($rows) === 1
592  ) {
593  $versionId = $pageRepository->versioningWorkspaceId;
594  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
595  $queryBuilder->getRestrictions()->removeAll();
596  $movePlaceholder = $queryBuilder->select($tableName . '.*')
597  ->from($tableName)
598  ->where(
599  $queryBuilder->expr()->eq('t3ver_state', $queryBuilder->createNamedParameter(3, \PDO::PARAM_INT)),
600  $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter($versionId, \PDO::PARAM_INT)),
601  $queryBuilder->expr()->eq('t3ver_move_id', $queryBuilder->createNamedParameter($rows[0]['uid'], \PDO::PARAM_INT))
602  )
603  ->setMaxResults(1)
604  ->execute()
605  ->fetch();
606  if (!empty($movePlaceholder)) {
607  $rows = [$movePlaceholder];
608  }
609  }
610 
611  $overlaidRows = [];
612  foreach ($rows as $row) {
613  // If current row is a translation select its parent
614  if (isset($tableName) && isset($GLOBALS['TCA'][$tableName])
615  && isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
616  && isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
617  && $tableName !== 'pages_language_overlay'
618  ) {
619  if (isset($row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']])
620  && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0
621  ) {
622  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
623  $queryBuilder->getRestrictions()->removeAll();
624  $row = $queryBuilder->select($tableName . '.*')
625  ->from($tableName)
626  ->where(
627  $queryBuilder->expr()->eq(
628  $tableName . '.uid',
629  $queryBuilder->createNamedParameter(
630  $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']],
631  \PDO::PARAM_INT
632  )
633  ),
634  $queryBuilder->expr()->eq(
635  $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'],
636  $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
637  )
638  )
639  ->setMaxResults(1)
640  ->execute()
641  ->fetch();
642  }
643  }
644  $pageRepository->versionOL($tableName, $row, true);
645  if ($tableName === 'pages') {
646  $row = $pageRepository->getPageOverlay($row, $querySettings->getLanguageUid());
647  } elseif (isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
648  && $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] !== ''
649  && $tableName !== 'pages_language_overlay'
650  ) {
651  if (in_array($row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']], [-1, 0])) {
652  $overlayMode = $querySettings->getLanguageMode() === 'strict' ? 'hideNonTranslated' : '';
653  $row = $pageRepository->getRecordOverlay($tableName, $row, $querySettings->getLanguageUid(), $overlayMode);
654  }
655  }
656  if ($row !== null && is_array($row)) {
657  $overlaidRows[] = $row;
658  }
659  }
660  return $overlaidRows;
661  }
662 
666  protected function getPageRepository()
667  {
668  if (!$this->pageRepository instanceof PageRepository) {
669  if ($this->environmentService->isEnvironmentInFrontendMode() && is_object($this->getTSFE())) {
670  $this->pageRepository = $this->getTSFE()->sys_page;
671  } else {
672  $this->pageRepository = GeneralUtility::makeInstance(PageRepository::class);
673  }
674  }
675 
676  return $this->pageRepository;
677  }
678 
689  protected function clearPageCache($tableName, $uid)
690  {
691  $frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
692  if (isset($frameworkConfiguration['persistence']['enableAutomaticCacheClearing']) && $frameworkConfiguration['persistence']['enableAutomaticCacheClearing'] === '1') {
693  } else {
694  // if disabled, return
695  return;
696  }
697  $pageIdsToClear = [];
698  $storagePage = null;
699 
700  // As determining the table columns is a costly operation this is done only once per table during runtime and cached then
701  if (!isset($this->hasPidColumn[$tableName])) {
702  $columns = GeneralUtility::makeInstance(ConnectionPool::class)
703  ->getConnectionForTable($tableName)
704  ->getSchemaManager()
705  ->listTableColumns($tableName);
706  $this->hasPidColumn[$tableName] = array_key_exists('pid', $columns);
707  }
708 
709  $tsfe = $this->getTSFE();
710  if ($this->hasPidColumn[$tableName]) {
711  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
712  $queryBuilder->getRestrictions()->removeAll();
713  $result = $queryBuilder
714  ->select('pid')
715  ->from($tableName)
716  ->where(
717  $queryBuilder->expr()->eq(
718  'uid',
719  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
720  )
721  )
722  ->execute();
723  if ($row = $result->fetch()) {
724  $storagePage = $row['pid'];
725  $pageIdsToClear[] = $storagePage;
726  }
727  } elseif (isset($tsfe)) {
728  // No PID column - we can do a best-effort to clear the cache of the current page if in FE
729  $storagePage = $tsfe->id;
730  $pageIdsToClear[] = $storagePage;
731  }
732  if ($storagePage === null) {
733  return;
734  }
735 
736  $pageTS = BackendUtility::getPagesTSconfig($storagePage);
737  if (isset($pageTS['TCEMAIN.']['clearCacheCmd'])) {
738  $clearCacheCommands = GeneralUtility::trimExplode(',', strtolower($pageTS['TCEMAIN.']['clearCacheCmd']), true);
739  $clearCacheCommands = array_unique($clearCacheCommands);
740  foreach ($clearCacheCommands as $clearCacheCommand) {
741  if (MathUtility::canBeInterpretedAsInteger($clearCacheCommand)) {
742  $pageIdsToClear[] = $clearCacheCommand;
743  }
744  }
745  }
746 
747  foreach ($pageIdsToClear as $pageIdToClear) {
748  $this->cacheService->getPageIdStack()->push($pageIdToClear);
749  }
750  }
751 
755  protected function getTSFE()
756  {
757  return $GLOBALS['TSFE'] ?? null;
758  }
759 
763  protected function getBeUser()
764  {
765  return $GLOBALS['BE_USER'] ?? null;
766  }
767 }
static getPagesTSconfig($id, $rootLine=null, $returnPartArray=false)
injectEnvironmentService(EnvironmentService $environmentService)
doLanguageAndWorkspaceOverlay(Qom\SourceInterface $source, array $rows, QuerySettingsInterface $querySettings, $workspaceUid=null)
injectConfigurationManager(ConfigurationManagerInterface $configurationManager)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
addRow($tableName, array $fieldValues, $isRelation=false)
static makeInstance($className,... $constructorArguments)
injectObjectManager(ObjectManagerInterface $objectManager)
updateRow($tableName, array $fieldValues, $isRelation=false)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
getMaxValueFromTable($tableName, array $where, $columnName)
removeRow($tableName, array $where, $isRelation=false)