diff --git a/features/content_negotiation.feature b/features/content_negotiation.feature index 47e98e93983..770fe75dfe6 100644 --- a/features/content_negotiation.feature +++ b/features/content_negotiation.feature @@ -17,7 +17,7 @@ Feature: Content Negotiation support And the response should be equal to """ - 1XML! + /dummies/1XML! """ Scenario: Retrieve a collection in XML @@ -28,7 +28,7 @@ Feature: Content Negotiation support And the response should be equal to """ - 1XML! + /dummies/1XML! """ Scenario: Retrieve a collection in XML using the .xml URL @@ -38,7 +38,7 @@ Feature: Content Negotiation support And the response should be equal to """ - 1XML! + /dummies/1XML! """ Scenario: Retrieve a collection in JSON @@ -51,18 +51,18 @@ Feature: Content Negotiation support """ [ { - "id": 1, - "name": "XML!", - "alias": null, + "id": "/dummies/1", "description": null, + "dummy": null, + "dummyBoolean": null, "dummyDate": null, "dummyPrice": null, - "jsonData": [], "relatedDummy": null, - "dummyBoolean": null, - "dummy": null, "relatedDummies": [], - "nameConverted": null + "jsonData": [], + "name_converted": null, + "name": "XML!", + "alias": null } ] """ @@ -79,7 +79,7 @@ Feature: Content Negotiation support And the response should be equal to """ - 2Sent in JSON + /dummies/2Sent in JSON """ @dropSchema diff --git a/features/hal.feature b/features/hal.feature index 86d348ec931..8c65b6ebd0b 100644 --- a/features/hal.feature +++ b/features/hal.feature @@ -5,21 +5,24 @@ Feature: HAL support @createSchema Scenario: Create a third level - When I send a "POST" request to "/third_levels" with body: + When I add "Content-Type" header equal to "application/json" + And I send a "POST" request to "/third_levels" with body: """ {"level": 3} """ Then the response status code should be 201 Scenario: Create a related dummy - When I send a "POST" request to "/related_dummies" with body: + When I add "Content-Type" header equal to "application/json" + And I send a "POST" request to "/related_dummies" with body: """ {"thirdLevel": "/third_levels/1"} """ Then the response status code should be 201 Scenario: Create a dummy with relations - When I send a "POST" request to "/dummies" with body: + When I add "Content-Type" header equal to "application/json" + And I send a "POST" request to "/dummies" with body: """ { "name": "Dummy with relations", @@ -68,12 +71,11 @@ Feature: HAL support Scenario: Update a resource When I add "Accept" header equal to "application/hal+json" + And I add "Content-Type" header equal to "application/json" And I send a "PUT" request to "/dummies/1" with body: - """ - { - "name": "A nice dummy" - } - """ + """ + {"name": "A nice dummy"} + """ Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/hal+json" @@ -106,7 +108,8 @@ Feature: HAL support """ Scenario: Embed a relation in a parent object - When I send a "POST" request to "/relation_embedders" with body: + When I add "Content-Type" header equal to "application/json" + And I send a "POST" request to "/relation_embedders" with body: """ { "related": "/related_dummies/1" diff --git a/features/overridden_operation.feature b/features/overridden_operation.feature index a19ededcdf1..46c229d8081 100644 --- a/features/overridden_operation.feature +++ b/features/overridden_operation.feature @@ -52,8 +52,8 @@ Feature: Create-Retrieve-Update-Delete with a Overridden Operation context And the header "Content-Type" should be equal to "application/xml" And the response should be equal to """ - -My Overridden Operation DummyGerard + + /overridden_operation_dummies/1My Overridden Operation DummyGerard """ Scenario: Get a not found exception @@ -69,11 +69,11 @@ Feature: Create-Retrieve-Update-Delete with a Overridden Operation context """ { "@context": "/contexts/OverriddenOperationDummy", - "@id": "\/overridden_operation_dummies", + "@id": "/overridden_operation_dummies", "@type": "hydra:Collection", "hydra:member": [ { - "@id": "\/overridden_operation_dummies\/1", + "@id": "/overridden_operation_dummies/1", "@type": "OverriddenOperationDummy", "name": "My Overridden Operation Dummy", "alias": null, diff --git a/features/relation.feature b/features/relation.feature index 38b70ef9962..b76b8ee1949 100644 --- a/features/relation.feature +++ b/features/relation.feature @@ -5,7 +5,8 @@ Feature: Relations support @createSchema Scenario: Create a third level - When I send a "POST" request to "/third_levels" with body: + When I add "Content-Type" header equal to "application/ld+json" + And I send a "POST" request to "/third_levels" with body: """ {"level": 3} """ diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml index 059e2c8f7de..40dbb25b446 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml @@ -46,6 +46,17 @@ + + + + + + + + + + + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml index f5fcd603d7a..8c1a411f333 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml @@ -21,7 +21,7 @@ - + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml b/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml index 2fc020aa646..acb9472edec 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml @@ -25,7 +25,7 @@ - + diff --git a/src/Serializer/ItemNormalizer.php b/src/Serializer/ItemNormalizer.php new file mode 100644 index 00000000000..7cd7ed729d1 --- /dev/null +++ b/src/Serializer/ItemNormalizer.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Serializer; + +/** + * Generic item normalizer. + * + * @author Kévin Dunglas + */ +class ItemNormalizer extends AbstractItemNormalizer +{ + public function normalize($object, $format = null, array $context = []) + { + $rawData = parent::normalize($object, $format, $context); + if (!is_array($rawData)) { + return $rawData; + } + + if (!isset($data['id'])) { + $data['id'] = $this->iriConverter->getIriFromItem($object); + } + + return array_merge($data, $rawData); + } + + public function denormalize($data, $class, $format = null, array $context = []) + { + // Avoid issues with proxies if we populated the object + if (isset($data['id']) && !isset($context['object_to_populate'])) { + $context['object_to_populate'] = $this->iriConverter->getItemFromIri($data['id'], true); + } + + return parent::denormalize($data, $class, $format, $context); // TODO: Change the autogenerated stub + } +} diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 58a70ef0bbe..9b9d6e9dd77 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -259,6 +259,7 @@ private function getContainerBuilderProphecy() 'api_platform.listener.view.serialize', 'api_platform.listener.view.validate', 'api_platform.listener.view.respond', + 'api_platform.serializer.normalizer.item', 'api_platform.serializer.context_builder', 'api_platform.doctrine.metadata_factory', 'api_platform.doctrine.orm.collection_data_provider', diff --git a/tests/Hal/Serializer/ItemNormalizerTest.php b/tests/Hal/Serializer/ItemNormalizerTest.php index 4e12ac1b9aa..c786a3d429e 100644 --- a/tests/Hal/Serializer/ItemNormalizerTest.php +++ b/tests/Hal/Serializer/ItemNormalizerTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace ApiPlatform\Core\tests\Hal; +namespace ApiPlatform\Core\Tests\Hal; use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\Api\ResourceClassResolverInterface; diff --git a/tests/Serializer/ItemNormalizerTest.php b/tests/Serializer/ItemNormalizerTest.php new file mode 100644 index 00000000000..4069571eb3e --- /dev/null +++ b/tests/Serializer/ItemNormalizerTest.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Tests\Serializer; + +use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Core\Api\ResourceClassResolverInterface; +use ApiPlatform\Core\Exception\InvalidArgumentException; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Core\Metadata\Property\PropertyMetadata; +use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Core\Serializer\ItemNormalizer; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; +use Prophecy\Argument; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\SerializerInterface; + +/** + * @author Kévin Dunglas + */ +class ItemNormalizerTest extends \PHPUnit_Framework_TestCase +{ + public function testSupportNormalization() + { + $std = new \stdClass(); + $dummy = new Dummy(); + $dummy->setDescription('hello'); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy)->willReturn(Dummy::class)->shouldBeCalled(); + $resourceClassResolverProphecy->getResourceClass($std)->willThrow(new InvalidArgumentException())->shouldBeCalled(); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true)->shouldBeCalled(); + $resourceClassResolverProphecy->isResourceClass(\stdClass::class)->willReturn(false)->shouldBeCalled(); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal() + ); + + $this->assertTrue($normalizer->supportsNormalization($dummy)); + $this->assertTrue($normalizer->supportsNormalization($dummy)); + $this->assertFalse($normalizer->supportsNormalization($std)); + + $this->assertTrue($normalizer->supportsDenormalization($dummy, Dummy::class)); + $this->assertTrue($normalizer->supportsDenormalization($dummy, Dummy::class)); + $this->assertFalse($normalizer->supportsDenormalization($std, \stdClass::class)); + } + + public function testNormalize() + { + $dummy = new Dummy(); + $dummy->setName('hello'); + + $propertyNameCollection = new PropertyNameCollection(['name']); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn($propertyNameCollection)->shouldBeCalled(); + + $propertyMetadataFactory = new PropertyMetadata(null, null, true); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn($propertyMetadataFactory)->shouldBeCalled(); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromItem($dummy)->willReturn('/dummies/12')->shouldBeCalled(); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, null, true)->willReturn(Dummy::class)->shouldBeCalled(); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + $serializerProphecy->normalize('hello', null, Argument::type('array'))->willReturn('hello')->shouldBeCalled(); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal() + ); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $this->assertEquals(['id' => '/dummies/12', 'name' => 'hello'], $normalizer->normalize($dummy)); + } + + public function testDenormalize() + { + $context = ['resource_class' => Dummy::class]; + + $propertyNameCollection = new PropertyNameCollection(['name']); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn($propertyNameCollection)->shouldBeCalled(); + + $propertyMetadataFactory = new PropertyMetadata(null, null, true); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn($propertyMetadataFactory)->shouldBeCalled(); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal() + ); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $this->assertInstanceOf(Dummy::class, $normalizer->denormalize(['id' => '/dummies/12', 'name' => 'hello'], Dummy::class, null, $context)); + } +}