From 80ba354c2c62e1b8218a12d25ca2d54c71858e23 Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 4 Oct 2018 16:56:42 +0200 Subject: [PATCH] Allow an input and an output for a given resource class --- src/Annotation/ApiResource.php | 16 +++ src/Bridge/Symfony/Routing/ApiLoader.php | 4 + .../OperationDataProviderTrait.php | 8 +- src/EventListener/DeserializeListener.php | 2 +- src/EventListener/WriteListener.php | 3 +- .../Factory/SubresourceOperationFactory.php | 2 + src/Serializer/AbstractItemNormalizer.php | 8 ++ src/Serializer/SerializerContextBuilder.php | 2 + src/Util/AttributesExtractor.php | 3 + .../RequestDataCollectorTest.php | 2 +- .../Bridge/Symfony/Routing/ApiLoaderTest.php | 4 + tests/EventListener/AddFormatListenerTest.php | 2 +- .../EventListener/DeserializeListenerTest.php | 2 +- .../DataPersister/DummyInputDataPersister.php | 46 +++++++ .../Fixtures/TestBundle/Entity/DummyInput.php | 40 ++++++ .../TestBundle/Entity/DummyInputOutput.php | 33 +++++ .../TestBundle/Entity/DummyOutput.php | 46 +++++++ tests/Fixtures/app/config/config_test.yml | 6 + .../SubresourceOperationFactoryTest.php | 44 +++++++ .../SerializerContextBuilderTest.php | 14 +- .../SerializerFilterContextBuilderTest.php | 8 ++ tests/Util/RequestAttributesExtractorTest.php | 122 ++++++++++++++++-- 22 files changed, 393 insertions(+), 24 deletions(-) create mode 100644 tests/Fixtures/TestBundle/DataPersister/DummyInputDataPersister.php create mode 100644 tests/Fixtures/TestBundle/Entity/DummyInput.php create mode 100644 tests/Fixtures/TestBundle/Entity/DummyInputOutput.php create mode 100644 tests/Fixtures/TestBundle/Entity/DummyOutput.php diff --git a/src/Annotation/ApiResource.php b/src/Annotation/ApiResource.php index 23bc0148b64..ca4ab6658ff 100644 --- a/src/Annotation/ApiResource.php +++ b/src/Annotation/ApiResource.php @@ -36,11 +36,13 @@ * @Attribute("filters", type="string[]"), * @Attribute("graphql", type="array"), * @Attribute("hydraContext", type="array"), + * @Attribute("inputClass", type="string"), * @Attribute("iri", type="string"), * @Attribute("itemOperations", type="array"), * @Attribute("maximumItemsPerPage", type="int"), * @Attribute("normalizationContext", type="array"), * @Attribute("order", type="array"), + * @Attribute("outputClass", type="string"), * @Attribute("paginationClientEnabled", type="bool"), * @Attribute("paginationClientItemsPerPage", type="bool"), * @Attribute("paginationClientPartial", type="bool"), @@ -256,6 +258,20 @@ final class ApiResource */ private $sunset; + /** + * @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112 + * + * @var string + */ + private $inputClass; + + /** + * @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112 + * + * @var string + */ + private $outputClass; + /** * @throws InvalidArgumentException */ diff --git a/src/Bridge/Symfony/Routing/ApiLoader.php b/src/Bridge/Symfony/Routing/ApiLoader.php index 6af14127068..f414e0d8de4 100644 --- a/src/Bridge/Symfony/Routing/ApiLoader.php +++ b/src/Bridge/Symfony/Routing/ApiLoader.php @@ -123,6 +123,8 @@ public function load($data, $type = null): RouteCollection '_controller' => $controller, '_format' => null, '_api_resource_class' => $operation['resource_class'], + '_api_input_resource_class' => $operation['input_class'], + '_api_output_resource_class' => $operation['output_class'], '_api_subresource_operation_name' => $operation['route_name'], '_api_subresource_context' => [ 'property' => $operation['property'], @@ -210,6 +212,8 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas '_controller' => $controller, '_format' => null, '_api_resource_class' => $resourceClass, + '_api_input_resource_class' => $resourceMetadata->getAttribute('input_class', $resourceClass), + '_api_output_resource_class' => $resourceMetadata->getAttribute('output_class', $resourceClass), sprintf('_api_%s_operation_name', $operationType) => $operationName, ] + ($operation['defaults'] ?? []), $operation['requirements'] ?? [], diff --git a/src/DataProvider/OperationDataProviderTrait.php b/src/DataProvider/OperationDataProviderTrait.php index d5a656fbfaf..a24037592ab 100644 --- a/src/DataProvider/OperationDataProviderTrait.php +++ b/src/DataProvider/OperationDataProviderTrait.php @@ -49,7 +49,7 @@ trait OperationDataProviderTrait */ private function getCollectionData(array $attributes, array $context) { - return $this->collectionDataProvider->getCollection($attributes['resource_class'], $attributes['collection_operation_name'], $context); + return $this->collectionDataProvider->getCollection($attributes['output_resource_class'], $attributes['collection_operation_name'], $context); } /** @@ -59,7 +59,7 @@ private function getCollectionData(array $attributes, array $context) */ private function getItemData($identifiers, array $attributes, array $context) { - return $this->itemDataProvider->getItem($attributes['resource_class'], $identifiers, $attributes['item_operation_name'], $context); + return $this->itemDataProvider->getItem($attributes['output_resource_class'], $identifiers, $attributes['item_operation_name'], $context); } /** @@ -75,7 +75,7 @@ private function getSubresourceData($identifiers, array $attributes, array $cont throw new RuntimeException('Subresources not supported'); } - return $this->subresourceDataProvider->getSubresource($attributes['resource_class'], $identifiers, $attributes['subresource_context'] + $context, $attributes['subresource_operation_name']); + return $this->subresourceDataProvider->getSubresource($attributes['output_resource_class'], $identifiers, $attributes['subresource_context'] + $context, $attributes['subresource_operation_name']); } /** @@ -93,7 +93,7 @@ private function extractIdentifiers(array $parameters, array $attributes) $id = $parameters['id']; if (null !== $this->identifierConverter) { - return $this->identifierConverter->convert((string) $id, $attributes['resource_class']); + return $this->identifierConverter->convert((string) $id, $attributes['output_resource_class']); } return $id; diff --git a/src/EventListener/DeserializeListener.php b/src/EventListener/DeserializeListener.php index 68058549160..2b821887ce8 100644 --- a/src/EventListener/DeserializeListener.php +++ b/src/EventListener/DeserializeListener.php @@ -90,7 +90,7 @@ public function onKernelRequest(GetResponseEvent $event) $request->attributes->set( 'data', $this->serializer->deserialize( - $requestContent, $attributes['resource_class'], $format, $context + $requestContent, $attributes['input_resource_class'], $format, $context ) ); } diff --git a/src/EventListener/WriteListener.php b/src/EventListener/WriteListener.php index 3c9e6364cde..520a252d00b 100644 --- a/src/EventListener/WriteListener.php +++ b/src/EventListener/WriteListener.php @@ -61,7 +61,8 @@ public function onKernelView(GetResponseForControllerResultEvent $event) $event->setControllerResult($persistResult ?? $controllerResult); - if (null !== $this->iriConverter) { + // Comparing the class is necessary because the input might not be readable + if (null !== $this->iriConverter && \get_class($controllerResult) === \get_class($event->getControllerResult())) { $request->attributes->set('_api_write_item_iri', $this->iriConverter->getIriFromItem($controllerResult)); } break; diff --git a/src/Operation/Factory/SubresourceOperationFactory.php b/src/Operation/Factory/SubresourceOperationFactory.php index d930ad8eae0..3ac9b14aa7f 100644 --- a/src/Operation/Factory/SubresourceOperationFactory.php +++ b/src/Operation/Factory/SubresourceOperationFactory.php @@ -106,6 +106,8 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre 'collection' => $subresource->isCollection(), 'resource_class' => $subresourceClass, 'shortNames' => [$subresourceMetadata->getShortName()], + 'input_class' => $subresourceMetadata->getAttribute('input_class', $subresourceClass), + 'output_class' => $subresourceMetadata->getAttribute('output_class', $subresourceClass), ]; if (null === $parentOperation) { diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 9091a13147e..4c9c4a8217d 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -96,6 +96,10 @@ public function hasCacheableSupportsMethod(): bool */ public function normalize($object, $format = null, array $context = []) { + if (isset($context['resource_class']) && isset($context['normalize_resource_class'])) { + $context['resource_class'] = $context['normalize_resource_class']; + } + $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true); $context = $this->initContext($resourceClass, $context); $context['api_normalize'] = true; @@ -124,6 +128,10 @@ public function denormalize($data, $class, $format = null, array $context = []) $context['api_denormalize'] = true; if (!isset($context['resource_class'])) { $context['resource_class'] = $class; + + if (isset($context['denormalize_resource_class'])) { + $context['resource_class'] = $context['denormalize_resource_class']; + } } return parent::denormalize($data, $class, $format, $context); diff --git a/src/Serializer/SerializerContextBuilder.php b/src/Serializer/SerializerContextBuilder.php index f760f08635b..01bdf7da2c0 100644 --- a/src/Serializer/SerializerContextBuilder.php +++ b/src/Serializer/SerializerContextBuilder.php @@ -73,6 +73,8 @@ public function createFromRequest(Request $request, bool $normalization, array $ } $context['resource_class'] = $attributes['resource_class']; + $context['denormalize_resource_class'] = $attributes['input_resource_class'] ?? $attributes['resource_class']; + $context['normalize_resource_class'] = $attributes['output_resource_class'] ?? $attributes['resource_class']; $context['request_uri'] = $request->getRequestUri(); $context['uri'] = $request->getUri(); diff --git a/src/Util/AttributesExtractor.php b/src/Util/AttributesExtractor.php index beb971c5104..f9a0f37f1f7 100644 --- a/src/Util/AttributesExtractor.php +++ b/src/Util/AttributesExtractor.php @@ -36,6 +36,9 @@ public static function extractAttributes(array $attributes): array { $result = ['resource_class' => $attributes['_api_resource_class'] ?? null]; + $result['input_resource_class'] = $attributes['_api_input_resource_class'] ?? $result['resource_class']; + $result['output_resource_class'] = $attributes['_api_output_resource_class'] ?? $result['resource_class']; + if ($subresourceContext = $attributes['_api_subresource_context'] ?? null) { $result['subresource_context'] = $subresourceContext; } diff --git a/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php b/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php index 65e28dbe7ff..a2b3d08ba48 100644 --- a/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php +++ b/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php @@ -107,7 +107,7 @@ public function testWithResource() $this->response ); - $this->assertSame(['resource_class' => DummyEntity::class, 'item_operation_name' => 'get', 'receive' => true, 'persist' => true], $dataCollector->getRequestAttributes()); + $this->assertSame(['resource_class' => DummyEntity::class, 'input_resource_class' => DummyEntity::class, 'output_resource_class' => DummyEntity::class, 'item_operation_name' => 'get', 'receive' => true, 'persist' => true], $dataCollector->getRequestAttributes()); $this->assertSame(['foo', 'bar'], $dataCollector->getAcceptableContentTypes()); $this->assertSame(DummyEntity::class, $dataCollector->getResourceClass()); $this->assertSame(['foo' => null, 'a_filter' => \stdClass::class], $dataCollector->getFilters()); diff --git a/tests/Bridge/Symfony/Routing/ApiLoaderTest.php b/tests/Bridge/Symfony/Routing/ApiLoaderTest.php index bd9bd2506b0..a65158140a9 100644 --- a/tests/Bridge/Symfony/Routing/ApiLoaderTest.php +++ b/tests/Bridge/Symfony/Routing/ApiLoaderTest.php @@ -305,6 +305,8 @@ private function getRoute(string $path, string $controller, string $resourceClas '_controller' => $controller, '_format' => null, '_api_resource_class' => $resourceClass, + '_api_input_resource_class' => $resourceClass, + '_api_output_resource_class' => $resourceClass, sprintf('_api_%s_operation_name', $collection ? 'collection' : 'item') => $operationName, ] + $extraDefaults, $requirements, @@ -324,6 +326,8 @@ private function getSubresourceRoute(string $path, string $controller, string $r '_controller' => $controller, '_format' => null, '_api_resource_class' => $resourceClass, + '_api_input_resource_class' => $resourceClass, + '_api_output_resource_class' => $resourceClass, '_api_subresource_operation_name' => $operationName, '_api_subresource_context' => $context, ], diff --git a/tests/EventListener/AddFormatListenerTest.php b/tests/EventListener/AddFormatListenerTest.php index 1ca382ad173..ed833eb81af 100644 --- a/tests/EventListener/AddFormatListenerTest.php +++ b/tests/EventListener/AddFormatListenerTest.php @@ -224,7 +224,7 @@ public function testResourceClassSupportedRequestFormat() $event = $eventProphecy->reveal(); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); - $formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'collection_operation_name' => 'get', 'receive' => true, 'persist' => true])->willReturn(['csv' => ['text/csv']])->shouldBeCalled(); + $formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'input_resource_class' => 'Foo', 'output_resource_class' => 'Foo', 'collection_operation_name' => 'get', 'receive' => true, 'persist' => true])->willReturn(['csv' => ['text/csv']])->shouldBeCalled(); $listener = new AddFormatListener(new Negotiator(), $formatsProviderProphecy->reveal()); $listener->onKernelRequest($event); diff --git a/tests/EventListener/DeserializeListenerTest.php b/tests/EventListener/DeserializeListenerTest.php index d11056f3f97..3d73e168307 100644 --- a/tests/EventListener/DeserializeListenerTest.php +++ b/tests/EventListener/DeserializeListenerTest.php @@ -172,7 +172,7 @@ public function testDeserializeResourceClassSupportedFormat(string $method, bool $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn([])->shouldBeCalled(); $formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class); - $formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'collection_operation_name' => 'post', 'receive' => true, 'persist' => true])->willReturn(self::FORMATS)->shouldBeCalled(); + $formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'input_resource_class' => 'Foo', 'output_resource_class' => 'Foo', 'collection_operation_name' => 'post', 'receive' => true, 'persist' => true])->willReturn(self::FORMATS)->shouldBeCalled(); $listener = new DeserializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), $formatsProviderProphecy->reveal()); diff --git a/tests/Fixtures/TestBundle/DataPersister/DummyInputDataPersister.php b/tests/Fixtures/TestBundle/DataPersister/DummyInputDataPersister.php new file mode 100644 index 00000000000..18a35d0f567 --- /dev/null +++ b/tests/Fixtures/TestBundle/DataPersister/DummyInputDataPersister.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\DataPersister; + +use ApiPlatform\Core\DataPersister\DataPersisterInterface; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyInput; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyOutput; + +class DummyInputDataPersister implements DataPersisterInterface +{ + public function supports($data): bool + { + return $data instanceof DummyInput; + } + + /** + * {@inheritdoc} + */ + public function persist($data) + { + $output = new DummyOutput(); + $output->name = $data->name; + $output->id = 1; + + return $output; + } + + /** + * {@inheritdoc} + */ + public function remove($data) + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/DummyInput.php b/tests/Fixtures/TestBundle/Entity/DummyInput.php new file mode 100644 index 00000000000..24ffc48b405 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyInput.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiProperty; +use ApiPlatform\Core\Annotation\ApiResource; + +/** + * Dummy Input. + * + * @author Kévin Dunglas + * + * @ApiResource + */ +class DummyInput +{ + /** + * @var int The id + * @ApiProperty(identifier=true) + */ + public $id; + + /** + * @var string The dummy name + * + * @ApiProperty + */ + public $name; +} diff --git a/tests/Fixtures/TestBundle/Entity/DummyInputOutput.php b/tests/Fixtures/TestBundle/Entity/DummyInputOutput.php new file mode 100644 index 00000000000..442a83c1121 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyInputOutput.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiProperty; +use ApiPlatform\Core\Annotation\ApiResource; + +/** + * Dummy InputOutput. + * + * @author Kévin Dunglas + * + * @ApiResource(attributes={"input_class"=DummyInput::class, "output_class"=DummyOutput::class}) + */ +class DummyInputOutput +{ + /** + * @var int The id + * @ApiProperty(identifier=true) + */ + public $id; +} diff --git a/tests/Fixtures/TestBundle/Entity/DummyOutput.php b/tests/Fixtures/TestBundle/Entity/DummyOutput.php new file mode 100644 index 00000000000..b7cbb15de90 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyOutput.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Core\Annotation\ApiProperty; +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\ORM\Mapping as ORM; + +/** + * Dummy Output. + * + * @author Kévin Dunglas + * + * @ApiResource + * @ORM\Entity + */ +class DummyOutput +{ + /** + * @var int The id + * + * @ORM\Column(type="integer") + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + */ + public $id; + + /** + * @var string The dummy name + * + * @ORM\Column + * @ApiProperty(iri="http://schema.org/name") + */ + public $name; +} diff --git a/tests/Fixtures/app/config/config_test.yml b/tests/Fixtures/app/config/config_test.yml index 04b464a2ded..511219d4167 100644 --- a/tests/Fixtures/app/config/config_test.yml +++ b/tests/Fixtures/app/config/config_test.yml @@ -241,3 +241,9 @@ services: app.dummy_validation.group_generator: class: ApiPlatform\Core\Tests\Fixtures\TestBundle\Validator\DummyValidationGroupsGenerator public: true + + app.dummy_input_data_persister: + class: ApiPlatform\Core\Tests\Fixtures\TestBundle\DataPersister\DummyInputDataPersister + public: false + tags: + - { name: 'api_platform.data_persister' } diff --git a/tests/Operation/Factory/SubresourceOperationFactoryTest.php b/tests/Operation/Factory/SubresourceOperationFactoryTest.php index 065039e35e9..fed4902fa44 100644 --- a/tests/Operation/Factory/SubresourceOperationFactoryTest.php +++ b/tests/Operation/Factory/SubresourceOperationFactoryTest.php @@ -66,6 +66,8 @@ public function testCreate() 'property' => 'subresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, + 'input_class' => RelatedDummyEntity::class, + 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -78,6 +80,8 @@ public function testCreate() 'property' => 'anotherSubresource', 'collection' => false, 'resource_class' => DummyEntity::class, + 'input_class' => DummyEntity::class, + 'output_class' => DummyEntity::class, 'shortNames' => ['dummyEntity', 'relatedDummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -91,6 +95,8 @@ public function testCreate() 'property' => 'subcollection', 'collection' => true, 'resource_class' => RelatedDummyEntity::class, + 'input_class' => RelatedDummyEntity::class, + 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -105,6 +111,8 @@ public function testCreate() 'property' => 'subcollection', 'collection' => true, 'resource_class' => RelatedDummyEntity::class, + 'input_class' => RelatedDummyEntity::class, + 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -117,6 +125,8 @@ public function testCreate() 'property' => 'anotherSubresource', 'collection' => false, 'resource_class' => DummyEntity::class, + 'input_class' => DummyEntity::class, + 'output_class' => DummyEntity::class, 'shortNames' => ['dummyEntity', 'relatedDummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -130,6 +140,8 @@ public function testCreate() 'property' => 'subresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, + 'input_class' => RelatedDummyEntity::class, + 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -184,6 +196,8 @@ public function testCreateByOverriding() 'property' => 'subresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, + 'input_class' => RelatedDummyEntity::class, + 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -196,6 +210,8 @@ public function testCreateByOverriding() 'property' => 'anotherSubresource', 'collection' => false, 'resource_class' => DummyEntity::class, + 'input_class' => DummyEntity::class, + 'output_class' => DummyEntity::class, 'shortNames' => ['dummyEntity', 'relatedDummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -209,6 +225,8 @@ public function testCreateByOverriding() 'property' => 'subcollection', 'collection' => true, 'resource_class' => RelatedDummyEntity::class, + 'input_class' => RelatedDummyEntity::class, + 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -223,6 +241,8 @@ public function testCreateByOverriding() 'property' => 'subcollection', 'collection' => true, 'resource_class' => RelatedDummyEntity::class, + 'input_class' => RelatedDummyEntity::class, + 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -235,6 +255,8 @@ public function testCreateByOverriding() 'property' => 'anotherSubresource', 'collection' => false, 'resource_class' => DummyEntity::class, + 'input_class' => DummyEntity::class, + 'output_class' => DummyEntity::class, 'shortNames' => ['dummyEntity', 'relatedDummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -248,6 +270,8 @@ public function testCreateByOverriding() 'property' => 'subresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, + 'input_class' => RelatedDummyEntity::class, + 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -295,6 +319,8 @@ public function testCreateWithMaxDepth() 'property' => 'subresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, + 'input_class' => RelatedDummyEntity::class, + 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -355,6 +381,8 @@ public function testCreateWithMaxDepthMultipleSubresources() 'property' => 'subresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, + 'input_class' => RelatedDummyEntity::class, + 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -367,6 +395,8 @@ public function testCreateWithMaxDepthMultipleSubresources() 'property' => 'secondSubresource', 'collection' => false, 'resource_class' => DummyValidatedEntity::class, + 'input_class' => DummyValidatedEntity::class, + 'output_class' => DummyValidatedEntity::class, 'shortNames' => ['dummyValidatedEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -379,6 +409,8 @@ public function testCreateWithMaxDepthMultipleSubresources() 'property' => 'moreSubresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, + 'input_class' => RelatedDummyEntity::class, + 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyValidatedEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -439,6 +471,8 @@ public function testCreateWithMaxDepthMultipleSubresourcesSameMaxDepth() 'property' => 'subresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, + 'input_class' => RelatedDummyEntity::class, + 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -451,6 +485,8 @@ public function testCreateWithMaxDepthMultipleSubresourcesSameMaxDepth() 'property' => 'secondSubresource', 'collection' => false, 'resource_class' => DummyValidatedEntity::class, + 'input_class' => DummyValidatedEntity::class, + 'output_class' => DummyValidatedEntity::class, 'shortNames' => ['dummyValidatedEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -494,6 +530,8 @@ public function testCreateSelfReferencingSubresources() 'property' => 'subresource', 'collection' => false, 'resource_class' => DummyEntity::class, + 'input_class' => DummyEntity::class, + 'output_class' => DummyEntity::class, 'shortNames' => ['dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -539,6 +577,8 @@ public function testCreateWithEnd() 'property' => 'subresource', 'collection' => true, 'resource_class' => RelatedDummyEntity::class, + 'input_class' => RelatedDummyEntity::class, + 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -551,6 +591,8 @@ public function testCreateWithEnd() 'property' => 'id', 'collection' => false, 'resource_class' => DummyEntity::class, + 'input_class' => DummyEntity::class, + 'output_class' => DummyEntity::class, 'shortNames' => ['dummyEntity', 'relatedDummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], @@ -597,6 +639,8 @@ public function testCreateWithEndButNoCollection() 'property' => 'subresource', 'collection' => false, 'resource_class' => RelatedDummyEntity::class, + 'input_class' => RelatedDummyEntity::class, + 'output_class' => RelatedDummyEntity::class, 'shortNames' => ['relatedDummyEntity', 'dummyEntity'], 'identifiers' => [ ['id', DummyEntity::class, true], diff --git a/tests/Serializer/SerializerContextBuilderTest.php b/tests/Serializer/SerializerContextBuilderTest.php index 4cbb79a1d01..612ef1f5918 100644 --- a/tests/Serializer/SerializerContextBuilderTest.php +++ b/tests/Serializer/SerializerContextBuilderTest.php @@ -55,32 +55,32 @@ public function testCreateFromRequest() { $request = Request::create('/foos/1'); $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); - $expected = ['foo' => 'bar', 'item_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'operation_type' => 'item', 'uri' => 'http://localhost/foos/1']; + $expected = ['foo' => 'bar', 'item_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'operation_type' => 'item', 'uri' => 'http://localhost/foos/1', 'denormalize_resource_class' => 'Foo', 'normalize_resource_class' => 'Foo']; $this->assertEquals($expected, $this->builder->createFromRequest($request, true)); $request = Request::create('/foos'); $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'pot', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); - $expected = ['foo' => 'bar', 'collection_operation_name' => 'pot', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'operation_type' => 'collection', 'uri' => 'http://localhost/foos']; + $expected = ['foo' => 'bar', 'collection_operation_name' => 'pot', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'operation_type' => 'collection', 'uri' => 'http://localhost/foos', 'denormalize_resource_class' => 'Foo', 'normalize_resource_class' => 'Foo']; $this->assertEquals($expected, $this->builder->createFromRequest($request, true)); $request = Request::create('/foos/1'); $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); - $expected = ['bar' => 'baz', 'item_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'operation_type' => 'item', 'uri' => 'http://localhost/foos/1']; + $expected = ['bar' => 'baz', 'item_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'operation_type' => 'item', 'uri' => 'http://localhost/foos/1', 'denormalize_resource_class' => 'Foo', 'normalize_resource_class' => 'Foo']; $this->assertEquals($expected, $this->builder->createFromRequest($request, false)); $request = Request::create('/foos', 'POST'); $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'post', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); - $expected = ['bar' => 'baz', 'collection_operation_name' => 'post', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => false, 'operation_type' => 'collection', 'uri' => 'http://localhost/foos']; + $expected = ['bar' => 'baz', 'collection_operation_name' => 'post', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => false, 'operation_type' => 'collection', 'uri' => 'http://localhost/foos', 'denormalize_resource_class' => 'Foo', 'normalize_resource_class' => 'Foo']; $this->assertEquals($expected, $this->builder->createFromRequest($request, false)); $request = Request::create('/foos', 'PUT'); $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'put', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); - $expected = ['bar' => 'baz', 'collection_operation_name' => 'put', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => true, 'operation_type' => 'collection', 'uri' => 'http://localhost/foos']; + $expected = ['bar' => 'baz', 'collection_operation_name' => 'put', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => true, 'operation_type' => 'collection', 'uri' => 'http://localhost/foos', 'denormalize_resource_class' => 'Foo', 'normalize_resource_class' => 'Foo']; $this->assertEquals($expected, $this->builder->createFromRequest($request, false)); $request = Request::create('/bars/1/foos'); $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_subresource_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); - $expected = ['bar' => 'baz', 'subresource_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/bars/1/foos', 'operation_type' => 'subresource', 'api_allow_update' => false, 'uri' => 'http://localhost/bars/1/foos']; + $expected = ['bar' => 'baz', 'subresource_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/bars/1/foos', 'operation_type' => 'subresource', 'api_allow_update' => false, 'uri' => 'http://localhost/bars/1/foos', 'denormalize_resource_class' => 'Foo', 'normalize_resource_class' => 'Foo']; $this->assertEquals($expected, $this->builder->createFromRequest($request, false)); } @@ -93,7 +93,7 @@ public function testThrowExceptionOnInvalidRequest() public function testReuseExistingAttributes() { - $expected = ['bar' => 'baz', 'item_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'operation_type' => 'item', 'uri' => 'http://localhost/foos/1']; + $expected = ['bar' => 'baz', 'item_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'operation_type' => 'item', 'uri' => 'http://localhost/foos/1', 'denormalize_resource_class' => 'Foo', 'normalize_resource_class' => 'Foo']; $this->assertEquals($expected, $this->builder->createFromRequest(Request::create('/foos/1'), false, ['resource_class' => 'Foo', 'item_operation_name' => 'get'])); } } diff --git a/tests/Serializer/SerializerFilterContextBuilderTest.php b/tests/Serializer/SerializerFilterContextBuilderTest.php index 2ebd1708c7f..fc57de6de19 100644 --- a/tests/Serializer/SerializerFilterContextBuilderTest.php +++ b/tests/Serializer/SerializerFilterContextBuilderTest.php @@ -36,6 +36,8 @@ public function testCreateFromRequestWithCollectionOperation() $attributes = [ 'resource_class' => DummyGroup::class, + 'input_resource_class' => DummyGroup::class, + 'output_resource_class' => DummyGroup::class, 'collection_operation_name' => 'get', ]; @@ -77,6 +79,8 @@ public function testCreateFromRequestWithItemOperation() $attributes = [ 'resource_class' => DummyGroup::class, + 'input_resource_class' => DummyGroup::class, + 'output_resource_class' => DummyGroup::class, 'item_operation_name' => 'put', ]; @@ -118,6 +122,8 @@ public function testCreateFromRequestWithoutFilters() $attributes = [ 'resource_class' => DummyGroup::class, + 'input_resource_class' => DummyGroup::class, + 'output_resource_class' => DummyGroup::class, 'collection_operation_name' => 'get', ]; @@ -152,6 +158,8 @@ public function testCreateFromRequestWithoutAttributes() $attributes = [ 'resource_class' => DummyGroup::class, + 'input_resource_class' => DummyGroup::class, + 'output_resource_class' => DummyGroup::class, 'collection_operation_name' => 'get', 'receive' => true, 'persist' => true, diff --git a/tests/Util/RequestAttributesExtractorTest.php b/tests/Util/RequestAttributesExtractorTest.php index c9bda2cfa7b..589526dd434 100644 --- a/tests/Util/RequestAttributesExtractorTest.php +++ b/tests/Util/RequestAttributesExtractorTest.php @@ -27,7 +27,14 @@ public function testExtractCollectionAttributes() $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'post']); $this->assertEquals( - ['resource_class' => 'Foo', 'collection_operation_name' => 'post', 'receive' => true, 'persist' => true], + [ + 'resource_class' => 'Foo', + 'collection_operation_name' => 'post', + 'input_resource_class' => 'Foo', + 'output_resource_class' => 'Foo', + 'receive' => true, + 'persist' => true, + ], RequestAttributesExtractor::extractAttributes($request) ); } @@ -37,7 +44,14 @@ public function testExtractItemAttributes() $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get']); $this->assertEquals( - ['resource_class' => 'Foo', 'item_operation_name' => 'get', 'receive' => true, 'persist' => true], + [ + 'resource_class' => 'Foo', + 'item_operation_name' => 'get', + 'input_resource_class' => 'Foo', + 'output_resource_class' => 'Foo', + 'receive' => true, + 'persist' => true, + ], RequestAttributesExtractor::extractAttributes($request) ); } @@ -47,21 +61,42 @@ public function testExtractReceive() $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_receive' => '0']); $this->assertEquals( - ['resource_class' => 'Foo', 'item_operation_name' => 'get', 'receive' => false, 'persist' => true], + [ + 'resource_class' => 'Foo', + 'item_operation_name' => 'get', + 'input_resource_class' => 'Foo', + 'output_resource_class' => 'Foo', + 'receive' => false, + 'persist' => true, + ], RequestAttributesExtractor::extractAttributes($request) ); $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_receive' => '1']); $this->assertEquals( - ['resource_class' => 'Foo', 'item_operation_name' => 'get', 'receive' => true, 'persist' => true], + [ + 'resource_class' => 'Foo', + 'item_operation_name' => 'get', + 'input_resource_class' => 'Foo', + 'output_resource_class' => 'Foo', + 'receive' => true, + 'persist' => true, + ], RequestAttributesExtractor::extractAttributes($request) ); $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get']); $this->assertEquals( - ['resource_class' => 'Foo', 'item_operation_name' => 'get', 'receive' => true, 'persist' => true], + [ + 'resource_class' => 'Foo', + 'input_resource_class' => 'Foo', + 'output_resource_class' => 'Foo', + 'item_operation_name' => 'get', + 'receive' => true, + 'persist' => true, + ], RequestAttributesExtractor::extractAttributes($request) ); } @@ -71,21 +106,92 @@ public function testExtractPersist() $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_persist' => '0']); $this->assertEquals( - ['resource_class' => 'Foo', 'item_operation_name' => 'get', 'receive' => true, 'persist' => false], + [ + 'resource_class' => 'Foo', + 'input_resource_class' => 'Foo', + 'output_resource_class' => 'Foo', + 'item_operation_name' => 'get', + 'receive' => true, + 'persist' => false, + ], RequestAttributesExtractor::extractAttributes($request) ); $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_persist' => '1']); $this->assertEquals( - ['resource_class' => 'Foo', 'item_operation_name' => 'get', 'receive' => true, 'persist' => true], + [ + 'resource_class' => 'Foo', + 'input_resource_class' => 'Foo', + 'output_resource_class' => 'Foo', + 'item_operation_name' => 'get', + 'receive' => true, + 'persist' => true, + ], RequestAttributesExtractor::extractAttributes($request) ); $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get']); $this->assertEquals( - ['resource_class' => 'Foo', 'item_operation_name' => 'get', 'receive' => true, 'persist' => true], + [ + 'resource_class' => 'Foo', + 'input_resource_class' => 'Foo', + 'output_resource_class' => 'Foo', + 'item_operation_name' => 'get', + 'receive' => true, + 'persist' => true, + ], + RequestAttributesExtractor::extractAttributes($request) + ); + } + + public function testExtractInputOutputResourceClass() + { + $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_input_resource_class' => 'Bar']); + + $this->assertEquals( + [ + 'resource_class' => 'Foo', + 'item_operation_name' => 'get', + 'input_resource_class' => 'Bar', + 'output_resource_class' => 'Foo', + 'receive' => true, + 'persist' => true, + ], + RequestAttributesExtractor::extractAttributes($request) + ); + + $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_output_resource_class' => 'Bar']); + + $this->assertEquals( + [ + 'resource_class' => 'Foo', + 'item_operation_name' => 'get', + 'input_resource_class' => 'Foo', + 'output_resource_class' => 'Bar', + 'receive' => true, + 'persist' => true, + ], + RequestAttributesExtractor::extractAttributes($request) + ); + + $request = new Request([], [], [ + '_api_resource_class' => 'Foo', + '_api_item_operation_name' => 'get', + '_api_input_resource_class' => 'FooBar', + '_api_output_resource_class' => 'Bar', + ]); + + $this->assertEquals( + [ + 'resource_class' => 'Foo', + 'item_operation_name' => 'get', + 'input_resource_class' => 'FooBar', + 'output_resource_class' => 'Bar', + 'receive' => true, + 'persist' => true, + ], RequestAttributesExtractor::extractAttributes($request) ); }