From 57d3b0be8c8aa023d560dbd00d60a4a5f8c9faf3 Mon Sep 17 00:00:00 2001 From: Ingolf Steinhardt Date: Wed, 20 May 2026 21:25:52 +0200 Subject: [PATCH] Fix copy all languages --- phpunit.xml.dist | 33 +++----- src/Attribute/Base.php | 1 + .../ITranslatedWithFallbackControl.php | 2 + src/Attribute/TranslatedReference.php | 28 ++----- src/DcGeneral/Data/Model.php | 4 +- .../Events/MetaModel/CopyTranslatedData.php | 2 +- src/MetaModel.php | 66 ++++++++++++---- src/TranslatedMetaModel.php | 76 ++++++++++++++++++- tests/MetaModelsTest.php | 1 + 9 files changed, 149 insertions(+), 64 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index cd6578363..cbdf7ab34 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,25 +1,14 @@ - - - - ./tests - - - - - ./src - - + + + + ./tests + + + + + ./src + + diff --git a/src/Attribute/Base.php b/src/Attribute/Base.php index 6152b0680..8a197af4a 100644 --- a/src/Attribute/Base.php +++ b/src/Attribute/Base.php @@ -650,6 +650,7 @@ public function parseValue($arrRowData, $strOutputFormat = 'text', $objSettings try { $arrResult['text'] = $objTemplate->parse('text', true); } catch (\Exception $e) { + // FIXME: this throws when no parent has been set - need to catch! $objSettingsFallback = $this->getDefaultRenderSettings()->setParent($objSettings->getParent()); $objTemplate = new Template($objSettingsFallback->get('template') ?? ''); diff --git a/src/Attribute/ITranslatedWithFallbackControl.php b/src/Attribute/ITranslatedWithFallbackControl.php index 8049a72fe..71fb7b90f 100644 --- a/src/Attribute/ITranslatedWithFallbackControl.php +++ b/src/Attribute/ITranslatedWithFallbackControl.php @@ -25,6 +25,8 @@ * * This separate interface allows opt-in without breaking existing ITranslated implementors. * Consumers check instanceof before calling getTranslatedDataForWithoutFallback(). + * + * @deprecated Was a bad idea, sorry. */ interface ITranslatedWithFallbackControl extends ITranslated { diff --git a/src/Attribute/TranslatedReference.php b/src/Attribute/TranslatedReference.php index b8cf6e05c..70c53d7d8 100644 --- a/src/Attribute/TranslatedReference.php +++ b/src/Attribute/TranslatedReference.php @@ -38,7 +38,7 @@ * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ -abstract class TranslatedReference extends BaseComplex implements ITranslatedWithFallbackControl +abstract class TranslatedReference extends BaseComplex implements ITranslated { /** * Database connection. @@ -170,7 +170,6 @@ protected function getOptionizer() ]; } - /** * {@inheritDoc} */ @@ -189,9 +188,10 @@ public function valueToWidget($varValue) public function widgetToValue($varValue, $itemId) { return [ - 'tstamp' => \time(), - 'value' => $varValue, - 'att_id' => $this->get('id'), + 'tstamp' => \time(), + 'value' => $varValue, + 'att_id' => $this->get('id'), + 'item_id' => $itemId, ]; } @@ -455,24 +455,6 @@ protected function fetchExistingIdsFor($idList, $langCode) return $queryBuilder->executeQuery()->fetchFirstColumn(); } - /** - * {@inheritDoc} - */ - #[\Override] - public function getTranslatedDataForWithoutFallback(array $arrIds, string $strLangCode): array - { - return $this->getTranslatedDataFor($arrIds, $strLangCode); - } - - /** - * {@inheritDoc} - */ - #[\Override] - public function applyTranslatedDataFor(array $arrValues, string $strLangCode): void - { - $this->setTranslatedDataFor($arrValues, $strLangCode); - } - /** * {@inheritDoc} */ diff --git a/src/DcGeneral/Data/Model.php b/src/DcGeneral/Data/Model.php index fd992616e..30f53e842 100644 --- a/src/DcGeneral/Data/Model.php +++ b/src/DcGeneral/Data/Model.php @@ -3,7 +3,7 @@ /** * This file is part of MetaModels/core. * - * (c) 2012-2024 The MetaModels team. + * (c) 2012-2026 The MetaModels team. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -15,7 +15,7 @@ * @author Stefan Heimes * @author Sven Baumann * @author Ingolf Steinhardt - * @copyright 2012-2024 The MetaModels team. + * @copyright 2012-2026 The MetaModels team. * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ diff --git a/src/DcGeneral/Events/MetaModel/CopyTranslatedData.php b/src/DcGeneral/Events/MetaModel/CopyTranslatedData.php index 8b380e654..bed6dc1c6 100644 --- a/src/DcGeneral/Events/MetaModel/CopyTranslatedData.php +++ b/src/DcGeneral/Events/MetaModel/CopyTranslatedData.php @@ -98,7 +98,7 @@ private function copyLanguage( } $data = $attribute->getTranslatedDataForWithoutFallback([$sourceId], $language); - if ([] === $data || !isset($data[$sourceId])) { + if ([] === $data || !\array_key_exists($sourceId, $data)) { continue; } diff --git a/src/MetaModel.php b/src/MetaModel.php index 6ca89bd5a..4774d1f4e 100644 --- a/src/MetaModel.php +++ b/src/MetaModel.php @@ -759,7 +759,7 @@ public function isTranslated(bool $deprecation = true) #[\Override] public function hasVariants() { - return $this->arrData['varsupport']; + return $this->arrData['varsupport'] ?? false; } /** @@ -1240,22 +1240,13 @@ protected function saveAttribute($objAttribute, $arrIds, $varData, $strLangCode) protected function updateVariants($item, $activeLanguage, $allIds, $baseAttributes = false) { foreach ($this->getAttributes() as $strAttributeId => $objAttribute) { - // Skip unset attributes. - if (!$item->isAttributeSet($objAttribute->getColName())) { + if ($this->shouldSkipAttributeUpdate($item, $objAttribute, $baseAttributes)) { continue; } - if (!$baseAttributes && $item->isVariant() && !($objAttribute->get('isvariant'))) { - // Skip base attribute. - continue; - } - - if ($item->isVariantBase() && !($objAttribute->get('isvariant'))) { - // We have to override in variants. - $arrIds = $allIds; - } else { - $arrIds = array($item->get('id')); - } + $arrIds = ($item->isVariantBase() && !($objAttribute->get('isvariant'))) + ? $allIds + : [$item->get('id')]; $this->saveAttribute($objAttribute, $arrIds, $item->get($strAttributeId), $activeLanguage); } @@ -1443,6 +1434,53 @@ public function getView($intViewId = 0) return $this->getServiceContainer()->getRenderSettingFactory()->createCollection($this, (string) $intViewId); } + /** + * Determine whether the given attribute should be skipped during updateVariants(). + * + * @param IItem $item The item being saved. + * @param IAttribute $attribute The attribute to check. + * @param bool $baseAttributes Whether base attributes are included. + */ + protected function shouldSkipAttributeUpdate(IItem $item, IAttribute $attribute, bool $baseAttributes): bool + { + if (!$item->isAttributeSet($attribute->getColName())) { + return true; + } + if ($item instanceof IDirtyTracking && !$item->isDirty($attribute->getColName())) { + return true; + } + return !$baseAttributes && $item->isVariant() && !(bool) $attribute->get('isvariant'); + } + + /** + * Clear an attribute for the given ids - this only clears IComplex and ITranslated and throws for ISimple. + * + * @param IAttribute $attribute The attribute to save. + * @param array $idList The ids of the rows that shall be updated. + * @param string $langCode The language code to save. + * + * @throws \RuntimeException When an unsupported attribute type is encountered. + */ + protected function clearAttribute(IAttribute $attribute, array $idList, string $langCode): void + { + /** @var list $ids */ + $ids = array_values(array_map('strval', $idList)); + // Check for translated fields first, then for complex and save as simple then. + if ($langCode && $this->isTranslatedAttribute($attribute)) { + /** @var ITranslated $attribute */ + $attribute->unsetValueFor($ids, $langCode); + } elseif ($this->isComplexAttribute($attribute)) { + /** @var IComplex $attribute */ + // Complex saving. + $attribute->unsetDataFor($ids); + } else { + throw new \RuntimeException( + 'Unsupported attribute type, can not clear. Interfaces implemented: ' . + \implode(', ', (array) \class_implements($attribute)) + ); + } + } + private function getConnection(): Connection { return $this->connection; diff --git a/src/TranslatedMetaModel.php b/src/TranslatedMetaModel.php index 532f066dd..fd9f797ae 100644 --- a/src/TranslatedMetaModel.php +++ b/src/TranslatedMetaModel.php @@ -3,7 +3,7 @@ /** * This file is part of MetaModels/core. * - * (c) 2012-2024 The MetaModels team. + * (c) 2012-2026 The MetaModels team. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -13,7 +13,7 @@ * @package MetaModels/core * @author Christian Schiffler * @author Ingolf Steinhardt - * @copyright 2012-2024 The MetaModels team. + * @copyright 2012-2026 The MetaModels team. * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ @@ -21,6 +21,8 @@ namespace MetaModels; use Doctrine\DBAL\Connection; +use MetaModels\Attribute\IAttribute; +use MetaModels\Attribute\ISimple; use MetaModels\Attribute\ITranslated; use MetaModels\Helper\LocaleUtil; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -161,4 +163,74 @@ protected function fetchTranslatedAttributeValues(ITranslated $attribute, $ids) $GLOBALS['TL_LANGUAGE'] = LocaleUtil::formatAsLanguageTag($originalLanguage); } } + + /** + * Update the variants with the value if needed. + * + * @param IItem $item The item to save. + * @param string $activeLanguage The language the values are in. + * @param int[] $allIds The ids of all variants. + * @param bool $baseAttributes If also the base attributes get updated as well. + * + * @return void + */ + #[\Override] + protected function updateVariants($item, $activeLanguage, $allIds, $baseAttributes = false): void + { + $mainLanguage = $this->getMainLanguage(); + if ($mainLanguage === $activeLanguage) { + parent::updateVariants($item, $activeLanguage, $allIds, $baseAttributes); + return; + } + + $fallbackItem = $this->loadFallbackItem($item, $mainLanguage); + + foreach ($this->getAttributes() as $attributeName => $attribute) { + if ($this->shouldSkipAttributeUpdate($item, $attribute, $baseAttributes)) { + continue; + } + + $idList = ($item->isVariantBase() && !($attribute->get('isvariant'))) + ? $allIds + : [$item->get('id')]; + + if ($this->hasSameFallbackValue($item, $attribute, $attributeName, $fallbackItem)) { + $this->clearAttribute($attribute, $idList, $activeLanguage); + continue; + } + + $this->saveAttribute($attribute, $idList, $item->get($attributeName), $activeLanguage); + } + } + + /** + * Load the item in the main (fallback) language for comparison. + */ + private function loadFallbackItem(IItem $item, string $mainLanguage): ?IItem + { + $currentLanguage = $this->getLanguage(); + $this->selectLanguage($mainLanguage); + try { + return $this->getItemsWithId([$item->get('id')], $item->getSetAttributes())->getItem(); + } finally { + $this->selectLanguage($currentLanguage); + } + } + + /** + * Check whether the attribute value matches the fallback item value. + * Returns false for simple attributes or when no fallback item is available. + */ + private function hasSameFallbackValue( + IItem $item, + IAttribute $attribute, + string $attributeName, + ?IItem $fallbackItem + ): bool { + if ($attribute instanceof ISimple || null === $fallbackItem) { + return false; + } + return $attribute->valueToWidget($item->get($attributeName)) + === $attribute->valueToWidget($fallbackItem->get($attributeName)); + } } diff --git a/tests/MetaModelsTest.php b/tests/MetaModelsTest.php index 0c1229e48..474c9e858 100644 --- a/tests/MetaModelsTest.php +++ b/tests/MetaModelsTest.php @@ -449,6 +449,7 @@ public function testGetCountForNonEmptyList(): void self::assertEquals(4, $metaModel->getCount($metaModel->getEmptyFilter())); } + /** * Mock a database connection with hte passed query builders. *