From 7b765e265db5d4bc5fb3dce3bb4992a6d8d53fc5 Mon Sep 17 00:00:00 2001 From: Jonathan LELIEVRE Date: Mon, 16 Dec 2024 12:19:01 +0100 Subject: [PATCH] Handle denormalization of localized values based on locale --- .../Normalizer/CQRSApiNormalizer.php | 49 ++++++++++-- .../Serializer/DomainSerializerTest.php | 78 ++++++++++++++----- 2 files changed, 104 insertions(+), 23 deletions(-) diff --git a/src/PrestaShopBundle/ApiPlatform/Normalizer/CQRSApiNormalizer.php b/src/PrestaShopBundle/ApiPlatform/Normalizer/CQRSApiNormalizer.php index e25d7d962e5f..d880b3b4aab3 100644 --- a/src/PrestaShopBundle/ApiPlatform/Normalizer/CQRSApiNormalizer.php +++ b/src/PrestaShopBundle/ApiPlatform/Normalizer/CQRSApiNormalizer.php @@ -51,10 +51,10 @@ * our CQRS <-> ApiPlatform conversion: * - handle getters that match the property without starting by get, has, is * - set appropriate context for the ValueObjectNormalizer for when we don't want a ValueObject but the scalar value to be used - * - converts localized values keys in the arrays: + * - converts localized values keys in the arrays on properties that have been flagged as LocalizedValue: * - the input is indexed by locale ['fr-FR' => 'Nom de la valeur', 'en-US' => 'Value name'] * - the data is normalized and indexed by locale ID [1 => 'Nom de la valeur', 2 => 'Value name'] - * - reversely localized data indexed by IDs are converted into an array localized by locale + * - reversely localized data indexed by IDs are converted into an array localized by locale during denormalization * - handle setter methods that use multiple parameters * - handle casting of boolean values */ @@ -157,8 +157,8 @@ protected function extractAttributes(object $object, ?string $format = null, arr } /** - * This method is overridden, so we can dynamically change the localized properties identified by a context or the LocalizedValue - * helper attribute. The used key that are based on Language's locale are automatically converted to rely on Language's ID. + * This method is overridden in order to dynamically change the localized properties identified by a context or the LocalizedValue + * helper attribute. The used key that are based on Language's locale are automatically converted to rely on Language's database ID. */ protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed { @@ -170,6 +170,19 @@ protected function getAttributeValue(object $object, string $attribute, ?string return $attributeValue; } + /** + * This method is overridden in order to dynamically change the localized properties identified by a context or the LocalizedValue + * helper attribute. he used key that are based on Language's database ID are automatically converted to rely on Language's locale. + */ + protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = []) + { + if (($context[LocalizedValue::IS_LOCALIZED_VALUE] ?? false) && is_array($value)) { + $value = $this->updateLanguageIndexesWithLocales($value); + } + + parent::setAttributeValue($object, $attribute, $value, $format, $context); + } + /** * Call all the method with multiple arguments and remove the data from the normalized data since it has already been denormalized into * the object. @@ -276,7 +289,7 @@ protected function findMethodsWithMultipleArguments(ReflectionClass $reflectionC } /** - * Return the localized array with keys based on local string value transformed into integer database IDs. + * Return the localized array with keys based on locale string value transformed into integer database IDs. * * @param array $localizedValue * @@ -301,6 +314,32 @@ protected function updateLanguageIndexesWithIDs(array $localizedValue): array return $indexLocalizedValue; } + /** + * Return the localized array with keys based on integer database IDs transformed into locale string values. + * + * @param array $localizedValue + * + * @return array + * + * @throws LocaleNotFoundException + */ + protected function updateLanguageIndexesWithLocales(array $localizedValue): array + { + $localeLocalizedValue = []; + $this->fetchLanguagesMapping(); + foreach ($localizedValue as $localeId => $localeValue) { + if (is_numeric($localeId)) { + if (!isset($this->localesByID[$localeId])) { + throw new LocaleNotFoundException('Locale with ID "' . $localeId . '" not found.'); + } + + $localeLocalizedValue[$this->localesByID[$localeId]] = $localeValue; + } + } + + return $localeLocalizedValue; + } + /** * Fetches the language mapping once and save them in local property for better performance. * diff --git a/tests/Integration/ApiPlatform/Serializer/DomainSerializerTest.php b/tests/Integration/ApiPlatform/Serializer/DomainSerializerTest.php index 1826600ae6b9..dd40f813f7f9 100644 --- a/tests/Integration/ApiPlatform/Serializer/DomainSerializerTest.php +++ b/tests/Integration/ApiPlatform/Serializer/DomainSerializerTest.php @@ -66,23 +66,32 @@ public static function tearDownAfterClass(): void LanguageResetter::resetLanguages(); } + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + self::$frenchLangId = null; + LanguageResetter::resetLanguages(); + } + protected static function getFrenchId(): int { if (empty(self::$frenchLangId)) { - LanguageResetter::resetLanguages(); self::$frenchLangId = self::addLanguageByLocale('fr-FR'); } return self::$frenchLangId; } - /** - * @dataProvider getExpectedDenormalizedData - */ - public function testDenormalize($dataToDenormalize, $denormalizedObject, ?array $normalizationMapping = []): void + public function testDenormalize(): void { $serializer = self::getContainer()->get(DomainSerializer::class); - self::assertEquals($denormalizedObject, $serializer->denormalize($dataToDenormalize, get_class($denormalizedObject), null, [DomainSerializer::NORMALIZATION_MAPPING => $normalizationMapping])); + + // We don't use @dataProvider because the class DB setup was messy, so we do it manually + foreach ($this->getExpectedDenormalizedData() as $denormalizedData) { + list($dataToDenormalize, $denormalizedObject) = $denormalizedData; + $normalizationMapping = $denormalizedData[2] ?? []; + self::assertEquals($denormalizedObject, $serializer->denormalize($dataToDenormalize, get_class($denormalizedObject), null, [DomainSerializer::NORMALIZATION_MAPPING => $normalizationMapping])); + } } public function testDenormalizeWithEmptyValues(): void @@ -94,11 +103,41 @@ public function testDenormalizeWithEmptyValues(): void public function getExpectedDenormalizedData() { + $productResource = new Product(); + $productResource->productId = 42; + $productResource->names = [ + 'en-US' => 'english name', + 'fr-FR' => 'nom français', + ]; + $productResource->descriptions = [ + 'en-US' => 'english description', + 'fr-FR' => 'description française', + ]; + $productResource->active = true; + $productResource->type = ProductType::TYPE_STANDARD; + + yield 'api resource with localized properties should have indexes based on locale values instead of integers' => [ + [ + 'productId' => 42, + 'names' => [ + self::EN_LANG_ID => 'english name', + self::getFrenchId() => 'nom français', + ], + 'descriptions' => [ + self::EN_LANG_ID => 'english description', + self::getFrenchId() => 'description française', + ], + 'active' => true, + 'type' => ProductType::TYPE_STANDARD, + ], + $productResource, + ]; + yield 'command with various property types all in constructor' => [ [ 'localizedNames' => [ - 1 => 'test1', - 2 => 'test2', + self::EN_LANG_ID => 'test en', + self::getFrenchId() => 'test fr', ], 'reductionPercent' => 10.3, 'displayPriceTaxExcluded' => true, @@ -107,8 +146,8 @@ public function getExpectedDenormalizedData() ], new AddCustomerGroupCommand( [ - 1 => 'test1', - 2 => 'test2', + self::EN_LANG_ID => 'test en', + self::getFrenchId() => 'test fr', ], new DecimalNumber('10.3'), true, @@ -122,7 +161,7 @@ public function getExpectedDenormalizedData() $editCartRuleCommand->setCode('test code'); $editCartRuleCommand->setMinimumAmount('10', 1, true, true); $editCartRuleCommand->setCustomerId(1); - $editCartRuleCommand->setLocalizedNames([1 => 'test1', 2 => 'test2']); + $editCartRuleCommand->setLocalizedNames([self::EN_LANG_ID => 'test en', self::getFrenchId() => 'test fr']); $editCartRuleCommand->setHighlightInCart(true); $editCartRuleCommand->setAllowPartialUse(true); $editCartRuleCommand->setPriority(1); @@ -139,8 +178,8 @@ public function getExpectedDenormalizedData() 'minimumAmount' => ['minimumAmount' => '10', 'currencyId' => 1, 'taxIncluded' => true, 'shippingIncluded' => true], 'customerId' => 1, 'localizedNames' => [ - 1 => 'test1', - 2 => 'test2', + self::EN_LANG_ID => 'test en', + self::getFrenchId() => 'test fr', ], 'highlightInCart' => true, 'allowPartialUse' => true, @@ -263,13 +302,16 @@ public function getExpectedDenormalizedData() ]; } - /** - * @dataProvider getNormalizationData - */ - public function testNormalize($dataToNormalize, $expectedNormalizedData, ?array $normalizationMapping = []): void + public function testNormalize(): void { $serializer = self::getContainer()->get(DomainSerializer::class); - self::assertEquals($expectedNormalizedData, $serializer->normalize($dataToNormalize, null, [DomainSerializer::NORMALIZATION_MAPPING => $normalizationMapping])); + + // We don't use @dataProvider because the class DB setup was messy, so we do it manually + foreach ($this->getNormalizationData() as $normalizationData) { + list($dataToNormalize, $expectedNormalizedData) = $normalizationData; + $normalizationMapping = $normalizationData[2] ?? []; + self::assertEquals($expectedNormalizedData, $serializer->normalize($dataToNormalize, null, [DomainSerializer::NORMALIZATION_MAPPING => $normalizationMapping])); + } } public function testNormalizeWithEmptyValues(): void