diff --git a/src/OpenApi/Model/Components.php b/src/OpenApi/Model/Components.php index 1035ab1d7f0..75cd874a8e5 100644 --- a/src/OpenApi/Model/Components.php +++ b/src/OpenApi/Model/Components.php @@ -29,6 +29,10 @@ final class Components public function __construct(\ArrayObject $schemas = null, \ArrayObject $responses = null, \ArrayObject $parameters = null, \ArrayObject $examples = null, \ArrayObject $requestBodies = null, \ArrayObject $headers = null, \ArrayObject $securitySchemes = null, \ArrayObject $links = null, \ArrayObject $callbacks = null) { + if ($schemas) { + $schemas->ksort(); + } + $this->schemas = $schemas; $this->responses = $responses; $this->parameters = $parameters; diff --git a/src/OpenApi/Model/Paths.php b/src/OpenApi/Model/Paths.php index 80829b3d876..efaa04a8e8a 100644 --- a/src/OpenApi/Model/Paths.php +++ b/src/OpenApi/Model/Paths.php @@ -20,6 +20,8 @@ final class Paths public function addPath(string $path, PathItem $pathItem) { $this->paths[$path] = $pathItem; + + ksort($this->paths); } public function getPath(string $path): ?PathItem diff --git a/src/OpenApi/Serializer/OpenApiNormalizer.php b/src/OpenApi/Serializer/OpenApiNormalizer.php index ba6a80abfb9..db37bffbb2a 100644 --- a/src/OpenApi/Serializer/OpenApiNormalizer.php +++ b/src/OpenApi/Serializer/OpenApiNormalizer.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\OpenApi\Serializer; +use ApiPlatform\Core\OpenApi\Model\Paths; use ApiPlatform\Core\OpenApi\OpenApi; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; @@ -38,8 +39,14 @@ public function __construct(NormalizerInterface $decorated) */ public function normalize($object, $format = null, array $context = []): array { + $pathsCallback = static function ($innerObject) { + return $innerObject instanceof Paths ? $innerObject->getPaths() : []; + }; $context[AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS] = true; $context[AbstractObjectNormalizer::SKIP_NULL_VALUES] = true; + $context[AbstractObjectNormalizer::CALLBACKS] = [ + 'paths' => $pathsCallback, + ]; return $this->recursiveClean($this->decorated->normalize($object, $format, $context)); } @@ -54,23 +61,8 @@ private function recursiveClean($data): array continue; } - if ('schemas' === $key && \is_array($value)) { - ksort($value); - } - - // Side effect of using getPaths(): Paths which itself contains the array - if ('paths' === $key) { - $value = $data['paths'] = $data['paths']['paths']; - if ($value) { - ksort($value); - } - unset($data['paths']['paths']); - } - if (\is_array($value)) { $data[$key] = $this->recursiveClean($value); - // arrays must stay even if empty - continue; } } diff --git a/tests/OpenApi/Serializer/OpenApiNormalizerTest.php b/tests/OpenApi/Serializer/OpenApiNormalizerTest.php index f2ea35be85f..1227647c7a2 100644 --- a/tests/OpenApi/Serializer/OpenApiNormalizerTest.php +++ b/tests/OpenApi/Serializer/OpenApiNormalizerTest.php @@ -60,7 +60,7 @@ public function testNormalize() $resourceNameCollectionFactoryProphecy->create()->shouldBeCalled()->willReturn(new ResourceNameCollection([Dummy::class, 'Zorro'])); $defaultContext = ['base_url' => '/app_dev.php/']; $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['id', 'name', 'description', 'dummyDate'])); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['id', 'name', 'description', 'dummyDate', 'paths'])); $propertyNameCollectionFactoryProphecy->create('Zorro', Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['id'])); $dummyMetadata = new ResourceMetadata( @@ -104,6 +104,8 @@ public function testNormalize() $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a name.', true, true, true, true, false, false, null, null, [], null, null, null, null, ['minLength' => 3, 'maxLength' => 20, 'pattern' => '^dummyPattern$'])); $propertyMetadataFactoryProphecy->create(Dummy::class, 'description', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is an initializable but not writable property.', true, false, true, true, false, false, null, null, [], null, true)); $propertyMetadataFactoryProphecy->create(Dummy::class, 'dummyDate', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTime::class), 'This is a \DateTimeInterface object.', true, true, true, true, false, false, null, null, [])); + // Check reserved word "paths": when normalize->recursiveClean in OpenApi Component Schema. + $propertyMetadataFactoryProphecy->create(Dummy::class, 'paths', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_ARRAY), 'This is a array.', true, true, true, true, false, false, null, null, [])); $propertyMetadataFactoryProphecy->create('Zorro', 'id', Argument::any())->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT), 'This is an id.', true, false, null, null, null, true));