diff --git a/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php b/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php index dd165208b26..ce7c1c0dd90 100644 --- a/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php +++ b/src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php @@ -61,6 +61,11 @@ public function create(string $resourceClass, string $property, array $options = return $propertyMetadata; } + $serializerMetadataAttributes = $this->serializerClassMetadataFactory->getMetadataFor($resourceClass)->getAttributesMetadata(); + if (!empty($serializerMetadataAttributes[$property]) && null !== ($serializedName = $serializerMetadataAttributes[$property]->getSerializedName())) { + $propertyMetadata = $propertyMetadata->withSerializedName($serializedName); + } + $propertyMetadata = $this->transformReadWrite($propertyMetadata, $resourceClass, $property, $normalizationGroups, $denormalizationGroups); return $this->transformLinkStatus($propertyMetadata, $normalizationGroups, $denormalizationGroups); diff --git a/src/Metadata/Property/PropertyMetadata.php b/src/Metadata/Property/PropertyMetadata.php index 1cdb697fe75..9833147a336 100644 --- a/src/Metadata/Property/PropertyMetadata.php +++ b/src/Metadata/Property/PropertyMetadata.php @@ -35,8 +35,9 @@ final class PropertyMetadata private $attributes; private $subresource; private $initializable; + private $serializedName; - public function __construct(Type $type = null, string $description = null, bool $readable = null, bool $writable = null, bool $readableLink = null, bool $writableLink = null, bool $required = null, bool $identifier = null, string $iri = null, $childInherited = null, array $attributes = null, SubresourceMetadata $subresource = null, bool $initializable = null) + public function __construct(Type $type = null, string $description = null, bool $readable = null, bool $writable = null, bool $readableLink = null, bool $writableLink = null, bool $required = null, bool $identifier = null, string $iri = null, $childInherited = null, array $attributes = null, SubresourceMetadata $subresource = null, bool $initializable = null, string $serializedName = null) { $this->type = $type; $this->description = $description; @@ -51,6 +52,7 @@ public function __construct(Type $type = null, string $description = null, bool $this->attributes = $attributes; $this->subresource = $subresource; $this->initializable = $initializable; + $this->serializedName = $serializedName; } /** @@ -343,4 +345,23 @@ public function withInitializable(bool $initializable): self return $metadata; } + + /** + * Gets custom serialized name of this property. + */ + public function getSerializedName(): ?string + { + return $this->serializedName; + } + + /** + * Returns a new instance with the given serialized name. + */ + public function withSerializedName(string $serializedName = null): self + { + $metadata = clone $this; + $metadata->serializedName = $serializedName; + + return $metadata; + } } diff --git a/src/Swagger/Serializer/DocumentationNormalizer.php b/src/Swagger/Serializer/DocumentationNormalizer.php index b13b73d7ee2..4b307e0ebb8 100644 --- a/src/Swagger/Serializer/DocumentationNormalizer.php +++ b/src/Swagger/Serializer/DocumentationNormalizer.php @@ -793,6 +793,7 @@ private function getLinkObject(string $resourceClass, string $operationId, strin continue; } + $propertyName = $propertyMetadata->getSerializedName() ?? $propertyName; $linkObject['parameters'][$propertyName] = sprintf('$response.body#/%s', $propertyName); $identifiers[] = $propertyName; } diff --git a/tests/Metadata/Property/Factory/SerializerPropertyMetadataFactoryTest.php b/tests/Metadata/Property/Factory/SerializerPropertyMetadataFactoryTest.php index 322b93be5e9..220766c2515 100644 --- a/tests/Metadata/Property/Factory/SerializerPropertyMetadataFactoryTest.php +++ b/tests/Metadata/Property/Factory/SerializerPropertyMetadataFactoryTest.php @@ -83,6 +83,9 @@ public function testCreate($readGroups, $writeGroups) $dummySerializerClassMetadata->addAttributeMetadata($relatedDummySerializerAttributeMetadata); $nameConvertedSerializerAttributeMetadata = new SerializerAttributeMetadata('nameConverted'); $dummySerializerClassMetadata->addAttributeMetadata($nameConvertedSerializerAttributeMetadata); + $nameSerializedSerializerAttributeMetadata = new SerializerAttributeMetadata('nameSerialized'); + $nameSerializedSerializerAttributeMetadata->setSerializedName('name_serialized'); + $dummySerializerClassMetadata->addAttributeMetadata($nameSerializedSerializerAttributeMetadata); $serializerClassMetadataFactoryProphecy->getMetadataFor(Dummy::class)->willReturn($dummySerializerClassMetadata); $relatedDummySerializerClassMetadata = new SerializerClassMetadata(RelatedDummy::class); $idSerializerAttributeMetadata = new SerializerAttributeMetadata('id'); @@ -105,6 +108,9 @@ public function testCreate($readGroups, $writeGroups) $nameConvertedPropertyMetadata = (new PropertyMetadata()) ->withType(new Type(Type::BUILTIN_TYPE_STRING, true)); $decoratedProphecy->create(Dummy::class, 'nameConverted', [])->willReturn($nameConvertedPropertyMetadata); + $nameSerializedPropertyMetadata = (new PropertyMetadata()) + ->withType(new Type(Type::BUILTIN_TYPE_STRING, true)); + $decoratedProphecy->create(Dummy::class, 'nameSerialized', [])->willReturn($nameSerializedPropertyMetadata); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); @@ -116,6 +122,7 @@ public function testCreate($readGroups, $writeGroups) $actual[] = $serializerPropertyMetadataFactory->create(Dummy::class, 'foo'); $actual[] = $serializerPropertyMetadataFactory->create(Dummy::class, 'relatedDummy'); $actual[] = $serializerPropertyMetadataFactory->create(Dummy::class, 'nameConverted'); + $actual[] = $serializerPropertyMetadataFactory->create(Dummy::class, 'nameSerialized'); $this->assertInstanceOf(PropertyMetadata::class, $actual[0]); $this->assertFalse($actual[0]->isReadable()); @@ -130,6 +137,9 @@ public function testCreate($readGroups, $writeGroups) $this->assertInstanceOf(PropertyMetadata::class, $actual[2]); $this->assertFalse($actual[2]->isReadable()); $this->assertFalse($actual[2]->isWritable()); + + $this->assertInstanceOf(PropertyMetadata::class, $actual[3]); + $this->assertEquals($actual[3]->getSerializedName(), 'name_serialized'); } public function groupsProvider(): array diff --git a/tests/Metadata/Property/PropertyMetadataTest.php b/tests/Metadata/Property/PropertyMetadataTest.php index afee7f6adb6..dc8e5ac488f 100644 --- a/tests/Metadata/Property/PropertyMetadataTest.php +++ b/tests/Metadata/Property/PropertyMetadataTest.php @@ -82,6 +82,10 @@ public function testValueObject() $newMetadata = $metadata->withInitializable(true); $this->assertNotSame($metadata, $newMetadata); $this->assertTrue($newMetadata->isInitializable()); + + $newMetadata = $metadata->withSerializedName('foo'); + $this->assertNotSame($metadata, $newMetadata); + $this->assertEquals('foo', $newMetadata->getSerializedName()); } public function testShouldReturnRequiredFalseWhenRequiredTrueIsSetButMaskedByWritableFalse() diff --git a/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php b/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php index b2e74261ff0..f35125a6587 100644 --- a/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php +++ b/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php @@ -3131,4 +3131,148 @@ private function doTestNormalizeWithCustomFormatsDefinedAtOperationLevel(Operati $this->assertEquals($expected, $normalizer->normalize($documentation, DocumentationNormalizer::FORMAT, ['base_url' => '/'])); } + + public function testNormalizeWithCustomSerializedName(): void + { + $documentation = new Documentation(new ResourceNameCollection([Dummy::class]), 'Test API', 'This is a test API.', '1.2.3'); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::cetera())->willReturn(new PropertyNameCollection(['id'])); + + $dummyMetadata = new ResourceMetadata( + 'Dummy', + 'This is a dummy.', + 'http://schema.example.com/Dummy', + [ + 'get' => ['method' => 'GET'] + self::OPERATION_FORMATS, + ], + [ + 'post' => ['method' => 'POST'] + self::OPERATION_FORMATS, + ] + ); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn($dummyMetadata); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'id', Argument::cetera())->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_INT), 'This is an id.', true, false, null, null, null, true, null, null, [], null, null, 'code')); + + $operationPathResolver = new OperationPathResolver(new UnderscorePathSegmentNameGenerator()); + + $normalizer = new DocumentationNormalizer( + $resourceMetadataFactoryProphecy->reveal(), + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + null, + null, + $operationPathResolver, + null, + null, + null, false, + '', + '', + '', + '', + [], + [], + null, + false, + 'page', + false, + 'itemsPerPage', + [], + false, + 'pagination', + ['spec_version' => 3] + ); + + $expected = [ + 'openapi' => '3.0.2', + 'info' => [ + 'title' => 'Test API', + 'description' => 'This is a test API.', + 'version' => '1.2.3', + ], + 'paths' => new \ArrayObject([ + '/dummies' => [ + 'post' => new \ArrayObject([ + 'tags' => ['Dummy'], + 'operationId' => 'postDummyCollection', + 'summary' => 'Creates a Dummy resource.', + 'requestBody' => [ + 'content' => [ + 'application/ld+json' => [ + 'schema' => ['$ref' => '#/components/schemas/Dummy'], + ], + ], + 'description' => 'The new Dummy resource', + ], + 'responses' => [ + 201 => [ + 'description' => 'Dummy resource created', + 'content' => [ + 'application/ld+json' => [ + 'schema' => ['$ref' => '#/components/schemas/Dummy'], + ], + ], + 'links' => [ + 'GetDummyItem' => [ + 'operationId' => 'getDummyItem', + 'parameters' => ['code' => '$response.body#/code'], + 'description' => 'The `code` value returned in the response can be used as the `code` parameter in `GET /dummies/{id}`.', + ], + ], + ], + 400 => ['description' => 'Invalid input'], + 404 => ['description' => 'Resource not found'], + ], + ]), + ], + '/dummies/{id}' => [ + 'get' => new \ArrayObject([ + 'tags' => ['Dummy'], + 'operationId' => 'getDummyItem', + 'summary' => 'Retrieves a Dummy resource.', + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'path', + 'schema' => ['type' => 'string'], + 'required' => true, + ], + ], + 'responses' => [ + '200' => [ + 'description' => 'Dummy resource response', + 'content' => [ + 'application/ld+json' => [ + 'schema' => ['$ref' => '#/components/schemas/Dummy'], + ], + ], + ], + '404' => ['description' => 'Resource not found'], + ], + ]), + ], + ]), + 'components' => [ + 'schemas' => new \ArrayObject([ + 'Dummy' => new \ArrayObject([ + 'type' => 'object', + 'description' => 'This is a dummy.', + 'externalDocs' => ['url' => 'http://schema.example.com/Dummy'], + 'properties' => [ + 'id' => new \ArrayObject([ + 'type' => 'integer', + 'description' => 'This is an id.', + 'readOnly' => true, + ]), + ], + 'additionalProperties' => false, + ]), + ]), + ], + ]; + + $this->assertEquals($expected, $normalizer->normalize($documentation, DocumentationNormalizer::FORMAT, ['base_url' => '/'])); + } }