diff --git a/src/PrestaShopBundle/ApiPlatform/OpenApi/Factory/CQRSOpenApiFactory.php b/src/PrestaShopBundle/ApiPlatform/OpenApi/Factory/CQRSOpenApiFactory.php index fb4865d9f700..13b7afd84116 100644 --- a/src/PrestaShopBundle/ApiPlatform/OpenApi/Factory/CQRSOpenApiFactory.php +++ b/src/PrestaShopBundle/ApiPlatform/OpenApi/Factory/CQRSOpenApiFactory.php @@ -29,14 +29,17 @@ use ApiPlatform\JsonSchema\DefinitionNameFactoryInterface; use ApiPlatform\JsonSchema\ResourceMetadataTrait; use ApiPlatform\JsonSchema\Schema; +use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; use ApiPlatform\OpenApi\OpenApi; use ArrayObject; +use PrestaShopBundle\ApiPlatform\Metadata\LocalizedValue; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; /** * This service decorates the main service that builds the Open API schema. It waits for the whole generation @@ -51,11 +54,12 @@ class CQRSOpenApiFactory implements OpenApiFactoryInterface protected PropertyAccessorInterface $propertyAccessor; public function __construct( - private readonly OpenApiFactoryInterface $decorated, - private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, + protected readonly OpenApiFactoryInterface $decorated, + protected readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, + protected readonly DefinitionNameFactoryInterface $definitionNameFactory, + protected readonly ClassMetadataFactoryInterface $classMetadataFactory, // No property promotion for this one since it's already defined in the ResourceMetadataTrait ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, - private readonly DefinitionNameFactoryInterface $definitionNameFactory, ) { $this->resourceMetadataFactory = $resourceMetadataFactory; $this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() @@ -73,9 +77,16 @@ public function __invoke(array $context = []): OpenApi $resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass); foreach ($resourceMetadataCollection as $resourceMetadata) { + $resourceDefinitionName = $this->definitionNameFactory->create($resourceMetadata->getClass()); + + // Adapt localized value schema for API resource schema (mostly for read schema) + if ($parentOpenApi->getComponents()->getSchemas()->offsetExists($resourceDefinitionName)) { + $this->adaptLocalizedValues($resourceMetadata, $parentOpenApi->getComponents()->getSchemas()->offsetGet($resourceDefinitionName)); + } + /** @var Operation $operation */ foreach ($resourceMetadata->getOperations() as $operation) { - if (empty($operation->getExtraProperties()['CQRSCommand']) || empty($operation->getExtraProperties()['CQRSCommandMapping'])) { + if (empty($operation->getExtraProperties()['CQRSCommand'])) { continue; } @@ -85,35 +96,21 @@ public function __invoke(array $context = []): OpenApi } // Build the operation name like SchemaFactory does so that we have the proper definition in the schema matching this operation - $definitionName = $this->definitionNameFactory->create($operation->getClass(), 'json', $inputClass, $operation, []); - if (!$parentOpenApi->getComponents()->getSchemas()->offsetExists($definitionName)) { + $operationDefinitionName = $this->definitionNameFactory->create($operation->getClass(), 'json', $inputClass, $operation, []); + if (!$parentOpenApi->getComponents()->getSchemas()->offsetExists($operationDefinitionName)) { continue; } /** @var ArrayObject $definition */ - $definition = $parentOpenApi->getComponents()->getSchemas()->offsetGet($definitionName); + $definition = $parentOpenApi->getComponents()->getSchemas()->offsetGet($operationDefinitionName); if (empty($definition['properties'])) { continue; } - foreach ($operation->getExtraProperties()['CQRSCommandMapping'] as $apiPath => $cqrsPath) { - // Replace properties that are scanned from CQRS command to their expected API path - if ($this->propertyAccessor->isReadable($definition['properties'], $cqrsPath) || $this->propertyAccessor->isWritable($definition['properties'], $cqrsPath)) { - // Automatic value from context are simply remove from the schema, the others are "moved" to match the expected property path - if (!str_starts_with($apiPath, '[_context]')) { - $this->propertyAccessor->setValue($definition['properties'], $apiPath, $this->propertyAccessor->getValue($definition['properties'], $cqrsPath)); - } - - // Use property path to set null, the null values will then be cleaned in a second loop (because unset cannot use property path as an input) - $this->propertyAccessor->setValue($definition['properties'], $cqrsPath, null); - } - } - - // Now clean the values that were set to null by the previous loop - foreach ($definition['properties'] as $propertyName => $propertyValue) { - if (null === $propertyValue) { - unset($definition['properties'][$propertyName]); - } + $this->applyCommandMapping($operation, $definition); + if ($resourceMetadata instanceof ApiResource) { + // Adapt localized value schema for operation definition (for valid input example) + $this->adaptLocalizedValues($resourceMetadata, $definition); } } } @@ -121,4 +118,60 @@ public function __invoke(array $context = []): OpenApi return $parentOpenApi; } + + protected function adaptLocalizedValues(ApiResource $apiResource, ArrayObject $definition): void + { + if (empty($definition['properties'])) { + return; + } + + $resourceClassMetadata = $this->classMetadataFactory->getMetadataFor($apiResource->getClass()); + $resourceReflectionClass = $resourceClassMetadata->getReflectionClass(); + + foreach ($definition['properties'] as $propertyName => $propertySchema) { + if (!$resourceReflectionClass->hasProperty($propertyName)) { + continue; + } + + $property = $resourceReflectionClass->getProperty($propertyName); + foreach ($property->getAttributes() as $attribute) { + // Adapt the schema of localized values, they must be an object index by the Language's locale (not an array) + if ($attribute->getName() === LocalizedValue::class || is_subclass_of($attribute->getName(), LocalizedValue::class)) { + $definition['properties'][$propertyName]['type'] = 'object'; + $definition['properties'][$propertyName]['example'] = [ + 'en-US' => 'value', + 'fr-FR' => 'valeur', + ]; + unset($definition['properties'][$propertyName]['items']); + } + } + } + } + + protected function applyCommandMapping(Operation $operation, ArrayObject $definition): void + { + if (empty($operation->getExtraProperties()['CQRSCommandMapping'])) { + return; + } + + foreach ($operation->getExtraProperties()['CQRSCommandMapping'] as $apiPath => $cqrsPath) { + // Replace properties that are scanned from CQRS command to their expected API path + if ($this->propertyAccessor->isReadable($definition['properties'], $cqrsPath) || $this->propertyAccessor->isWritable($definition['properties'], $cqrsPath)) { + // Automatic value from context are simply remove from the schema, the others are "moved" to match the expected property path + if (!str_starts_with($apiPath, '[_context]')) { + $this->propertyAccessor->setValue($definition['properties'], $apiPath, $this->propertyAccessor->getValue($definition['properties'], $cqrsPath)); + } + + // Use property path to set null, the null values will then be cleaned in a second loop (because unset cannot use property path as an input) + $this->propertyAccessor->setValue($definition['properties'], $cqrsPath, null); + } + } + + // Now clean the values that were set to null by the previous loop + foreach ($definition['properties'] as $propertyName => $propertyValue) { + if (null === $propertyValue) { + unset($definition['properties'][$propertyName]); + } + } + } } diff --git a/tests/Integration/PrestaShopBundle/ApiPlatform/OpenApiFactoryTest.php b/tests/Integration/PrestaShopBundle/ApiPlatform/OpenApiFactoryTest.php index 6d5cf3cbfb78..7687ad405ac0 100644 --- a/tests/Integration/PrestaShopBundle/ApiPlatform/OpenApiFactoryTest.php +++ b/tests/Integration/PrestaShopBundle/ApiPlatform/OpenApiFactoryTest.php @@ -69,12 +69,18 @@ public static function provideJsonSchemaFactoryCases(): iterable 'type' => 'boolean', ]), 'names' => new ArrayObject([ - 'type' => 'array', - 'items' => ['type' => 'string'], + 'type' => 'object', + 'example' => [ + 'en-US' => 'value', + 'fr-FR' => 'valeur', + ], ]), 'descriptions' => new ArrayObject([ - 'type' => 'array', - 'items' => ['type' => 'string'], + 'type' => 'object', + 'example' => [ + 'en-US' => 'value', + 'fr-FR' => 'valeur', + ], ]), // Shop IDs are documented via an ApiProperty attribute 'shopIds' => new ArrayObject([ @@ -100,8 +106,11 @@ public static function provideJsonSchemaFactoryCases(): iterable 'type' => 'string', ]), 'names' => new ArrayObject([ - 'type' => 'array', - 'items' => ['type' => 'string'], + 'type' => 'object', + 'example' => [ + 'en-US' => 'value', + 'fr-FR' => 'valeur', + ], ]), ], ]),