397 lines
15 KiB
PHP
397 lines
15 KiB
PHP
<?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;
|
|
}
|
|
}
|