0) * - It only deals with RawRecord (not other Records) as we are usually interested in the raw values * - It has no dependency on $GLOBALS['BE_USER'] */ #[Autoconfigure(public: true)] readonly class LocalizationRepository { public function __construct( protected TcaSchemaFactory $tcaSchemaFactory, protected RecordFactory $recordFactory, ) {} /** * Get records for copy process */ public function getRecordsToCopyDatabaseResult(int $pageId, int $destLanguageId, int $languageId, int $workspaceId = 0): Result { $originalUids = []; // Get original uid of existing elements triggered language $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content'); $queryBuilder->getRestrictions() ->removeAll() ->add(new DeletedRestriction()) ->add(new WorkspaceRestriction($workspaceId)); $originalUidsStatement = $queryBuilder ->select('l10n_source') ->from('tt_content') ->where( $queryBuilder->expr()->eq( 'sys_language_uid', $queryBuilder->createNamedParameter($destLanguageId, Connection::PARAM_INT) ), $queryBuilder->expr()->eq( 'pid', $queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT) ) ) ->executeQuery(); while ($origUid = $originalUidsStatement->fetchOne()) { $originalUids[] = (int)$origUid; } $queryBuilder ->select('*') ->from('tt_content') ->where( $queryBuilder->expr()->eq( 'sys_language_uid', $queryBuilder->createNamedParameter($languageId, Connection::PARAM_INT) ), $queryBuilder->expr()->eq( 'pid', $queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT) ) ) ->orderBy('sorting'); if ($originalUids !== []) { $queryBuilder ->andWhere( $queryBuilder->expr()->notIn( 'uid', $queryBuilder->createNamedParameter($originalUids, Connection::PARAM_INT_ARRAY) ) ); } return $queryBuilder->executeQuery(); } /** * Fetches the translated version of a record. * It automatically applies workspace overlay and filters out DELETE_PLACEHOLDER records. * * @param string|TcaSchema $tableOrSchema The table name or TCA schema * @param int|array|RecordInterface $recordOrUid The record UID, or the full record (array or RecordInterface). * When passing the full record, the pid is automatically used for filtering. * @param int|LanguageAspect $language The target language ID or LanguageAspect * @return RawRecord|null The translated record or null if not found */ public function getRecordTranslation( string|TcaSchema $tableOrSchema, int|array|RecordInterface $recordOrUid, int|LanguageAspect $language, int $workspaceId = 0, bool $includeDeletedRecords = false, ): ?RawRecord { // Resolve table name and schema if ($tableOrSchema instanceof TcaSchema) { $table = $tableOrSchema->getName(); $schema = $tableOrSchema; } else { $table = $tableOrSchema; if (!$this->tcaSchemaFactory->has($table)) { return null; } $schema = $this->tcaSchemaFactory->get($table); } if (!$schema->isLanguageAware()) { return null; } // Resolve uid and optional pid from record if ($recordOrUid instanceof RecordInterface) { $uid = $recordOrUid->getUid(); $pid = $recordOrUid->getPid(); } elseif (is_array($recordOrUid)) { $uid = (int)($recordOrUid['uid'] ?? 0); $pid = isset($recordOrUid['pid']) ? (int)$recordOrUid['pid'] : null; } else { $uid = $recordOrUid; $pid = null; } if ($uid === 0) { return null; } // Resolve language ID $languageId = $language instanceof LanguageAspect ? $language->getId() : $language; $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); $queryBuilder->getRestrictions()->removeAll(); if (!$includeDeletedRecords) { $queryBuilder->getRestrictions()->add(new DeletedRestriction()); } $queryBuilder->getRestrictions()->add(new WorkspaceRestriction($workspaceId)); // Prefer translationSourceField (l10n_source) over transOrigPointerField (l10n_parent) $parentPointerField = $languageCapability->hasTranslationSourceField() ? $languageCapability->getTranslationSourceField()->getName() : $languageCapability->getTranslationOriginPointerField()->getName(); $queryBuilder ->select('*') ->from($table) ->where( $queryBuilder->expr()->eq( $parentPointerField, $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT) ), $queryBuilder->expr()->eq( $languageCapability->getLanguageField()->getName(), $queryBuilder->createNamedParameter($languageId, Connection::PARAM_INT) ) ) ->setMaxResults(1); // When a full record is provided, automatically filter by pid if ($pid !== null) { $queryBuilder->andWhere( $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)) ); } $row = $queryBuilder->executeQuery()->fetchAssociative(); if ($row === false) { return null; } // Apply workspace overlay BackendUtility::workspaceOL($table, $row, $workspaceId); if (!is_array($row) || VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER) { return null; } return $this->recordFactory->createRawRecord($table, $row); } /** * Fetches all translations of a record. * It automatically applies workspace overlay and filters out DELETE_PLACEHOLDER records. * * @param string|TcaSchema $tableOrSchema The table name or TCA schema * @param int|array|RecordInterface $recordOrUid The record UID, or the full record (array or RecordInterface). * When passing the full record, the pid is automatically used for filtering. * @param array $limitToLanguageIds Optional list of language IDs to filter by * @return RawRecord[] Array of translated records indexed by language ID */ public function getRecordTranslations( string|TcaSchema $tableOrSchema, int|array|RecordInterface $recordOrUid, array $limitToLanguageIds = [], int $workspaceId = 0, bool $includeDeletedRecords = false, ): array { // Resolve table name and schema if ($tableOrSchema instanceof TcaSchema) { $table = $tableOrSchema->getName(); $schema = $tableOrSchema; } else { $table = $tableOrSchema; if (!$this->tcaSchemaFactory->has($table)) { return []; } $schema = $this->tcaSchemaFactory->get($table); } if (!$schema->isLanguageAware()) { return []; } // Resolve uid and optional pid from record if ($recordOrUid instanceof RecordInterface) { $uid = $recordOrUid->getUid(); $pid = $recordOrUid->getPid(); } elseif (is_array($recordOrUid)) { $uid = (int)($recordOrUid['uid'] ?? 0); $pid = isset($recordOrUid['pid']) ? (int)$recordOrUid['pid'] : null; } else { $uid = $recordOrUid; $pid = null; } if ($uid === 0) { return []; } $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); $languageFieldName = $languageCapability->getLanguageField()->getName(); // Prefer translationSourceField (l10n_source) over transOrigPointerField (l10n_parent) $parentPointerField = $languageCapability->hasTranslationSourceField() ? $languageCapability->getTranslationSourceField()->getName() : $languageCapability->getTranslationOriginPointerField()->getName(); $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); $queryBuilder->getRestrictions()->removeAll(); if (!$includeDeletedRecords) { $queryBuilder->getRestrictions()->add(new DeletedRestriction()); } $queryBuilder->getRestrictions()->add(new WorkspaceRestriction($workspaceId)); $queryBuilder ->select('*') ->from($table) ->where( $queryBuilder->expr()->eq( $parentPointerField, $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT) ), $queryBuilder->expr()->gt( $languageFieldName, $queryBuilder->createNamedParameter(0, Connection::PARAM_INT) ) ); // When a full record is provided, automatically filter by pid if ($pid !== null) { $queryBuilder->andWhere( $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)) ); } if ($limitToLanguageIds !== []) { $queryBuilder->andWhere( $queryBuilder->expr()->in( $languageFieldName, $queryBuilder->createNamedParameter($limitToLanguageIds, Connection::PARAM_INT_ARRAY) ) ); } $result = $queryBuilder->executeQuery(); $records = []; while ($row = $result->fetchAssociative()) { BackendUtility::workspaceOL($table, $row, $workspaceId); if (is_array($row) && VersionState::tryFrom($row['t3ver_state'] ?? 0) !== VersionState::DELETE_PLACEHOLDER) { $records[(int)$row[$languageFieldName]] = $this->recordFactory->createRawRecord($table, $row); } } return $records; } /** * Fetches all existing page translations for a given page. * It automatically applies workspace overlay and filters out DELETE_PLACEHOLDER records. * * @param int $pageUid The UID of the default language page * @param array $limitToLanguageIds Optional list of language IDs to filter by * @return RawRecord[] Array of page translation records indexed by language ID */ public function getPageTranslations( int $pageUid, array $limitToLanguageIds = [], int $workspaceId = 0, bool $includeDeletedRecords = false, ): array { if ($pageUid === 0) { return []; } if (!$this->tcaSchemaFactory->has('pages')) { return []; } $schema = $this->tcaSchemaFactory->get('pages'); if (!$schema->isLanguageAware()) { return []; } $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); $languageFieldName = $languageCapability->getLanguageField()->getName(); $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); $queryBuilder->getRestrictions()->removeAll(); if (!$includeDeletedRecords) { $queryBuilder->getRestrictions()->add(new DeletedRestriction()); } $queryBuilder->getRestrictions()->add(new WorkspaceRestriction($workspaceId)); $queryBuilder ->select('*') ->from('pages') ->where( $queryBuilder->expr()->eq( $languageCapability->getTranslationOriginPointerField()->getName(), $queryBuilder->createNamedParameter($pageUid, Connection::PARAM_INT) ) ); if ($limitToLanguageIds !== []) { $queryBuilder->andWhere( $queryBuilder->expr()->in( $languageFieldName, $queryBuilder->createNamedParameter($limitToLanguageIds, Connection::PARAM_INT_ARRAY) ) ); } $result = $queryBuilder->executeQuery(); $records = []; while ($row = $result->fetchAssociative()) { BackendUtility::workspaceOL('pages', $row, $workspaceId); if (is_array($row) && VersionState::tryFrom($row['t3ver_state'] ?? 0) !== VersionState::DELETE_PLACEHOLDER) { $records[(int)$row[$languageFieldName]] = $this->recordFactory->createRawRecord('pages', $row); } } return $records; } }