tableName = $tableName; $this->liveIds = $this->reindex($this->sanitizeIds($liveIds)); $this->sortingStatement = $sortingStatement; } /** * Sets the target workspace ID the final result shall use. * * @param int $workspaceId */ public function setWorkspaceId($workspaceId) { $this->workspaceId = (int)$workspaceId; } /** * Sets whether live IDs shall be kept in the final result set. * * @param bool $keepLiveIds * @return PlainDataResolver */ public function setKeepLiveIds($keepLiveIds) { $this->keepLiveIds = (bool)$keepLiveIds; return $this; } /** * Sets whether delete placeholders shall be kept in the final result set. * * @param bool $keepDeletePlaceholder * @return PlainDataResolver */ public function setKeepDeletePlaceholder($keepDeletePlaceholder) { $this->keepDeletePlaceholder = (bool)$keepDeletePlaceholder; return $this; } /** * Sets whether move placeholders shall be kept in case they cannot be substituted. * * @param bool $keepMovePlaceholder * @return PlainDataResolver */ public function setKeepMovePlaceholder($keepMovePlaceholder) { $this->keepMovePlaceholder = (bool)$keepMovePlaceholder; return $this; } /** * @return int[] */ public function get() { $resolvedIds = $this->processVersionOverlays($this->liveIds); if ($resolvedIds !== $this->liveIds) { $resolvedIds = $this->reindex($resolvedIds); } $tempIds = $this->processSorting($resolvedIds); if ($tempIds !== $resolvedIds) { $resolvedIds = $this->reindex($tempIds); } $tempIds = $this->applyLiveIds($resolvedIds); if ($tempIds !== $resolvedIds) { $resolvedIds = $this->reindex($tempIds); } return $resolvedIds; } /** * Processes version overlays on the final result set. * * @param int[] $ids * @return int[] * @internal */ public function processVersionOverlays(array $ids) { $ids = $this->sanitizeIds($ids); if (empty($this->workspaceId) || !$this->isWorkspaceEnabled() || empty($ids)) { return $ids; } $ids = $this->reindex( $this->processVersionMovePlaceholders($ids) ); $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable($this->tableName); $queryBuilder->getRestrictions()->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); $result = $queryBuilder ->select('uid', 't3ver_oid', 't3ver_state') ->from($this->tableName) ->where( $queryBuilder->expr()->in( 't3ver_oid', $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY) ), $queryBuilder->expr()->eq( 't3ver_wsid', $queryBuilder->createNamedParameter($this->workspaceId, Connection::PARAM_INT) ) ) ->executeQuery(); while ($version = $result->fetchAssociative()) { $liveReferenceId = (int)$version['t3ver_oid']; $versionId = (int)$version['uid']; if (isset($ids[$liveReferenceId])) { if (!$this->keepDeletePlaceholder && VersionState::tryFrom((int)($version['t3ver_state'] ?? 0)) === VersionState::DELETE_PLACEHOLDER ) { unset($ids[$liveReferenceId]); } else { $ids[$liveReferenceId] = $versionId; } } } return $ids; } /** * Processes and resolves move placeholders on the final result set. * * @param int[] $ids * @return int[] * @internal */ public function processVersionMovePlaceholders(array $ids) { $ids = $this->sanitizeIds($ids); // Early return on insufficient data-set if (empty($this->workspaceId) || !$this->isWorkspaceEnabled() || empty($ids)) { return $ids; } $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable($this->tableName); $queryBuilder->getRestrictions()->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); $result = $queryBuilder ->select('uid', 't3ver_oid') ->from($this->tableName) ->where( $queryBuilder->expr()->eq( 't3ver_state', $queryBuilder->createNamedParameter(VersionState::MOVE_POINTER->value, Connection::PARAM_INT) ), $queryBuilder->expr()->eq( 't3ver_wsid', $queryBuilder->createNamedParameter($this->workspaceId, Connection::PARAM_INT) ), $queryBuilder->expr()->in( 't3ver_oid', $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY) ) ) ->executeQuery(); while ($movedRecord = $result->fetchAssociative()) { $liveReferenceId = (int)$movedRecord['t3ver_oid']; $movedVersionId = (int)$movedRecord['uid']; // Substitute moved record and purge live reference if (isset($ids[$movedVersionId])) { $ids[$movedVersionId] = $liveReferenceId; unset($ids[$liveReferenceId]); } elseif (!$this->keepMovePlaceholder) { // Just purge live reference unset($ids[$liveReferenceId]); } } return $ids; } /** * Processes sorting of the final result set, if * a sorting statement (table column/expression) is given. * * @param int[] $ids * @return int[] * @internal */ public function processSorting(array $ids) { $ids = $this->sanitizeIds($ids); // Early return on missing sorting statement or insufficient data-set if (empty($this->sortingStatement) || count($ids) < 2) { return $ids; } $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->tableName); // Never apply additional restrictions like 'deleted' to the incoming id list $queryBuilder->getRestrictions()->removeAll(); $queryBuilder ->select('uid') ->from($this->tableName) ->where( $queryBuilder->expr()->in( 'uid', // do not use named parameter here as the list can get too long array_map(intval(...), $ids) ) ); foreach ($this->sortingStatement as $sortingStatement) { $queryBuilder->getConcreteQueryBuilder()->addOrderBy($sortingStatement); } // Always add explicit order by uid to have deterministic rows from dbms like postgres. // Scenario (see workspace FAL/Modify/ActionTest modifyContentAndDeleteFileReference): // A content element with two images - sys_file_reference uid=23 with sorting_foreign=2 (!) // and sys_file_reference uid=42 with sorting_foreign=1. The references have been added // and later changed their sorting that uid 42 is before 23. // Then, in workspaces, image reference 42 is deleted and 23 is changed (eg. title). This // creates two overlays: a 'delete placeholder' t3ver_state=2 with sorting_foreign=1 for 42, // and a 'changed' record t3ver_state=0 with sorting_foreign=1 for 23. // So both overlay records end up with sorting_foreign=1. This is technically ok since the // 'delete placeholder' "does not exist" from a live relation point of view, so the next // "real" record starts with 1 when published. // BUT, this scenario makes the order of returned rows non-deterministic for dbms that // do not implicitly order by uid (mysql does, postgres does not): The usual orderBy // is 'sorting_foreign' but both are 1 now. // We thus add a general explicit order by uid here to force deterministic row returns. $queryBuilder->addOrderBy('uid'); $sortedIds = $queryBuilder->executeQuery()->fetchAllAssociative(); return array_map(intval(...), array_column($sortedIds, 'uid')); } /** * Applies live IDs to the final result set, if * the current table is enabled for workspaces and * the keepLiveIds class member is enabled. * * @param int[] $ids * @return int[] * @internal */ public function applyLiveIds(array $ids) { $ids = $this->sanitizeIds($ids); if (!$this->keepLiveIds || !$this->isWorkspaceEnabled() || empty($ids)) { return $ids; } $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable($this->tableName); $queryBuilder->getRestrictions()->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); $result = $queryBuilder ->select('uid', 't3ver_oid') ->from($this->tableName) ->where( $queryBuilder->expr()->in( 'uid', $queryBuilder->createNamedParameter($ids, Connection::PARAM_INT_ARRAY) ) ) ->executeQuery(); $versionIds = []; while ($record = $result->fetchAssociative()) { $liveId = (int)$record['uid']; $versionIds[$liveId] = (int)$record['t3ver_oid']; } foreach ($ids as $id) { if (!empty($versionIds[$id])) { $ids[$id] = $versionIds[$id]; } } return $ids; } /** * Re-indexes the given IDs. * * @param int[] $ids * @return int[] */ protected function reindex(array $ids) { if (empty($ids)) { return $ids; } $ids = array_values($ids); $ids = array_combine($ids, $ids); return $ids; } /** * Removes empty values (null, '0', 0, false). * * @param int[] $ids */ protected function sanitizeIds(array $ids): array { return array_filter($ids); } /** * @return bool */ protected function isWorkspaceEnabled() { if (ExtensionManagementUtility::isLoaded('workspaces')) { $schemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class); return $schemaFactory->has($this->tableName) && $schemaFactory->get($this->tableName)->hasCapability(TcaSchemaCapability::Workspace); } return false; } }