TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:00 +02:00
commit f9941541b7
1178 changed files with 135377 additions and 0 deletions
@@ -0,0 +1,396 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Domain\Repository\Localization;
use Doctrine\DBAL\Result;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Context\LanguageAspect;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Domain\RawRecord;
use TYPO3\CMS\Core\Domain\RecordFactory;
use TYPO3\CMS\Core\Domain\RecordInterface;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchema;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Versioning\VersionState;
/**
* Repository for having low-level logic fetching "record translation" dealing with
* for the TYPO3 Backend!
*
* A few rules for this class:
* - It only returns translated records (having l10n_parent / l10n_source > 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<int> $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<int> $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;
}
}