TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
<?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\Core\DataHandling\Localization;
|
||||
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Entity for data-map item.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class DataMapItem
|
||||
{
|
||||
public const TYPE_PARENT = 'parent';
|
||||
public const TYPE_DIRECT_CHILD = 'directChild';
|
||||
public const TYPE_GRAND_CHILD = 'grandChild';
|
||||
|
||||
public const SCOPE_PARENT = State::STATE_PARENT;
|
||||
public const SCOPE_SOURCE = State::STATE_SOURCE;
|
||||
public const SCOPE_EXCLUDE = 'exclude';
|
||||
|
||||
protected string $tableName;
|
||||
protected string|int $id;
|
||||
protected int $liveId;
|
||||
protected array $suggestedValues;
|
||||
protected array $persistedValues;
|
||||
protected array $configurationFieldNames;
|
||||
protected bool $new;
|
||||
protected ?string $type = null;
|
||||
protected ?State $state = null;
|
||||
protected string|int $language = 0;
|
||||
protected string|int $parent = '';
|
||||
protected string|int|null $source = null;
|
||||
/** @var DataMapItem[][] */
|
||||
protected array $dependencies = [];
|
||||
|
||||
/**
|
||||
* Builds a data-map item. In addition to the constructor, the values
|
||||
* for language, parent and source record pointers are assigned as well.
|
||||
*/
|
||||
public static function build(
|
||||
string $tableName,
|
||||
string|int $id,
|
||||
array $suggestedValues,
|
||||
array $persistedValues,
|
||||
array $configurationFieldNames
|
||||
): static {
|
||||
$item = GeneralUtility::makeInstance(
|
||||
static::class,
|
||||
$tableName,
|
||||
$id,
|
||||
$suggestedValues,
|
||||
$persistedValues,
|
||||
$configurationFieldNames
|
||||
);
|
||||
|
||||
// assign language specific settings of item
|
||||
$item->language = (int)($suggestedValues[$item->getLanguageFieldName()] ?? $persistedValues[$item->getLanguageFieldName()] ?? 0);
|
||||
$item->setParent($suggestedValues[$item->getParentFieldName()] ?? $persistedValues[$item->getParentFieldName()] ?? '');
|
||||
if ($item->getSourceFieldName() !== null) {
|
||||
$item->setSource($suggestedValues[$item->getSourceFieldName()] ?? $persistedValues[$item->getSourceFieldName()] ?? '');
|
||||
}
|
||||
// assign live-id of item if available
|
||||
$versionSourceFieldName = $item->getVersionSourceFieldName();
|
||||
if ($versionSourceFieldName !== null && !empty($persistedValues[$versionSourceFieldName])) {
|
||||
$item->liveId = $persistedValues[$versionSourceFieldName];
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
|
||||
public function __construct(
|
||||
string $tableName,
|
||||
string|int $id,
|
||||
array $suggestedValues,
|
||||
array $persistedValues,
|
||||
array $configurationFieldNames
|
||||
) {
|
||||
$this->tableName = $tableName;
|
||||
$this->id = $id;
|
||||
|
||||
$this->suggestedValues = $suggestedValues;
|
||||
$this->persistedValues = $persistedValues;
|
||||
$this->configurationFieldNames = $configurationFieldNames;
|
||||
|
||||
$this->new = !MathUtility::canBeInterpretedAsInteger($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current table name of this data-map item.
|
||||
*/
|
||||
public function getTableName(): string
|
||||
{
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the id of this data-map item.
|
||||
*/
|
||||
public function getId(): string|int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getLiveId(): int|string
|
||||
{
|
||||
return $this->liveId ?? $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the suggested values that were initially
|
||||
* submitted as the whole data-map to the DataHandler.
|
||||
*/
|
||||
public function getSuggestedValues(): array
|
||||
{
|
||||
return $this->suggestedValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the persisted values that represent the persisted state
|
||||
* of the record this data-map item is a surrogate for - does only
|
||||
* contain relevant field values.
|
||||
*/
|
||||
public function getPersistedValues(): array
|
||||
{
|
||||
return $this->persistedValues;
|
||||
}
|
||||
|
||||
public function getConfigurationFieldNames(): array
|
||||
{
|
||||
return $this->configurationFieldNames;
|
||||
}
|
||||
|
||||
public function getLanguageFieldName(): string
|
||||
{
|
||||
return $this->configurationFieldNames['language'];
|
||||
}
|
||||
|
||||
public function getParentFieldName(): string
|
||||
{
|
||||
return $this->configurationFieldNames['parent'];
|
||||
}
|
||||
|
||||
public function getSourceFieldName(): ?string
|
||||
{
|
||||
return $this->configurationFieldNames['source'] ?? null;
|
||||
}
|
||||
|
||||
protected function getVersionSourceFieldName(): ?string
|
||||
{
|
||||
return $this->configurationFieldNames['versionSource'] ?? null;
|
||||
}
|
||||
|
||||
public function isNew(): bool
|
||||
{
|
||||
return $this->new;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
if ($this->type === null) {
|
||||
// implicit: default language, it's a parent
|
||||
if ($this->language === 0) {
|
||||
$this->type = static::TYPE_PARENT;
|
||||
} elseif (
|
||||
// implicit: having source value different to parent value, it's a 2nd or higher level translation
|
||||
$this->source !== 0
|
||||
&& $this->source !== null
|
||||
&& $this->source !== $this->parent
|
||||
) {
|
||||
$this->type = static::TYPE_GRAND_CHILD;
|
||||
} else {
|
||||
// implicit: otherwise, it's a 1st level translation
|
||||
$this->type = static::TYPE_DIRECT_CHILD;
|
||||
}
|
||||
}
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function isParentType(): bool
|
||||
{
|
||||
return $this->getType() === static::TYPE_PARENT;
|
||||
}
|
||||
|
||||
public function isDirectChildType(): bool
|
||||
{
|
||||
return $this->getType() === static::TYPE_DIRECT_CHILD;
|
||||
}
|
||||
|
||||
public function isGrandChildType(): bool
|
||||
{
|
||||
return $this->getType() === static::TYPE_GRAND_CHILD;
|
||||
}
|
||||
|
||||
public function getState(): ?State
|
||||
{
|
||||
if ($this->state === null && !$this->isParentType()) {
|
||||
$this->state = $this->buildState();
|
||||
}
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
public function getLanguage(): string|int
|
||||
{
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
public function setLanguage(string|int $language): void
|
||||
{
|
||||
$this->language = $language;
|
||||
}
|
||||
|
||||
public function getParent(): string|int
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
public function setParent(string|int $parent): void
|
||||
{
|
||||
$this->parent = $this->extractId($parent);
|
||||
}
|
||||
|
||||
public function getSource(): string|int|null
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
public function setSource(string|int $source): void
|
||||
{
|
||||
$this->source = $this->extractId($source);
|
||||
}
|
||||
|
||||
public function getIdForScope(string $scope): string|int
|
||||
{
|
||||
if (
|
||||
$scope === static::SCOPE_PARENT
|
||||
|| $scope === static::SCOPE_EXCLUDE
|
||||
) {
|
||||
return $this->getParent();
|
||||
}
|
||||
if ($scope === static::SCOPE_SOURCE) {
|
||||
// $source is guaranteed to be non-null when SCOPE_SOURCE is applicable:
|
||||
// getApplicableScopes() only includes it when getSourceFieldName() !== null,
|
||||
// which means setSource() was called during build().
|
||||
return $this->source;
|
||||
}
|
||||
throw new \RuntimeException('Invalid scope', 1486325248);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataMapItem[][]
|
||||
*/
|
||||
public function getDependencies(): array
|
||||
{
|
||||
return $this->dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataMapItem[][] $dependencies
|
||||
*/
|
||||
public function setDependencies(array $dependencies): void
|
||||
{
|
||||
$this->dependencies = $dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataMapItem[]
|
||||
*/
|
||||
public function findDependencies(string $scope): array
|
||||
{
|
||||
return $this->dependencies[$scope] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getApplicableScopes(): array
|
||||
{
|
||||
$scopes = [];
|
||||
if (!empty($this->getSourceFieldName())) {
|
||||
$scopes[] = static::SCOPE_SOURCE;
|
||||
}
|
||||
$scopes[] = static::SCOPE_PARENT;
|
||||
$scopes[] = static::SCOPE_EXCLUDE;
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts real id from provided id-value, which can either be a real
|
||||
* integer value, a 'NEW...' id, or a combined identifier 'tt_content_13'.
|
||||
*/
|
||||
protected function extractId(int|string $idValue): int|string
|
||||
{
|
||||
if (MathUtility::canBeInterpretedAsInteger($idValue)) {
|
||||
return $idValue;
|
||||
}
|
||||
$idValue = (string)$idValue;
|
||||
if (str_starts_with($idValue, 'NEW')) {
|
||||
return $idValue;
|
||||
}
|
||||
// @todo Handle if $tableName does not match $this->tableName
|
||||
$id = BackendUtility::splitTable_Uid($idValue)[1];
|
||||
return $id;
|
||||
}
|
||||
|
||||
protected function buildState(): ?State
|
||||
{
|
||||
// build from persisted states
|
||||
if (!$this->isNew()) {
|
||||
$state = State::fromJSON(
|
||||
$this->tableName,
|
||||
$this->persistedValues['l10n_state'] ?? null
|
||||
);
|
||||
} elseif (is_string($this->suggestedValues['l10n_state'] ?? null)) {
|
||||
// use provided states for a new and copied element
|
||||
$state = State::fromJSON(
|
||||
$this->tableName,
|
||||
$this->suggestedValues['l10n_state']
|
||||
);
|
||||
} else {
|
||||
// provide the default states
|
||||
$state = State::create($this->tableName);
|
||||
}
|
||||
// switch "custom" to "source" state for 2nd level translations
|
||||
if ($this->isNew() && $this->isGrandChildType()) {
|
||||
$state->updateStates(State::STATE_CUSTOM, State::STATE_SOURCE);
|
||||
}
|
||||
// apply any provided updates to the states
|
||||
if (is_array($this->suggestedValues['l10n_state'] ?? null)) {
|
||||
$state->update($this->suggestedValues['l10n_state']);
|
||||
}
|
||||
return $state;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,240 @@
|
||||
<?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\Core\DataHandling\Localization;
|
||||
|
||||
use TYPO3\CMS\Core\Schema\Field\FieldTypeInterface;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Value object for l10n_state field value.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class State
|
||||
{
|
||||
public const STATE_CUSTOM = 'custom';
|
||||
public const STATE_PARENT = 'parent';
|
||||
public const STATE_SOURCE = 'source';
|
||||
|
||||
protected const VALID_STATES = [
|
||||
self::STATE_CUSTOM,
|
||||
self::STATE_SOURCE,
|
||||
self::STATE_PARENT,
|
||||
];
|
||||
|
||||
protected string $tableName;
|
||||
protected array $states;
|
||||
protected array $originalStates;
|
||||
|
||||
public static function create(string $tableName): ?State
|
||||
{
|
||||
if (!static::isApplicable($tableName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return GeneralUtility::makeInstance(
|
||||
static::class,
|
||||
$tableName
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromJSON(string $tableName, ?string $json = null): ?State
|
||||
{
|
||||
if (!static::isApplicable($tableName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$states = json_decode($json ?? '', true);
|
||||
return GeneralUtility::makeInstance(
|
||||
static::class,
|
||||
$tableName,
|
||||
$states ?? []
|
||||
);
|
||||
}
|
||||
|
||||
public static function isApplicable(string $tableName): bool
|
||||
{
|
||||
$schemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class);
|
||||
return $schemaFactory->has($tableName)
|
||||
&& $schemaFactory->get($tableName)->isLanguageAware()
|
||||
&& count(static::getFieldNames($tableName)) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function getFieldNames(string $tableName): array
|
||||
{
|
||||
$schemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class);
|
||||
if (!$schemaFactory->has($tableName)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(
|
||||
static fn(FieldTypeInterface $field) => $field->getName(),
|
||||
iterator_to_array(
|
||||
$schemaFactory->get($tableName)->getFields(
|
||||
static fn(FieldTypeInterface $field): bool => !empty($field->getConfiguration()['behaviour']['allowLanguageSynchronization'])
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function __construct(string $tableName, array $states = [])
|
||||
{
|
||||
$this->tableName = $tableName;
|
||||
$this->states = $states;
|
||||
$this->originalStates = $states;
|
||||
|
||||
$this->states = $this->enrich(
|
||||
$this->sanitize($states)
|
||||
);
|
||||
}
|
||||
|
||||
public function update(array $states): void
|
||||
{
|
||||
$this->states = array_merge(
|
||||
$this->states,
|
||||
$this->sanitize($states)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates field names having a particular state to a target state.
|
||||
*/
|
||||
public function updateStates(string $currentState, string $targetState): void
|
||||
{
|
||||
$states = [];
|
||||
foreach ($this->filterFieldNames($currentState) as $fieldName) {
|
||||
$states[$fieldName] = $targetState;
|
||||
}
|
||||
if (!empty($states)) {
|
||||
$this->update($states);
|
||||
}
|
||||
}
|
||||
|
||||
public function export(): string|false|null
|
||||
{
|
||||
if (empty($this->states)) {
|
||||
return null;
|
||||
}
|
||||
return json_encode($this->states);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->states;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getModifiedFieldNames(): array
|
||||
{
|
||||
return array_keys(
|
||||
array_diff_assoc(
|
||||
$this->states,
|
||||
$this->originalStates
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function isModified(): bool
|
||||
{
|
||||
return !empty($this->getModifiedFieldNames());
|
||||
}
|
||||
|
||||
public function isUndefined(string $fieldName): bool
|
||||
{
|
||||
return !isset($this->states[$fieldName]);
|
||||
}
|
||||
|
||||
public function isCustomState(string $fieldName): bool
|
||||
{
|
||||
return ($this->states[$fieldName] ?? null) === static::STATE_CUSTOM;
|
||||
}
|
||||
|
||||
public function isParentState(string $fieldName): bool
|
||||
{
|
||||
return ($this->states[$fieldName] ?? null) === static::STATE_PARENT;
|
||||
}
|
||||
|
||||
public function isSourceState(string $fieldName): bool
|
||||
{
|
||||
return ($this->states[$fieldName] ?? null) === static::STATE_SOURCE;
|
||||
}
|
||||
|
||||
public function getState(string $fieldName): ?string
|
||||
{
|
||||
return $this->states[$fieldName] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters field names having a desired state.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function filterFieldNames(string $desiredState, bool $modified = false): array
|
||||
{
|
||||
if (!$modified) {
|
||||
$fieldNames = array_keys($this->states);
|
||||
} else {
|
||||
$fieldNames = $this->getModifiedFieldNames();
|
||||
}
|
||||
return array_filter(
|
||||
$fieldNames,
|
||||
function (string $fieldName) use ($desiredState): bool {
|
||||
return $this->states[$fieldName] === $desiredState;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out field names that don't exist in TCA.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function sanitize(array $states): array
|
||||
{
|
||||
$fieldNames = static::getFieldNames($this->tableName);
|
||||
return array_intersect_key(
|
||||
$states,
|
||||
array_combine($fieldNames, $fieldNames) ?: []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add missing states for field names.
|
||||
*/
|
||||
protected function enrich(array $states): array
|
||||
{
|
||||
foreach (static::getFieldNames($this->tableName) as $fieldName) {
|
||||
$isValid = in_array(
|
||||
$states[$fieldName] ?? null,
|
||||
static::VALID_STATES,
|
||||
true
|
||||
);
|
||||
if ($isValid) {
|
||||
continue;
|
||||
}
|
||||
$states[$fieldName] = static::STATE_PARENT;
|
||||
}
|
||||
return $states;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?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\Core\DataHandling\Localization;
|
||||
|
||||
use TYPO3\CMS\Core\DataHandling\PlainDataResolver;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Version to live id map.
|
||||
*
|
||||
* This is on a per-workspace and per-table setup, so it is recommended
|
||||
* to use a runtime cache to hold these instances.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class VersionToLiveIdMap
|
||||
{
|
||||
protected string $tableName;
|
||||
protected int $workspaceId;
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
protected array $map = [];
|
||||
|
||||
public function __construct(string $tableName, int $workspaceId)
|
||||
{
|
||||
$this->tableName = $tableName;
|
||||
$this->workspaceId = $workspaceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $ids
|
||||
*/
|
||||
public function update(array $ids): self
|
||||
{
|
||||
$ids = array_map(intval(...), $ids);
|
||||
$candidateIds = array_diff(
|
||||
$ids,
|
||||
array_keys($this->map),
|
||||
array_values($this->map)
|
||||
);
|
||||
|
||||
if (empty($candidateIds)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$candidateIdMap = array_combine($candidateIds, $candidateIds);
|
||||
$schemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class);
|
||||
if (
|
||||
!$schemaFactory->has($this->tableName)
|
||||
|| $this->workspaceId === 0
|
||||
|| !$schemaFactory->get($this->tableName)->isWorkspaceAware()
|
||||
) {
|
||||
$this->map += $candidateIdMap;
|
||||
return $this;
|
||||
}
|
||||
|
||||
$plainDataResolver = GeneralUtility::makeInstance(
|
||||
PlainDataResolver::class,
|
||||
$this->tableName,
|
||||
[]
|
||||
);
|
||||
$plainDataResolver->setWorkspaceId($this->workspaceId);
|
||||
$plainDataResolver->setKeepLiveIds(true);
|
||||
$versionLiveIdMap = $plainDataResolver->applyLiveIds($candidateIdMap);
|
||||
$this->map += $versionLiveIdMap;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getVersionId(int $liveId): int
|
||||
{
|
||||
$versionId = array_search($liveId, $this->map, true);
|
||||
return $versionId ?: $liveId;
|
||||
}
|
||||
|
||||
public function getLiveId(int $versionId): string|int
|
||||
{
|
||||
return $this->map[$versionId] ?? $versionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $versionIds
|
||||
* @return int[]
|
||||
*/
|
||||
public function getLiveIds(array $versionIds): array
|
||||
{
|
||||
return array_map(
|
||||
function (int $versionId) {
|
||||
return $this->getLiveId($versionId);
|
||||
},
|
||||
$versionIds
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user