Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 11 additions & 22 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.1/phpunit.xsd"
colors="true"
forceCoversAnnotation="true"
failOnRisky="true"
failOnWarning="true"
beStrictAboutChangesToGlobalState="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutResourceUsageDuringSmallTests="true"
beStrictAboutTodoAnnotatedTests="true"
bootstrap="vendor/autoload.php"
>
<testsuites>
<testsuite name="MetaModels core tests">
<directory>./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src</directory>
</whitelist>
</filter>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" colors="true" failOnRisky="true" failOnWarning="true" beStrictAboutChangesToGlobalState="true" beStrictAboutOutputDuringTests="true" bootstrap="vendor/autoload.php" cacheDirectory=".phpunit.cache" requireCoverageMetadata="true">
<testsuites>
<testsuite name="MetaModels core tests">
<directory>./tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>./src</directory>
</include>
</source>
</phpunit>
1 change: 1 addition & 0 deletions src/Attribute/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -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') ?? '');
Expand Down
2 changes: 2 additions & 0 deletions src/Attribute/ITranslatedWithFallbackControl.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
28 changes: 5 additions & 23 deletions src/Attribute/TranslatedReference.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
*
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
*/
abstract class TranslatedReference extends BaseComplex implements ITranslatedWithFallbackControl
abstract class TranslatedReference extends BaseComplex implements ITranslated
{
/**
* Database connection.
Expand Down Expand Up @@ -170,7 +170,6 @@ protected function getOptionizer()
];
}


/**
* {@inheritDoc}
*/
Expand All @@ -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,
];
}

Expand Down Expand Up @@ -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}
*/
Expand Down
4 changes: 2 additions & 2 deletions src/DcGeneral/Data/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -15,7 +15,7 @@
* @author Stefan Heimes <stefan_heimes@hotmail.com>
* @author Sven Baumann <baumann.sv@gmail.com>
* @author Ingolf Steinhardt <info@e-spin.de>
* @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
*/
Expand Down
2 changes: 1 addition & 1 deletion src/DcGeneral/Events/MetaModel/CopyTranslatedData.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
66 changes: 52 additions & 14 deletions src/MetaModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@
#[\Override]
public function hasVariants()
{
return $this->arrData['varsupport'];
return $this->arrData['varsupport'] ?? false;
}

/**
Expand Down Expand Up @@ -1240,22 +1240,13 @@
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);
}
Expand Down Expand Up @@ -1443,6 +1434,53 @@
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())) {

Check failure on line 1449 in src/MetaModel.php

View workflow job for this annotation

GitHub Actions / PHP: 8.3 Contao: ~5.3.0

UndefinedClass: Class, interface or enum named MetaModels\IDirtyTracking does not exist (reported by psalm)

Check failure on line 1449 in src/MetaModel.php

View workflow job for this annotation

GitHub Actions / PHP: 8.2 Contao: ~5.3.0

UndefinedClass: Class, interface or enum named MetaModels\IDirtyTracking does not exist (reported by psalm)

Check failure on line 1449 in src/MetaModel.php

View workflow job for this annotation

GitHub Actions / PHP: 8.2 Contao: ~5.3.0

UndefinedClass: Class, interface or enum named MetaModels\IDirtyTracking does not exist (reported by psalm)

Check failure on line 1449 in src/MetaModel.php

View workflow job for this annotation

GitHub Actions / PHP: 8.3 Contao: ~5.3.0

UndefinedClass: Class, interface or enum named MetaModels\IDirtyTracking does not exist (reported by psalm)
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<string> $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;
Expand Down
76 changes: 74 additions & 2 deletions src/TranslatedMetaModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -13,14 +13,16 @@
* @package MetaModels/core
* @author Christian Schiffler <c.schiffler@cyberspectrum.de>
* @author Ingolf Steinhardt <info@e-spin.de>
* @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
*/

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;
Expand Down Expand Up @@ -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));
}
}
1 change: 1 addition & 0 deletions tests/MetaModelsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ public function testGetCountForNonEmptyList(): void
self::assertEquals(4, $metaModel->getCount($metaModel->getEmptyFilter()));
}


/**
* Mock a database connection with hte passed query builders.
*
Expand Down
Loading