Skip to content

Commit

Permalink
Handle denormalization of localized values based on locale
Browse files Browse the repository at this point in the history
  • Loading branch information
jolelievre committed Dec 16, 2024
1 parent 5150742 commit 7b765e2
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 23 deletions.
49 changes: 44 additions & 5 deletions src/PrestaShopBundle/ApiPlatform/Normalizer/CQRSApiNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
{
Expand All @@ -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.
Expand Down Expand Up @@ -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
*
Expand All @@ -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.
*
Expand Down
78 changes: 60 additions & 18 deletions tests/Integration/ApiPlatform/Serializer/DomainSerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 7b765e2

Please sign in to comment.