| Total Complexity | 109 |
| Total Lines | 679 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like Typo3DbBackend often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Typo3DbBackend, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 54 | class Typo3DbBackend implements BackendInterface, SingletonInterface |
||
| 55 | { |
||
| 56 | /** |
||
| 57 | * @var ConnectionPool |
||
| 58 | */ |
||
| 59 | protected $connectionPool; |
||
| 60 | |||
| 61 | /** |
||
| 62 | * @var ConfigurationManagerInterface |
||
| 63 | */ |
||
| 64 | protected $configurationManager; |
||
| 65 | |||
| 66 | /** |
||
| 67 | * @var CacheService |
||
| 68 | */ |
||
| 69 | protected $cacheService; |
||
| 70 | |||
| 71 | /** |
||
| 72 | * @var EnvironmentService |
||
| 73 | */ |
||
| 74 | protected $environmentService; |
||
| 75 | |||
| 76 | /** |
||
| 77 | * @var ObjectManagerInterface |
||
| 78 | */ |
||
| 79 | protected $objectManager; |
||
| 80 | |||
| 81 | /** |
||
| 82 | * As determining the table columns is a costly operation this is done only once per table during runtime and cached then |
||
| 83 | * |
||
| 84 | * @var array |
||
| 85 | * @see clearPageCache() |
||
| 86 | */ |
||
| 87 | protected $hasPidColumn = []; |
||
| 88 | |||
| 89 | /** |
||
| 90 | * @param ConfigurationManagerInterface $configurationManager |
||
| 91 | */ |
||
| 92 | public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void |
||
| 93 | { |
||
| 94 | $this->configurationManager = $configurationManager; |
||
| 95 | } |
||
| 96 | |||
| 97 | /** |
||
| 98 | * @param CacheService $cacheService |
||
| 99 | */ |
||
| 100 | public function injectCacheService(CacheService $cacheService): void |
||
| 101 | { |
||
| 102 | $this->cacheService = $cacheService; |
||
| 103 | } |
||
| 104 | |||
| 105 | /** |
||
| 106 | * @param EnvironmentService $environmentService |
||
| 107 | */ |
||
| 108 | public function injectEnvironmentService(EnvironmentService $environmentService): void |
||
| 109 | { |
||
| 110 | $this->environmentService = $environmentService; |
||
| 111 | } |
||
| 112 | |||
| 113 | /** |
||
| 114 | * @param ObjectManagerInterface $objectManager |
||
| 115 | */ |
||
| 116 | public function injectObjectManager(ObjectManagerInterface $objectManager): void |
||
| 117 | { |
||
| 118 | $this->objectManager = $objectManager; |
||
| 119 | } |
||
| 120 | |||
| 121 | /** |
||
| 122 | * Constructor. |
||
| 123 | */ |
||
| 124 | public function __construct() |
||
| 125 | { |
||
| 126 | $this->connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); |
||
| 127 | } |
||
| 128 | |||
| 129 | /** |
||
| 130 | * Adds a row to the storage |
||
| 131 | * |
||
| 132 | * @param string $tableName The database table name |
||
| 133 | * @param array $fieldValues The row to be inserted |
||
| 134 | * @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default |
||
| 135 | * @return int The uid of the inserted row |
||
| 136 | * @throws SqlErrorException |
||
| 137 | */ |
||
| 138 | public function addRow(string $tableName, array $fieldValues, bool $isRelation = false): int |
||
| 168 | } |
||
| 169 | |||
| 170 | /** |
||
| 171 | * Updates a row in the storage |
||
| 172 | * |
||
| 173 | * @param string $tableName The database table name |
||
| 174 | * @param array $fieldValues The row to be updated |
||
| 175 | * @param bool $isRelation TRUE if we are currently inserting into a relation table, FALSE by default |
||
| 176 | * @throws \InvalidArgumentException |
||
| 177 | * @throws SqlErrorException |
||
| 178 | */ |
||
| 179 | public function updateRow(string $tableName, array $fieldValues, bool $isRelation = false): void |
||
| 209 | } |
||
| 210 | } |
||
| 211 | |||
| 212 | /** |
||
| 213 | * Updates a relation row in the storage. |
||
| 214 | * |
||
| 215 | * @param string $tableName The database relation table name |
||
| 216 | * @param array $fieldValues The row to be updated |
||
| 217 | * @throws SqlErrorException |
||
| 218 | * @throws \InvalidArgumentException |
||
| 219 | */ |
||
| 220 | public function updateRelationTableRow(string $tableName, array $fieldValues): void |
||
| 248 | } |
||
| 249 | } |
||
| 250 | |||
| 251 | /** |
||
| 252 | * Deletes a row in the storage |
||
| 253 | * |
||
| 254 | * @param string $tableName The database table name |
||
| 255 | * @param array $where An array of where array('fieldname' => value). |
||
| 256 | * @param bool $isRelation TRUE if we are currently manipulating a relation table, FALSE by default |
||
| 257 | * @throws SqlErrorException |
||
| 258 | */ |
||
| 259 | public function removeRow(string $tableName, array $where, bool $isRelation = false): void |
||
| 269 | } |
||
| 270 | } |
||
| 271 | |||
| 272 | /** |
||
| 273 | * Returns the object data matching the $query. |
||
| 274 | * |
||
| 275 | * @param QueryInterface $query |
||
| 276 | * @return array |
||
| 277 | * @throws SqlErrorException |
||
| 278 | */ |
||
| 279 | public function getObjectDataByQuery(QueryInterface $query): array |
||
| 320 | } |
||
| 321 | |||
| 322 | /** |
||
| 323 | * Returns the object data using a custom statement |
||
| 324 | * |
||
| 325 | * @param Qom\Statement $statement |
||
| 326 | * @return array |
||
| 327 | * @throws SqlErrorException when the raw SQL statement fails in the database |
||
| 328 | */ |
||
| 329 | protected function getObjectDataByRawQuery(Statement $statement): array |
||
| 365 | } |
||
| 366 | |||
| 367 | /** |
||
| 368 | * Returns the number of tuples matching the query. |
||
| 369 | * |
||
| 370 | * @param QueryInterface $query |
||
| 371 | * @return int The number of matching tuples |
||
| 372 | * @throws BadConstraintException |
||
| 373 | * @throws SqlErrorException |
||
| 374 | */ |
||
| 375 | public function getObjectCountByQuery(QueryInterface $query): int |
||
| 418 | } |
||
| 419 | |||
| 420 | /** |
||
| 421 | * Checks if a Value Object equal to the given Object exists in the database |
||
| 422 | * |
||
| 423 | * @param AbstractValueObject $object The Value Object |
||
| 424 | * @return int|null The matching uid if an object was found, else FALSE |
||
| 425 | * @throws SqlErrorException |
||
| 426 | */ |
||
| 427 | public function getUidOfAlreadyPersistedValueObject(AbstractValueObject $object): ?int |
||
| 468 | } |
||
| 469 | } |
||
| 470 | |||
| 471 | /** |
||
| 472 | * Performs workspace and language overlay on the given row array. The language and workspace id is automatically |
||
| 473 | * detected (depending on FE or BE context). You can also explicitly set the language/workspace id. |
||
| 474 | * |
||
| 475 | * @param Qom\SourceInterface $source The source (selector or join) |
||
| 476 | * @param array $rows |
||
| 477 | * @param QueryInterface $query |
||
| 478 | * @param int|null $workspaceUid |
||
| 479 | * @return array |
||
| 480 | * @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException |
||
| 481 | */ |
||
| 482 | protected function overlayLanguageAndWorkspace(SourceInterface $source, array $rows, QueryInterface $query, int $workspaceUid = null): array |
||
| 511 | } |
||
| 512 | |||
| 513 | /** |
||
| 514 | * If the result is a plain SELECT (no JOIN) then the regular overlay process works for tables |
||
| 515 | * - overlay workspace |
||
| 516 | * - overlay language of versioned record again |
||
| 517 | */ |
||
| 518 | protected function overlayLanguageAndWorkspaceForSelect(string $tableName, array $rows, PageRepository $pageRepository, QueryInterface $query): array |
||
| 519 | { |
||
| 520 | $overlaidRows = []; |
||
| 521 | foreach ($rows as $row) { |
||
| 522 | $row = $this->overlayLanguageAndWorkspaceForSingleRecord($tableName, $row, $pageRepository, $query); |
||
| 523 | if (is_array($row)) { |
||
| 524 | $overlaidRows[] = $row; |
||
| 525 | } |
||
| 526 | } |
||
| 527 | return $overlaidRows; |
||
| 528 | } |
||
| 529 | |||
| 530 | /** |
||
| 531 | * If the result consists of a JOIN (usually happens if a property is a relation with a MM table) then it is necessary |
||
| 532 | * to only do overlays for the fields that are contained in the main database table, otherwise a SQL error is thrown. |
||
| 533 | * In order to make this happen, a single SQL query is made to fetch all possible field names (= array keys) of |
||
| 534 | * a record (TCA[$tableName][columns] does not contain all needed information), which is then used to compute |
||
| 535 | * a separate subset of the row which can be overlaid properly. |
||
| 536 | */ |
||
| 537 | protected function overlayLanguageAndWorkspaceForJoinedSelect(string $tableName, array $rows, PageRepository $pageRepository, QueryInterface $query): array |
||
| 538 | { |
||
| 539 | // No valid rows, so this is skipped |
||
| 540 | if (!isset($rows[0]['uid'])) { |
||
| 541 | return $rows; |
||
| 542 | } |
||
| 543 | // First, find out the fields that belong to the "main" selected table which is defined by TCA, and take the first |
||
| 544 | // record to find out all possible fields in this database table |
||
| 545 | $fieldsOfMainTable = $pageRepository->getRawRecord($tableName, $rows[0]['uid']); |
||
| 546 | $overlaidRows = []; |
||
| 547 | foreach ($rows as $row) { |
||
| 548 | $mainRow = array_intersect_key($row, $fieldsOfMainTable); |
||
|
|
|||
| 549 | $joinRow = array_diff_key($row, $mainRow); |
||
| 550 | $mainRow = $this->overlayLanguageAndWorkspaceForSingleRecord($tableName, $mainRow, $pageRepository, $query); |
||
| 551 | if (is_array($mainRow)) { |
||
| 552 | $overlaidRows[] = array_replace($joinRow, $mainRow); |
||
| 553 | } |
||
| 554 | } |
||
| 555 | return $overlaidRows; |
||
| 556 | } |
||
| 557 | |||
| 558 | /** |
||
| 559 | * Takes one specific row, as defined in TCA and does all overlays. |
||
| 560 | * |
||
| 561 | * @param string $tableName |
||
| 562 | * @param array $row |
||
| 563 | * @param PageRepository $pageRepository |
||
| 564 | * @param QueryInterface $query |
||
| 565 | * @return array|int|mixed|null the overlaid row or false or null if overlay failed. |
||
| 566 | */ |
||
| 567 | protected function overlayLanguageAndWorkspaceForSingleRecord(string $tableName, array $row, PageRepository $pageRepository, QueryInterface $query) |
||
| 568 | { |
||
| 569 | $querySettings = $query->getQuerySettings(); |
||
| 570 | // If current row is a translation select its parent |
||
| 571 | $languageOfCurrentRecord = 0; |
||
| 572 | if ($GLOBALS['TCA'][$tableName]['ctrl']['languageField'] ?? null |
||
| 573 | && $row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']] ?? 0 |
||
| 574 | ) { |
||
| 575 | $languageOfCurrentRecord = $row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']]; |
||
| 576 | } |
||
| 577 | if ($querySettings->getLanguageOverlayMode() |
||
| 578 | && $languageOfCurrentRecord > 0 |
||
| 579 | && isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']) |
||
| 580 | && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0 |
||
| 581 | ) { |
||
| 582 | $row = $pageRepository->getRawRecord( |
||
| 583 | $tableName, |
||
| 584 | (int)$row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] |
||
| 585 | ); |
||
| 586 | } |
||
| 587 | // Handle workspace overlays |
||
| 588 | $pageRepository->versionOL($tableName, $row, true); |
||
| 589 | if (is_array($row) && $querySettings->getLanguageOverlayMode()) { |
||
| 590 | if ($tableName === 'pages') { |
||
| 591 | $row = $pageRepository->getPageOverlay($row, $querySettings->getLanguageUid()); |
||
| 592 | } else { |
||
| 593 | // todo: remove type cast once getLanguageUid strictly returns an int |
||
| 594 | $languageUid = (int)$querySettings->getLanguageUid(); |
||
| 595 | if (!$querySettings->getRespectSysLanguage() |
||
| 596 | && $languageOfCurrentRecord > 0 |
||
| 597 | && (!$query instanceof Query || !$query->getParentQuery()) |
||
| 598 | ) { |
||
| 599 | // No parent query means we're processing the aggregate root. |
||
| 600 | // respectSysLanguage is false which means that records returned by the query |
||
| 601 | // might be from different languages (which is desired). |
||
| 602 | // So we must set the language used for overlay to the language of the current record |
||
| 603 | $languageUid = $languageOfCurrentRecord; |
||
| 604 | } |
||
| 605 | if (isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']) |
||
| 606 | && $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']] > 0 |
||
| 607 | && $languageOfCurrentRecord > 0 |
||
| 608 | ) { |
||
| 609 | // Force overlay by faking default language record, as getRecordOverlay can only handle default language records |
||
| 610 | $row['uid'] = $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']]; |
||
| 611 | $row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']] = 0; |
||
| 612 | } |
||
| 613 | $row = $pageRepository->getRecordOverlay($tableName, $row, $languageUid, (string)$querySettings->getLanguageOverlayMode()); |
||
| 614 | } |
||
| 615 | } |
||
| 616 | return $row; |
||
| 617 | } |
||
| 618 | |||
| 619 | /** |
||
| 620 | * Fetches the moved record in case it is supported |
||
| 621 | * by the table and if there's only one row in the result set |
||
| 622 | * (applying this to all rows does not work, since the sorting |
||
| 623 | * order would be destroyed and possible limits are not met anymore) |
||
| 624 | * The move pointers are later unset (see versionOL() last argument) |
||
| 625 | */ |
||
| 626 | protected function resolveMovedRecordsInWorkspace(string $tableName, array $rows, int $workspaceUid): array |
||
| 627 | { |
||
| 628 | if ($workspaceUid === 0) { |
||
| 629 | return $rows; |
||
| 630 | } |
||
| 631 | if (!BackendUtility::isTableWorkspaceEnabled($tableName)) { |
||
| 632 | return $rows; |
||
| 633 | } |
||
| 634 | if (count($rows) !== 1) { |
||
| 635 | return $rows; |
||
| 636 | } |
||
| 637 | $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName); |
||
| 638 | $queryBuilder->getRestrictions()->removeAll(); |
||
| 639 | $movedRecords = $queryBuilder |
||
| 640 | ->select('*') |
||
| 641 | ->from($tableName) |
||
| 642 | ->where( |
||
| 643 | $queryBuilder->expr()->eq('t3ver_state', $queryBuilder->createNamedParameter(VersionState::MOVE_POINTER, \PDO::PARAM_INT)), |
||
| 644 | $queryBuilder->expr()->eq('t3ver_wsid', $queryBuilder->createNamedParameter($workspaceUid, \PDO::PARAM_INT)), |
||
| 645 | $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($rows[0]['uid'], \PDO::PARAM_INT)) |
||
| 646 | ) |
||
| 647 | ->setMaxResults(1) |
||
| 648 | ->execute() |
||
| 649 | ->fetchAll(); |
||
| 650 | if (!empty($movedRecords)) { |
||
| 651 | $rows = $movedRecords; |
||
| 652 | } |
||
| 653 | return $rows; |
||
| 654 | } |
||
| 655 | |||
| 656 | /** |
||
| 657 | * Clear the TYPO3 page cache for the given record. |
||
| 658 | * If the record lies on a page, then we clear the cache of this page. |
||
| 659 | * If the record has no PID column, we clear the cache of the current page as best-effort. |
||
| 660 | * |
||
| 661 | * Much of this functionality is taken from DataHandler::clear_cache() which unfortunately only works with logged-in BE user. |
||
| 662 | * |
||
| 663 | * @param string $tableName Table name of the record |
||
| 664 | * @param int $uid UID of the record |
||
| 665 | */ |
||
| 666 | protected function clearPageCache(string $tableName, int $uid): void |
||
| 667 | { |
||
| 668 | $frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK); |
||
| 669 | if (empty($frameworkConfiguration['persistence']['enableAutomaticCacheClearing'])) { |
||
| 670 | return; |
||
| 671 | } |
||
| 672 | $pageIdsToClear = []; |
||
| 673 | $storagePage = null; |
||
| 674 | |||
| 675 | // As determining the table columns is a costly operation this is done only once per table during runtime and cached then |
||
| 676 | if (!isset($this->hasPidColumn[$tableName])) { |
||
| 677 | $columns = $this->connectionPool |
||
| 678 | ->getConnectionForTable($tableName) |
||
| 679 | ->getSchemaManager() |
||
| 680 | ->listTableColumns($tableName); |
||
| 681 | $this->hasPidColumn[$tableName] = array_key_exists('pid', $columns); |
||
| 682 | } |
||
| 683 | |||
| 684 | $tsfe = $this->getTSFE(); |
||
| 685 | if ($this->hasPidColumn[$tableName]) { |
||
| 686 | $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName); |
||
| 687 | $queryBuilder->getRestrictions()->removeAll(); |
||
| 688 | $result = $queryBuilder |
||
| 689 | ->select('pid') |
||
| 690 | ->from($tableName) |
||
| 691 | ->where( |
||
| 692 | $queryBuilder->expr()->eq( |
||
| 693 | 'uid', |
||
| 694 | $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) |
||
| 695 | ) |
||
| 696 | ) |
||
| 697 | ->execute(); |
||
| 698 | if ($row = $result->fetch()) { |
||
| 699 | $storagePage = $row['pid']; |
||
| 700 | $pageIdsToClear[] = $storagePage; |
||
| 701 | } |
||
| 702 | } elseif (isset($tsfe)) { |
||
| 703 | // No PID column - we can do a best-effort to clear the cache of the current page if in FE |
||
| 704 | $storagePage = $tsfe->id; |
||
| 705 | $pageIdsToClear[] = $storagePage; |
||
| 706 | } |
||
| 707 | if ($storagePage === null) { |
||
| 708 | return; |
||
| 709 | } |
||
| 710 | |||
| 711 | $pageTS = BackendUtility::getPagesTSconfig($storagePage); |
||
| 712 | if (isset($pageTS['TCEMAIN.']['clearCacheCmd'])) { |
||
| 713 | $clearCacheCommands = GeneralUtility::trimExplode(',', strtolower($pageTS['TCEMAIN.']['clearCacheCmd']), true); |
||
| 714 | $clearCacheCommands = array_unique($clearCacheCommands); |
||
| 715 | foreach ($clearCacheCommands as $clearCacheCommand) { |
||
| 716 | if (MathUtility::canBeInterpretedAsInteger($clearCacheCommand)) { |
||
| 717 | $pageIdsToClear[] = $clearCacheCommand; |
||
| 718 | } |
||
| 719 | } |
||
| 720 | } |
||
| 721 | |||
| 722 | foreach ($pageIdsToClear as $pageIdToClear) { |
||
| 723 | $this->cacheService->getPageIdStack()->push($pageIdToClear); |
||
| 724 | } |
||
| 725 | } |
||
| 726 | |||
| 727 | /** |
||
| 728 | * @return TypoScriptFrontendController|null |
||
| 729 | */ |
||
| 730 | protected function getTSFE(): ?TypoScriptFrontendController |
||
| 733 | } |
||
| 734 | } |
||
| 735 |