diff --git a/src/Annotation/ApiResource.php b/src/Annotation/ApiResource.php index 54e74773138..6b221c36c30 100644 --- a/src/Annotation/ApiResource.php +++ b/src/Annotation/ApiResource.php @@ -37,11 +37,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"), @@ -264,6 +266,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..991787712ec 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_class' => $operation['input_class'], + '_api_output_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_class' => $resourceMetadata->getAttribute('input_class', $resourceClass), + '_api_output_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..5c9f31e903d 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_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_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_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_class']); } return $id; diff --git a/src/EventListener/DeserializeListener.php b/src/EventListener/DeserializeListener.php index 68058549160..d977823eed1 100644 --- a/src/EventListener/DeserializeListener.php +++ b/src/EventListener/DeserializeListener.php @@ -81,6 +81,9 @@ public function onKernelRequest(GetResponseEvent $event) $format = $this->getFormat($request); $context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes); + if (isset($context['input_class'])) { + $context['resource_class'] = $context['input_class']; + } $data = $request->attributes->get('data'); if (null !== $data) { @@ -90,7 +93,7 @@ public function onKernelRequest(GetResponseEvent $event) $request->attributes->set( 'data', $this->serializer->deserialize( - $requestContent, $attributes['resource_class'], $format, $context + $requestContent, $attributes['input_class'], $format, $context ) ); } diff --git a/src/EventListener/SerializeListener.php b/src/EventListener/SerializeListener.php index 80e3b0602a7..3f47a476208 100644 --- a/src/EventListener/SerializeListener.php +++ b/src/EventListener/SerializeListener.php @@ -63,6 +63,10 @@ public function onKernelView(GetResponseForControllerResultEvent $event) } $resources = new ResourceList(); $context['resources'] = &$resources; + if (isset($context['output_class'])) { + $context['resource_class'] = $context['output_class']; + } + $request->attributes->set('_api_normalization_context', $context); $event->setControllerResult($this->serializer->serialize($controllerResult, $request->getRequestFormat(), $context)); diff --git a/src/EventListener/WriteListener.php b/src/EventListener/WriteListener.php index 3c9e6364cde..c0933cc2a13 100644 --- a/src/EventListener/WriteListener.php +++ b/src/EventListener/WriteListener.php @@ -61,7 +61,10 @@ public function onKernelView(GetResponseForControllerResultEvent $event) $event->setControllerResult($persistResult ?? $controllerResult); - if (null !== $this->iriConverter) { + // Controller result must be immutable for _api_write_item_iri + // if it's class changed compared to the base class let's avoid calling the IriConverter + // especially that the Output class could be a DTO that's not referencing any route + 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 49062af7d0b..cc4cb051673 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/SerializerContextBuilder.php b/src/Serializer/SerializerContextBuilder.php index f760f08635b..28267939d78 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['input_class'] = $attributes['input_class'] ?? $attributes['resource_class']; + $context['output_class'] = $attributes['output_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..da8f6e2742e 100644 --- a/src/Util/AttributesExtractor.php +++ b/src/Util/AttributesExtractor.php @@ -35,6 +35,8 @@ private function __construct() public static function extractAttributes(array $attributes): array { $result = ['resource_class' => $attributes['_api_resource_class'] ?? null]; + $result['input_class'] = $attributes['_api_input_class'] ?? $result['resource_class']; + $result['output_class'] = $attributes['_api_output_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 44139b29de9..034058399fb 100644 --- a/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php +++ b/tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php @@ -136,7 +136,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_class' => DummyEntity::class, 'output_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..ec9ecf162c9 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_class' => $resourceClass, + '_api_output_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_class' => $resourceClass, + '_api_output_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..014597a3f16 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_class' => 'Foo', 'output_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..24e2876280b 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_class' => 'Foo', 'output_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 0f4521b0399..7cc87b44c5d 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], @@ -646,6 +690,8 @@ public function testCreateWithRootResourcePrefix() 'identifiers' => [ ['id', DummyEntity::class, true], ], + 'input_class' => RelatedDummyEntity::class, + 'output_class' => RelatedDummyEntity::class, 'route_name' => 'api_dummy_entities_subresource_get_subresource', 'path' => '/root_resource_prefix/dummy_entities/{id}/subresource.{_format}', 'operation_name' => 'subresource_get_subresource', diff --git a/tests/Serializer/SerializerContextBuilderTest.php b/tests/Serializer/SerializerContextBuilderTest.php index 4cbb79a1d01..08f539522c7 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', 'output_class' => 'Foo', 'input_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', 'output_class' => 'Foo', 'input_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', 'output_class' => 'Foo', 'input_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', 'output_class' => 'Foo', 'input_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', 'output_class' => 'Foo', 'input_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', 'output_class' => 'Foo', 'input_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', 'output_class' => 'Foo', 'input_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..6ff7d027f9d 100644 --- a/tests/Serializer/SerializerFilterContextBuilderTest.php +++ b/tests/Serializer/SerializerFilterContextBuilderTest.php @@ -36,6 +36,8 @@ public function testCreateFromRequestWithCollectionOperation() $attributes = [ 'resource_class' => DummyGroup::class, + 'input_class' => DummyGroup::class, + 'output_class' => DummyGroup::class, 'collection_operation_name' => 'get', ]; @@ -77,6 +79,8 @@ public function testCreateFromRequestWithItemOperation() $attributes = [ 'resource_class' => DummyGroup::class, + 'input_class' => DummyGroup::class, + 'output_class' => DummyGroup::class, 'item_operation_name' => 'put', ]; @@ -118,6 +122,8 @@ public function testCreateFromRequestWithoutFilters() $attributes = [ 'resource_class' => DummyGroup::class, + 'input_class' => DummyGroup::class, + 'output_class' => DummyGroup::class, 'collection_operation_name' => 'get', ]; @@ -152,6 +158,8 @@ public function testCreateFromRequestWithoutAttributes() $attributes = [ 'resource_class' => DummyGroup::class, + 'input_class' => DummyGroup::class, + 'output_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..f383585ed74 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_class' => 'Foo', + 'output_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_class' => 'Foo', + 'output_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_class' => 'Foo', + 'output_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_class' => 'Foo', + 'output_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_class' => 'Foo', + 'output_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_class' => 'Foo', + 'output_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_class' => 'Foo', + 'output_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_class' => 'Foo', + 'output_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_class' => 'Bar']); + + $this->assertEquals( + [ + 'resource_class' => 'Foo', + 'item_operation_name' => 'get', + 'input_class' => 'Bar', + 'output_class' => 'Foo', + 'receive' => true, + 'persist' => true, + ], + RequestAttributesExtractor::extractAttributes($request) + ); + + $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_output_class' => 'Bar']); + + $this->assertEquals( + [ + 'resource_class' => 'Foo', + 'item_operation_name' => 'get', + 'input_class' => 'Foo', + 'output_class' => 'Bar', + 'receive' => true, + 'persist' => true, + ], + RequestAttributesExtractor::extractAttributes($request) + ); + + $request = new Request([], [], [ + '_api_resource_class' => 'Foo', + '_api_item_operation_name' => 'get', + '_api_input_class' => 'FooBar', + '_api_output_class' => 'Bar', + ]); + + $this->assertEquals( + [ + 'resource_class' => 'Foo', + 'item_operation_name' => 'get', + 'input_class' => 'FooBar', + 'output_class' => 'Bar', + 'receive' => true, + 'persist' => true, + ], RequestAttributesExtractor::extractAttributes($request) ); }