Skip to content

Commit 80ba354

Browse files
committed
Allow an input and an output for a given resource class
1 parent 549214a commit 80ba354

22 files changed

+393
-24
lines changed

src/Annotation/ApiResource.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@
3636
* @Attribute("filters", type="string[]"),
3737
* @Attribute("graphql", type="array"),
3838
* @Attribute("hydraContext", type="array"),
39+
* @Attribute("inputClass", type="string"),
3940
* @Attribute("iri", type="string"),
4041
* @Attribute("itemOperations", type="array"),
4142
* @Attribute("maximumItemsPerPage", type="int"),
4243
* @Attribute("normalizationContext", type="array"),
4344
* @Attribute("order", type="array"),
45+
* @Attribute("outputClass", type="string"),
4446
* @Attribute("paginationClientEnabled", type="bool"),
4547
* @Attribute("paginationClientItemsPerPage", type="bool"),
4648
* @Attribute("paginationClientPartial", type="bool"),
@@ -256,6 +258,20 @@ final class ApiResource
256258
*/
257259
private $sunset;
258260

261+
/**
262+
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
263+
*
264+
* @var string
265+
*/
266+
private $inputClass;
267+
268+
/**
269+
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
270+
*
271+
* @var string
272+
*/
273+
private $outputClass;
274+
259275
/**
260276
* @throws InvalidArgumentException
261277
*/

src/Bridge/Symfony/Routing/ApiLoader.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ public function load($data, $type = null): RouteCollection
123123
'_controller' => $controller,
124124
'_format' => null,
125125
'_api_resource_class' => $operation['resource_class'],
126+
'_api_input_resource_class' => $operation['input_class'],
127+
'_api_output_resource_class' => $operation['output_class'],
126128
'_api_subresource_operation_name' => $operation['route_name'],
127129
'_api_subresource_context' => [
128130
'property' => $operation['property'],
@@ -210,6 +212,8 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas
210212
'_controller' => $controller,
211213
'_format' => null,
212214
'_api_resource_class' => $resourceClass,
215+
'_api_input_resource_class' => $resourceMetadata->getAttribute('input_class', $resourceClass),
216+
'_api_output_resource_class' => $resourceMetadata->getAttribute('output_class', $resourceClass),
213217
sprintf('_api_%s_operation_name', $operationType) => $operationName,
214218
] + ($operation['defaults'] ?? []),
215219
$operation['requirements'] ?? [],

src/DataProvider/OperationDataProviderTrait.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ trait OperationDataProviderTrait
4949
*/
5050
private function getCollectionData(array $attributes, array $context)
5151
{
52-
return $this->collectionDataProvider->getCollection($attributes['resource_class'], $attributes['collection_operation_name'], $context);
52+
return $this->collectionDataProvider->getCollection($attributes['output_resource_class'], $attributes['collection_operation_name'], $context);
5353
}
5454

5555
/**
@@ -59,7 +59,7 @@ private function getCollectionData(array $attributes, array $context)
5959
*/
6060
private function getItemData($identifiers, array $attributes, array $context)
6161
{
62-
return $this->itemDataProvider->getItem($attributes['resource_class'], $identifiers, $attributes['item_operation_name'], $context);
62+
return $this->itemDataProvider->getItem($attributes['output_resource_class'], $identifiers, $attributes['item_operation_name'], $context);
6363
}
6464

6565
/**
@@ -75,7 +75,7 @@ private function getSubresourceData($identifiers, array $attributes, array $cont
7575
throw new RuntimeException('Subresources not supported');
7676
}
7777

78-
return $this->subresourceDataProvider->getSubresource($attributes['resource_class'], $identifiers, $attributes['subresource_context'] + $context, $attributes['subresource_operation_name']);
78+
return $this->subresourceDataProvider->getSubresource($attributes['output_resource_class'], $identifiers, $attributes['subresource_context'] + $context, $attributes['subresource_operation_name']);
7979
}
8080

8181
/**
@@ -93,7 +93,7 @@ private function extractIdentifiers(array $parameters, array $attributes)
9393
$id = $parameters['id'];
9494

9595
if (null !== $this->identifierConverter) {
96-
return $this->identifierConverter->convert((string) $id, $attributes['resource_class']);
96+
return $this->identifierConverter->convert((string) $id, $attributes['output_resource_class']);
9797
}
9898

9999
return $id;

src/EventListener/DeserializeListener.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public function onKernelRequest(GetResponseEvent $event)
9090
$request->attributes->set(
9191
'data',
9292
$this->serializer->deserialize(
93-
$requestContent, $attributes['resource_class'], $format, $context
93+
$requestContent, $attributes['input_resource_class'], $format, $context
9494
)
9595
);
9696
}

src/EventListener/WriteListener.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ public function onKernelView(GetResponseForControllerResultEvent $event)
6161

6262
$event->setControllerResult($persistResult ?? $controllerResult);
6363

64-
if (null !== $this->iriConverter) {
64+
// Comparing the class is necessary because the input might not be readable
65+
if (null !== $this->iriConverter && \get_class($controllerResult) === \get_class($event->getControllerResult())) {
6566
$request->attributes->set('_api_write_item_iri', $this->iriConverter->getIriFromItem($controllerResult));
6667
}
6768
break;

src/Operation/Factory/SubresourceOperationFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ private function computeSubresourceOperations(string $resourceClass, array &$tre
106106
'collection' => $subresource->isCollection(),
107107
'resource_class' => $subresourceClass,
108108
'shortNames' => [$subresourceMetadata->getShortName()],
109+
'input_class' => $subresourceMetadata->getAttribute('input_class', $subresourceClass),
110+
'output_class' => $subresourceMetadata->getAttribute('output_class', $subresourceClass),
109111
];
110112

111113
if (null === $parentOperation) {

src/Serializer/AbstractItemNormalizer.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ public function hasCacheableSupportsMethod(): bool
9696
*/
9797
public function normalize($object, $format = null, array $context = [])
9898
{
99+
if (isset($context['resource_class']) && isset($context['normalize_resource_class'])) {
100+
$context['resource_class'] = $context['normalize_resource_class'];
101+
}
102+
99103
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null, true);
100104
$context = $this->initContext($resourceClass, $context);
101105
$context['api_normalize'] = true;
@@ -124,6 +128,10 @@ public function denormalize($data, $class, $format = null, array $context = [])
124128
$context['api_denormalize'] = true;
125129
if (!isset($context['resource_class'])) {
126130
$context['resource_class'] = $class;
131+
132+
if (isset($context['denormalize_resource_class'])) {
133+
$context['resource_class'] = $context['denormalize_resource_class'];
134+
}
127135
}
128136

129137
return parent::denormalize($data, $class, $format, $context);

src/Serializer/SerializerContextBuilder.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ public function createFromRequest(Request $request, bool $normalization, array $
7373
}
7474

7575
$context['resource_class'] = $attributes['resource_class'];
76+
$context['denormalize_resource_class'] = $attributes['input_resource_class'] ?? $attributes['resource_class'];
77+
$context['normalize_resource_class'] = $attributes['output_resource_class'] ?? $attributes['resource_class'];
7678
$context['request_uri'] = $request->getRequestUri();
7779
$context['uri'] = $request->getUri();
7880

src/Util/AttributesExtractor.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ public static function extractAttributes(array $attributes): array
3636
{
3737
$result = ['resource_class' => $attributes['_api_resource_class'] ?? null];
3838

39+
$result['input_resource_class'] = $attributes['_api_input_resource_class'] ?? $result['resource_class'];
40+
$result['output_resource_class'] = $attributes['_api_output_resource_class'] ?? $result['resource_class'];
41+
3942
if ($subresourceContext = $attributes['_api_subresource_context'] ?? null) {
4043
$result['subresource_context'] = $subresourceContext;
4144
}

tests/Bridge/Symfony/Bundle/DataCollector/RequestDataCollectorTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public function testWithResource()
107107
$this->response
108108
);
109109

110-
$this->assertSame(['resource_class' => DummyEntity::class, 'item_operation_name' => 'get', 'receive' => true, 'persist' => true], $dataCollector->getRequestAttributes());
110+
$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());
111111
$this->assertSame(['foo', 'bar'], $dataCollector->getAcceptableContentTypes());
112112
$this->assertSame(DummyEntity::class, $dataCollector->getResourceClass());
113113
$this->assertSame(['foo' => null, 'a_filter' => \stdClass::class], $dataCollector->getFilters());

tests/Bridge/Symfony/Routing/ApiLoaderTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ private function getRoute(string $path, string $controller, string $resourceClas
305305
'_controller' => $controller,
306306
'_format' => null,
307307
'_api_resource_class' => $resourceClass,
308+
'_api_input_resource_class' => $resourceClass,
309+
'_api_output_resource_class' => $resourceClass,
308310
sprintf('_api_%s_operation_name', $collection ? 'collection' : 'item') => $operationName,
309311
] + $extraDefaults,
310312
$requirements,
@@ -324,6 +326,8 @@ private function getSubresourceRoute(string $path, string $controller, string $r
324326
'_controller' => $controller,
325327
'_format' => null,
326328
'_api_resource_class' => $resourceClass,
329+
'_api_input_resource_class' => $resourceClass,
330+
'_api_output_resource_class' => $resourceClass,
327331
'_api_subresource_operation_name' => $operationName,
328332
'_api_subresource_context' => $context,
329333
],

tests/EventListener/AddFormatListenerTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ public function testResourceClassSupportedRequestFormat()
224224
$event = $eventProphecy->reveal();
225225

226226
$formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class);
227-
$formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'collection_operation_name' => 'get', 'receive' => true, 'persist' => true])->willReturn(['csv' => ['text/csv']])->shouldBeCalled();
227+
$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();
228228

229229
$listener = new AddFormatListener(new Negotiator(), $formatsProviderProphecy->reveal());
230230
$listener->onKernelRequest($event);

tests/EventListener/DeserializeListenerTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ public function testDeserializeResourceClassSupportedFormat(string $method, bool
172172
$serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn([])->shouldBeCalled();
173173

174174
$formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class);
175-
$formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'collection_operation_name' => 'post', 'receive' => true, 'persist' => true])->willReturn(self::FORMATS)->shouldBeCalled();
175+
$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();
176176

177177
$listener = new DeserializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), $formatsProviderProphecy->reveal());
178178

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\DataPersister;
15+
16+
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
17+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyInput;
18+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyOutput;
19+
20+
class DummyInputDataPersister implements DataPersisterInterface
21+
{
22+
public function supports($data): bool
23+
{
24+
return $data instanceof DummyInput;
25+
}
26+
27+
/**
28+
* {@inheritdoc}
29+
*/
30+
public function persist($data)
31+
{
32+
$output = new DummyOutput();
33+
$output->name = $data->name;
34+
$output->id = 1;
35+
36+
return $output;
37+
}
38+
39+
/**
40+
* {@inheritdoc}
41+
*/
42+
public function remove($data)
43+
{
44+
return null;
45+
}
46+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Core\Annotation\ApiProperty;
17+
use ApiPlatform\Core\Annotation\ApiResource;
18+
19+
/**
20+
* Dummy Input.
21+
*
22+
* @author Kévin Dunglas <[email protected]>
23+
*
24+
* @ApiResource
25+
*/
26+
class DummyInput
27+
{
28+
/**
29+
* @var int The id
30+
* @ApiProperty(identifier=true)
31+
*/
32+
public $id;
33+
34+
/**
35+
* @var string The dummy name
36+
*
37+
* @ApiProperty
38+
*/
39+
public $name;
40+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Core\Annotation\ApiProperty;
17+
use ApiPlatform\Core\Annotation\ApiResource;
18+
19+
/**
20+
* Dummy InputOutput.
21+
*
22+
* @author Kévin Dunglas <[email protected]>
23+
*
24+
* @ApiResource(attributes={"input_class"=DummyInput::class, "output_class"=DummyOutput::class})
25+
*/
26+
class DummyInputOutput
27+
{
28+
/**
29+
* @var int The id
30+
* @ApiProperty(identifier=true)
31+
*/
32+
public $id;
33+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Core\Annotation\ApiProperty;
17+
use ApiPlatform\Core\Annotation\ApiResource;
18+
use Doctrine\ORM\Mapping as ORM;
19+
20+
/**
21+
* Dummy Output.
22+
*
23+
* @author Kévin Dunglas <[email protected]>
24+
*
25+
* @ApiResource
26+
* @ORM\Entity
27+
*/
28+
class DummyOutput
29+
{
30+
/**
31+
* @var int The id
32+
*
33+
* @ORM\Column(type="integer")
34+
* @ORM\Id
35+
* @ORM\GeneratedValue(strategy="AUTO")
36+
*/
37+
public $id;
38+
39+
/**
40+
* @var string The dummy name
41+
*
42+
* @ORM\Column
43+
* @ApiProperty(iri="http://schema.org/name")
44+
*/
45+
public $name;
46+
}

tests/Fixtures/app/config/config_test.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,9 @@ services:
241241
app.dummy_validation.group_generator:
242242
class: ApiPlatform\Core\Tests\Fixtures\TestBundle\Validator\DummyValidationGroupsGenerator
243243
public: true
244+
245+
app.dummy_input_data_persister:
246+
class: ApiPlatform\Core\Tests\Fixtures\TestBundle\DataPersister\DummyInputDataPersister
247+
public: false
248+
tags:
249+
- { name: 'api_platform.data_persister' }

0 commit comments

Comments
 (0)