Skip to content

Commit e10d658

Browse files
authored
input and output class refactoring (#2483)
* input and output class refactoring * Fix WriteListener * Fix unit tests
1 parent 7a272e8 commit e10d658

19 files changed

+108
-182
lines changed

src/Api/IdentifiersExtractor.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use ApiPlatform\Core\Exception\RuntimeException;
1717
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
1818
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
19+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1920
use ApiPlatform\Core\Util\ClassInfoTrait;
2021
use Symfony\Component\PropertyAccess\PropertyAccess;
2122
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
@@ -33,13 +34,15 @@ final class IdentifiersExtractor implements IdentifiersExtractorInterface
3334
private $propertyMetadataFactory;
3435
private $propertyAccessor;
3536
private $resourceClassResolver;
37+
private $resourceMetadataFactory;
3638

37-
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PropertyAccessorInterface $propertyAccessor = null, ResourceClassResolverInterface $resourceClassResolver = null)
39+
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PropertyAccessorInterface $propertyAccessor = null, ResourceClassResolverInterface $resourceClassResolver = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
3840
{
3941
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
4042
$this->propertyMetadataFactory = $propertyMetadataFactory;
4143
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
4244
$this->resourceClassResolver = $resourceClassResolver;
45+
$this->resourceMetadataFactory = $resourceMetadataFactory;
4346

4447
if (null === $this->resourceClassResolver) {
4548
@trigger_error(sprintf('Not injecting %s in the CachedIdentifiersExtractor might introduce cache issues with object identifiers.', ResourceClassResolverInterface::class), E_USER_DEPRECATED);

src/Bridge/Symfony/Bundle/Resources/config/api.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
<service id="api_platform.listener.view.write" class="ApiPlatform\Core\EventListener\WriteListener">
157157
<argument type="service" id="api_platform.data_persister" />
158158
<argument type="service" id="api_platform.iri_converter" on-invalid="null" />
159+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
159160

160161
<tag name="kernel.event_listener" event="kernel.view" method="onKernelView" priority="32" />
161162
</service>

src/Bridge/Symfony/Routing/ApiLoader.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,6 @@ public function load($data, $type = null): RouteCollection
123123
'_controller' => $controller,
124124
'_format' => null,
125125
'_api_resource_class' => $operation['resource_class'],
126-
'_api_input_class' => $operation['input_class'],
127-
'_api_output_class' => $operation['output_class'],
128126
'_api_subresource_operation_name' => $operation['route_name'],
129127
'_api_subresource_context' => [
130128
'property' => $operation['property'],
@@ -212,8 +210,6 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas
212210
'_controller' => $controller,
213211
'_format' => null,
214212
'_api_resource_class' => $resourceClass,
215-
'_api_input_class' => $resourceMetadata->getAttribute('input_class', $resourceClass),
216-
'_api_output_class' => $resourceMetadata->getAttribute('output_class', $resourceClass),
217213
sprintf('_api_%s_operation_name', $operationType) => $operationName,
218214
] + ($operation['defaults'] ?? []),
219215
$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['output_class'] ?: $attributes['resource_class'], $attributes['collection_operation_name'], $context);
52+
return $this->collectionDataProvider->getCollection($attributes['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['output_class'] ?: $attributes['resource_class'], $identifiers, $attributes['item_operation_name'], $context);
62+
return $this->itemDataProvider->getItem($attributes['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['output_class'] ?: $attributes['resource_class'], $identifiers, $attributes['subresource_context'] + $context, $attributes['subresource_operation_name']);
78+
return $this->subresourceDataProvider->getSubresource($attributes['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['output_class'] ?: $attributes['resource_class']);
96+
return $this->identifierConverter->convert((string) $id, $attributes['resource_class']);
9797
}
9898

9999
return $id;

src/EventListener/DeserializeListener.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,11 @@ public function onKernelRequest(GetResponseEvent $event)
6363
{
6464
$request = $event->getRequest();
6565
$method = $request->getMethod();
66+
6667
if (
6768
'DELETE' === $method
6869
|| $request->isMethodSafe(false)
6970
|| !($attributes = RequestAttributesExtractor::extractAttributes($request))
70-
|| false === ($attributes['input_class'] ?? null)
7171
|| !$attributes['receive']
7272
|| (
7373
'' === ($requestContent = $request->getContent())
@@ -76,17 +76,18 @@ public function onKernelRequest(GetResponseEvent $event)
7676
) {
7777
return;
7878
}
79+
80+
$context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes);
81+
if (false === $context['input_class']) {
82+
return;
83+
}
84+
7985
// BC check to be removed in 3.0
8086
if (null !== $this->formatsProvider) {
8187
$this->formats = $this->formatsProvider->getFormatsFromAttributes($attributes);
8288
}
8389
$this->formatMatcher = new FormatMatcher($this->formats);
84-
8590
$format = $this->getFormat($request);
86-
$context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes);
87-
if (isset($context['input_class'])) {
88-
$context['resource_class'] = $context['input_class'];
89-
}
9091

9192
$data = $request->attributes->get('data');
9293
if (null !== $data) {
@@ -96,7 +97,7 @@ public function onKernelRequest(GetResponseEvent $event)
9697
$request->attributes->set(
9798
'data',
9899
$this->serializer->deserialize(
99-
$requestContent, $attributes['input_class'], $format, $context
100+
$requestContent, $context['input_class'], $format, $context
100101
)
101102
);
102103
}

src/EventListener/WriteListener.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use ApiPlatform\Core\Api\IriConverterInterface;
1717
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
18+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1819
use ApiPlatform\Core\Util\RequestAttributesExtractor;
1920
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
2021

@@ -28,11 +29,13 @@ final class WriteListener
2829
{
2930
private $dataPersister;
3031
private $iriConverter;
32+
private $resourceMetadataFactory;
3133

32-
public function __construct(DataPersisterInterface $dataPersister, IriConverterInterface $iriConverter = null)
34+
public function __construct(DataPersisterInterface $dataPersister, IriConverterInterface $iriConverter = null, ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
3335
{
3436
$this->dataPersister = $dataPersister;
3537
$this->iriConverter = $iriConverter;
38+
$this->resourceMetadataFactory = $resourceMetadataFactory;
3639
}
3740

3841
/**
@@ -65,10 +68,21 @@ public function onKernelView(GetResponseForControllerResultEvent $event)
6568
// Controller result must be immutable for _api_write_item_iri
6669
// if it's class changed compared to the base class let's avoid calling the IriConverter
6770
// especially that the Output class could be a DTO that's not referencing any route
68-
if (null !== $this->iriConverter && (false !== $attributes['output_class'] ?? null) && $attributes['resource_class'] === ($class = \get_class($controllerResult)) && $class === \get_class($event->getControllerResult())) {
71+
if (null === $this->iriConverter) {
72+
return;
73+
}
74+
75+
$hasOutput = true;
76+
if (null !== $this->resourceMetadataFactory) {
77+
$resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']);
78+
$hasOutput = false !== $resourceMetadata->getOperationAttribute($attributes, 'output_class', null, true);
79+
}
80+
81+
$class = \get_class($controllerResult);
82+
if ($hasOutput && $attributes['resource_class'] === $class && $class === \get_class($event->getControllerResult())) {
6983
$request->attributes->set('_api_write_item_iri', $this->iriConverter->getIriFromItem($controllerResult));
7084
}
71-
break;
85+
break;
7286
case 'DELETE':
7387
$this->dataPersister->remove($controllerResult);
7488
$event->setControllerResult(null);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\Identifier;
15+
16+
/**
17+
* Identifier converter.
18+
*
19+
* @author Antoine Bluchet <soyuka@gmail.com>
20+
*/
21+
22+
namespace ApiPlatform\Core\Identifier;
23+
24+
/**
25+
* Gives access to the context.
26+
*
27+
* @author Kévin Dunglas <dunglas@gmail.com>
28+
*/
29+
interface ContextAwareIdentifierConverterInterface extends IdentifierConverterInterface
30+
{
31+
/**
32+
* {@inheritdoc}
33+
*/
34+
public function convert(string $data, string $class, array $context = []): array;
35+
}

src/Identifier/IdentifierConverter.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,39 @@
1616
use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
1717
use ApiPlatform\Core\Exception\InvalidIdentifierException;
1818
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
19+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1920
use Symfony\Component\PropertyInfo\Type;
2021

2122
/**
2223
* Identifier converter that chains identifier denormalizers.
2324
*
2425
* @author Antoine Bluchet <soyuka@gmail.com>
2526
*/
26-
final class IdentifierConverter implements IdentifierConverterInterface
27+
final class IdentifierConverter implements ContextAwareIdentifierConverterInterface
2728
{
2829
private $propertyMetadataFactory;
2930
private $identifiersExtractor;
3031
private $identifierDenormalizers;
32+
private $resourceMetadataFactory;
3133

32-
public function __construct(IdentifiersExtractorInterface $identifiersExtractor, PropertyMetadataFactoryInterface $propertyMetadataFactory, $identifierDenormalizers)
34+
public function __construct(IdentifiersExtractorInterface $identifiersExtractor, PropertyMetadataFactoryInterface $propertyMetadataFactory, $identifierDenormalizers, ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
3335
{
3436
$this->propertyMetadataFactory = $propertyMetadataFactory;
3537
$this->identifiersExtractor = $identifiersExtractor;
3638
$this->identifierDenormalizers = $identifierDenormalizers;
39+
$this->resourceMetadataFactory = $resourceMetadataFactory;
3740
}
3841

3942
/**
4043
* {@inheritdoc}
4144
*/
42-
public function convert(string $data, string $class): array
45+
public function convert(string $data, string $class, array $context = []): array
4346
{
47+
if (null !== $this->resourceMetadataFactory) {
48+
$resourceMetadata = $this->resourceMetadataFactory->create($class);
49+
$class = $resourceMetadata->getOperationAttribute($context, 'output_class', $class, true);
50+
}
51+
4452
$keys = $this->identifiersExtractor->getIdentifiersFromResourceClass($class);
4553

4654
if (($numIdentifiers = \count($keys)) > 1) {

src/Operation/Factory/SubresourceOperationFactory.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,6 @@ 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),
111109
];
112110

113111
if (null === $parentOperation) {

src/Serializer/SerializerContextBuilder.php

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,36 +45,28 @@ public function createFromRequest(Request $request, bool $normalization, array $
4545

4646
$resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']);
4747
$key = $normalization ? 'normalization_context' : 'denormalization_context';
48-
49-
$operationKey = null;
50-
$operationType = null;
51-
5248
if (isset($attributes['collection_operation_name'])) {
5349
$operationKey = 'collection_operation_name';
5450
$operationType = OperationType::COLLECTION;
55-
} elseif (isset($attributes['subresource_operation_name'])) {
51+
} elseif (isset($attributes['item_operation_name'])) {
52+
$operationKey = 'item_operation_name';
53+
$operationType = OperationType::ITEM;
54+
} else {
5655
$operationKey = 'subresource_operation_name';
5756
$operationType = OperationType::SUBRESOURCE;
5857
}
5958

60-
if (null !== $operationKey) {
61-
$attribute = $attributes[$operationKey];
62-
$context = $resourceMetadata->getCollectionOperationAttribute($attribute, $key, [], true);
63-
$context[$operationKey] = $attribute;
64-
} else {
65-
$context = $resourceMetadata->getItemOperationAttribute($attributes['item_operation_name'], $key, [], true);
66-
$context['item_operation_name'] = $attributes['item_operation_name'];
67-
}
68-
69-
$context['operation_type'] = $operationType ?: OperationType::ITEM;
59+
$context = $resourceMetadata->getTypedOperationAttribute($operationType, $attributes[$operationKey], $key, [], true);
60+
$context['operation_type'] = $operationType;
61+
$context[$operationKey] = $attributes[$operationKey];
7062

7163
if (!$normalization && !isset($context['api_allow_update'])) {
7264
$context['api_allow_update'] = \in_array($request->getMethod(), ['PUT', 'PATCH'], true);
7365
}
7466

7567
$context['resource_class'] = $attributes['resource_class'];
76-
$context['input_class'] = $attributes['input_class'] ?? $attributes['resource_class'];
77-
$context['output_class'] = $attributes['output_class'] ?? $attributes['resource_class'];
68+
$context['input_class'] = $resourceMetadata->getTypedOperationAttribute($operationKey, $attributes[$operationKey], 'input_class', $attributes['resource_class'], true);
69+
$context['output_class'] = $resourceMetadata->getTypedOperationAttribute($operationKey, $attributes[$operationKey], 'output_class', $attributes['resource_class'], true);
7870
$context['request_uri'] = $request->getRequestUri();
7971
$context['uri'] = $request->getUri();
8072

src/Util/AttributesExtractor.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@ private function __construct()
3535
public static function extractAttributes(array $attributes): array
3636
{
3737
$result = ['resource_class' => $attributes['_api_resource_class'] ?? null];
38-
$result['input_class'] = $attributes['_api_input_class'] ?? $result['resource_class'];
39-
$result['output_class'] = $attributes['_api_output_class'] ?? $result['resource_class'];
40-
4138
if ($subresourceContext = $attributes['_api_subresource_context'] ?? null) {
4239
$result['subresource_context'] = $subresourceContext;
4340
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public function testWithResource()
136136
$this->response
137137
);
138138

139-
$this->assertSame(['resource_class' => DummyEntity::class, 'input_class' => DummyEntity::class, 'output_class' => DummyEntity::class, 'item_operation_name' => 'get', 'receive' => true, 'persist' => true], $dataCollector->getRequestAttributes());
139+
$this->assertSame(['resource_class' => DummyEntity::class, 'item_operation_name' => 'get', 'receive' => true, 'persist' => true], $dataCollector->getRequestAttributes());
140140
$this->assertSame(['foo', 'bar'], $dataCollector->getAcceptableContentTypes());
141141
$this->assertSame(DummyEntity::class, $dataCollector->getResourceClass());
142142
$this->assertSame(['foo' => null, 'a_filter' => \stdClass::class], $dataCollector->getFilters());

tests/Bridge/Symfony/Routing/ApiLoaderTest.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -305,8 +305,6 @@ private function getRoute(string $path, string $controller, string $resourceClas
305305
'_controller' => $controller,
306306
'_format' => null,
307307
'_api_resource_class' => $resourceClass,
308-
'_api_input_class' => $resourceClass,
309-
'_api_output_class' => $resourceClass,
310308
sprintf('_api_%s_operation_name', $collection ? 'collection' : 'item') => $operationName,
311309
] + $extraDefaults,
312310
$requirements,
@@ -326,8 +324,6 @@ private function getSubresourceRoute(string $path, string $controller, string $r
326324
'_controller' => $controller,
327325
'_format' => null,
328326
'_api_resource_class' => $resourceClass,
329-
'_api_input_class' => $resourceClass,
330-
'_api_output_class' => $resourceClass,
331327
'_api_subresource_operation_name' => $operationName,
332328
'_api_subresource_context' => $context,
333329
],

tests/EventListener/AddFormatListenerTest.php

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

264264
$formatsProviderProphecy = $this->prophesize(FormatsProviderInterface::class);
265-
$formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'input_class' => 'Foo', 'output_class' => 'Foo', 'collection_operation_name' => 'get', 'receive' => true, 'persist' => true])->willReturn(['csv' => ['text/csv']])->shouldBeCalled();
265+
$formatsProviderProphecy->getFormatsFromAttributes(['resource_class' => 'Foo', 'collection_operation_name' => 'get', 'receive' => true, 'persist' => true])->willReturn(['csv' => ['text/csv']])->shouldBeCalled();
266266

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

0 commit comments

Comments
 (0)