Skip to content

Commit 5b7738e

Browse files
authored
Merge pull request #2619 from soyuka/fix-output-id-normalization
GraphQl: Normalize id based on the origin resource
2 parents e5559b4 + 8d5dde4 commit 5b7738e

File tree

22 files changed

+175
-157
lines changed

22 files changed

+175
-157
lines changed

features/main/content_negotiation.feature

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ Feature: Content Negotiation support
6363
"jsonData": [],
6464
"arrayData": [],
6565
"name_converted": null,
66-
"relatedOwnedDummy": null,
67-
"relatedOwningDummy": null,
66+
"relatedOwnedDummy": null,
67+
"relatedOwningDummy": null,
6868
"id": 1,
6969
"name": "XML!",
7070
"alias": null,

features/main/input_output.feature

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,16 @@ Feature: DTO input and output
4242
"@vocab": "http://example.com/docs.jsonld#",
4343
"hydra": "http://www.w3.org/ns/hydra/core#",
4444
"foo": {
45+
"@id": "/dummy_dto_customs/1/foo",
4546
"@type": "@id"
4647
},
4748
"bar": {
49+
"@id": "/dummy_dto_customs/1/bar",
4850
"@type": "@id"
4951
}
5052
},
5153
"@type": "CustomOutputDto",
54+
"@id": "/dummy_dto_customs/1",
5255
"foo": "test",
5356
"bar": 1
5457
}
@@ -69,10 +72,12 @@ Feature: DTO input and output
6972
"@type": "hydra:Collection",
7073
"hydra:member": [
7174
{
75+
"@id": "/dummy_dto_customs/1",
7276
"foo": "test",
7377
"bar": 1
7478
},
7579
{
80+
"@id": "/dummy_dto_customs/2",
7681
"foo": "test",
7782
"bar": 2
7883
}
@@ -244,7 +249,7 @@ Feature: DTO input and output
244249
"""
245250
{
246251
dummyDtoInputOutput(id: "/dummy_dto_input_outputs/1") {
247-
baz
252+
_id, id, baz
248253
}
249254
}
250255
"""
@@ -254,6 +259,8 @@ Feature: DTO input and output
254259
{
255260
"data": {
256261
"dummyDtoInputOutput": {
262+
"_id": 1,
263+
"id": "/dummy_dto_input_outputs/1",
257264
"baz": 1
258265
}
259266
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838

3939
<service id="api_platform.graphql.resolver.resource_field" class="ApiPlatform\Core\GraphQl\Resolver\ResourceFieldResolver" public="false">
4040
<argument type="service" id="api_platform.iri_converter" />
41+
<argument type="service" id="api_platform.resource_class_resolver" />
4142
</service>
4243

4344
<service id="api_platform.graphql.schema_builder" class="ApiPlatform\Core\GraphQl\Type\SchemaBuilder" public="false">

src/GraphQl/Resolver/ResourceFieldResolver.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
namespace ApiPlatform\Core\GraphQl\Resolver;
1515

1616
use ApiPlatform\Core\Api\IriConverterInterface;
17+
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
1718
use ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer;
19+
use ApiPlatform\Core\Util\ClassInfoTrait;
1820
use GraphQL\Type\Definition\ResolveInfo;
1921

2022
/**
@@ -26,21 +28,28 @@
2628
*/
2729
final class ResourceFieldResolver
2830
{
31+
use ClassInfoTrait;
32+
2933
private $iriConverter;
34+
private $resourceClassResolver;
3035

31-
public function __construct(IriConverterInterface $iriConverter)
36+
public function __construct(IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver)
3237
{
3338
$this->iriConverter = $iriConverter;
39+
$this->resourceClassResolver = $resourceClassResolver;
3440
}
3541

3642
public function __invoke($source, $args, $context, ResolveInfo $info)
3743
{
3844
$property = null;
39-
if ('id' === $info->fieldName && isset($source[ItemNormalizer::ITEM_KEY])) {
40-
return $this->iriConverter->getIriFromItem(unserialize($source[ItemNormalizer::ITEM_KEY]));
45+
if ('id' === $info->fieldName && !isset($source['_id']) && isset($source[ItemNormalizer::ITEM_KEY])) {
46+
$object = unserialize($source[ItemNormalizer::ITEM_KEY]);
47+
if ($this->resourceClassResolver->isResourceClass($this->getObjectClass($object))) {
48+
return $this->iriConverter->getIriFromItem($object);
49+
}
4150
}
4251

43-
if ('_id' === $info->fieldName && isset($source['id'])) {
52+
if ('_id' === $info->fieldName && !isset($source['_id']) && isset($source['id'])) {
4453
$property = $source['id'];
4554
} elseif (\is_array($source) && isset($source[$info->fieldName])) {
4655
$property = $source[$info->fieldName];

src/GraphQl/Serializer/ItemNormalizer.php

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
1717
use ApiPlatform\Core\Serializer\ItemNormalizer as BaseItemNormalizer;
1818
use ApiPlatform\Core\Util\ClassInfoTrait;
19-
use Symfony\Component\Serializer\Exception\LogicException;
2019
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
21-
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
2220

2321
/**
2422
* GraphQL normalizer.
@@ -47,22 +45,23 @@ public function supportsNormalization($data, $format = null, array $context = []
4745
*/
4846
public function normalize($object, $format = null, array $context = [])
4947
{
50-
if (!$this->handleNonResource && $object !== $transformed = $this->transformOutput($object, $context)) {
51-
if (!$this->serializer instanceof NormalizerInterface) {
52-
throw new LogicException('Cannot normalize the transformed value because the injected serializer is not a normalizer');
53-
}
54-
55-
$context['api_normalize'] = true;
56-
$context['resource_class'] = $this->getObjectClass($transformed);
57-
58-
return $this->serializer->normalize($transformed, $format, $context);
48+
if (!$this->handleNonResource && null !== $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) {
49+
return parent::normalize($object, $format, $context);
5950
}
6051

6152
$data = parent::normalize($object, $format, $context);
6253
if (!\is_array($data)) {
6354
throw new UnexpectedValueException('Expected data to be an array');
6455
}
6556

57+
if ($this->handleNonResource) {
58+
// when using an output class, get the IRI from the resource
59+
if (isset($context['api_resource']) && isset($data['id'])) {
60+
$data['_id'] = $data['id'];
61+
$data['id'] = $this->iriConverter->getIriFromItem($context['api_resource']);
62+
}
63+
}
64+
6665
$data[self::ITEM_KEY] = serialize($object); // calling serialize prevent weird normalization process done by Webonyx's GraphQL PHP
6766

6867
return $data;

src/GraphQl/Type/SchemaBuilder.php

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,6 @@ private function convertFilterArgsToTypes(array $args): array
342342
*/
343343
private function convertType(Type $type, bool $input = false, string $mutationName = null, int $depth = 0)
344344
{
345-
$resourceClass = null;
346345
switch ($builtinType = $type->getBuiltinType()) {
347346
case Type::BUILTIN_TYPE_BOOL:
348347
$graphqlType = GraphQLType::boolean();
@@ -402,6 +401,12 @@ private function convertType(Type $type, bool $input = false, string $mutationNa
402401
private function getResourceObjectType(?string $resourceClass, ResourceMetadata $resourceMetadata, bool $input = false, string $mutationName = null, int $depth = 0): GraphQLType
403402
{
404403
$shortName = $resourceMetadata->getShortName();
404+
$ioMetadata = $resourceMetadata->getAttribute($input ? 'input' : 'output');
405+
406+
if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null !== $ioMetadata['class']) {
407+
$resourceClass = $ioMetadata['class'];
408+
$shortName = $ioMetadata['name'];
409+
}
405410
if (null !== $mutationName) {
406411
$shortName = $mutationName.ucfirst($shortName);
407412
}
@@ -415,12 +420,6 @@ private function getResourceObjectType(?string $resourceClass, ResourceMetadata
415420
return $this->graphqlTypes[$shortName];
416421
}
417422

418-
$ioMetadata = $resourceMetadata->getAttribute($input ? 'input' : 'output');
419-
420-
if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null !== $ioMetadata['class']) {
421-
$resourceClass = $ioMetadata['class'];
422-
}
423-
424423
$configuration = [
425424
'name' => $shortName,
426425
'description' => $resourceMetadata->getDescription(),

src/Hal/Serializer/ItemNormalizer.php

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,12 @@
1313

1414
namespace ApiPlatform\Core\Hal\Serializer;
1515

16-
use ApiPlatform\Core\Exception\RuntimeException;
1716
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
1817
use ApiPlatform\Core\Serializer\ContextTrait;
1918
use ApiPlatform\Core\Util\ClassInfoTrait;
2019
use Symfony\Component\Serializer\Exception\LogicException;
2120
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
2221
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
23-
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
2422

2523
/**
2624
* Converts between objects and array including HAL metadata.
@@ -50,26 +48,8 @@ public function supportsNormalization($data, $format = null, array $context = []
5048
*/
5149
public function normalize($object, $format = null, array $context = [])
5250
{
53-
if (!$this->handleNonResource && $object !== $transformed = $this->transformOutput($object, $context)) {
54-
if (!$this->serializer instanceof NormalizerInterface) {
55-
throw new LogicException('Cannot normalize the transformed value because the injected serializer is not a normalizer');
56-
}
57-
58-
$context['api_normalize'] = true;
59-
$context['resource_class'] = $this->getObjectClass($transformed);
60-
61-
return $this->serializer->normalize($transformed, $format, $context);
62-
}
63-
64-
if ($this->handleNonResource && $context['api_normalize'] ?? false) {
65-
$object = $this->transformOutput($object, $context);
66-
$data = $this->initContext($this->getObjectClass($object), $context);
67-
$rawData = parent::normalize($object, $format, $context);
68-
if (!\is_array($rawData)) {
69-
return $rawData;
70-
}
71-
72-
return $data + $rawData;
51+
if ($this->handleNonResource || null !== $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) {
52+
return parent::normalize($object, $format, $context);
7353
}
7454

7555
if (!isset($context['cache_key'])) {
@@ -105,11 +85,11 @@ public function supportsDenormalization($data, $type, $format = null, array $con
10585
/**
10686
* {@inheritdoc}
10787
*
108-
* @throws RuntimeException
88+
* @throws LogicException
10989
*/
11090
public function denormalize($data, $class, $format = null, array $context = [])
11191
{
112-
throw new RuntimeException(sprintf('%s is a read-only format.', self::FORMAT));
92+
throw new LogicException(sprintf('%s is a read-only format.', self::FORMAT));
11393
}
11494

11595
/**

src/JsonApi/Serializer/ItemNormalizer.php

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use ApiPlatform\Core\Api\IriConverterInterface;
1717
use ApiPlatform\Core\Api\OperationType;
1818
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
19-
use ApiPlatform\Core\Exception\InvalidArgumentException;
2019
use ApiPlatform\Core\Exception\ItemNotFoundException;
2120
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2221
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
@@ -25,7 +24,9 @@
2524
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
2625
use ApiPlatform\Core\Util\ClassInfoTrait;
2726
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
28-
use Symfony\Component\Serializer\Exception\LogicException;
27+
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
28+
use Symfony\Component\Serializer\Exception\RuntimeException;
29+
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
2930
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
3031
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
3132
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@@ -63,22 +64,7 @@ public function supportsNormalization($data, $format = null, array $context = []
6364
*/
6465
public function normalize($object, $format = null, array $context = [])
6566
{
66-
if (!$this->handleNonResource && $object !== $transformed = $this->transformOutput($object, $context)) {
67-
if (!$this->serializer instanceof NormalizerInterface) {
68-
throw new LogicException('Cannot normalize the transformed value because the injected serializer is not a normalizer');
69-
}
70-
71-
$context['api_normalize'] = true;
72-
$context['resource_class'] = $this->getObjectClass($transformed);
73-
74-
return $this->serializer->normalize($transformed, $format, $context);
75-
}
76-
77-
if ($this->handleNonResource && $context['api_normalize'] ?? false) {
78-
$object = $this->transformOutput($object, $context);
79-
$context['api_normalize'] = true;
80-
$context['resource_class'] = $this->getObjectClass($object);
81-
67+
if ($this->handleNonResource || null !== $outputClass = $this->getOutputClass($this->getObjectClass($object), $context)) {
8268
return parent::normalize($object, $format, $context);
8369
}
8470

@@ -135,13 +121,15 @@ public function supportsDenormalization($data, $type, $format = null, array $con
135121

136122
/**
137123
* {@inheritdoc}
124+
*
125+
* @throws NotNormalizableValueException
138126
*/
139127
public function denormalize($data, $class, $format = null, array $context = [])
140128
{
141129
// Avoid issues with proxies if we populated the object
142130
if (!isset($context[self::OBJECT_TO_POPULATE]) && isset($data['data']['id'])) {
143131
if (isset($context['api_allow_update']) && true !== $context['api_allow_update']) {
144-
throw new InvalidArgumentException('Update is not allowed for this operation.');
132+
throw new NotNormalizableValueException('Update is not allowed for this operation.');
145133
}
146134

147135
$context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getItemFromIri(
@@ -184,6 +172,9 @@ protected function setAttributeValue($object, $attribute, $value, $format = null
184172
* {@inheritdoc}
185173
*
186174
* @see http://jsonapi.org/format/#document-resource-object-linkage
175+
*
176+
* @throws RuntimeException
177+
* @throws NotNormalizableValueException
187178
*/
188179
protected function denormalizeRelation(string $attributeName, PropertyMetadata $propertyMetadata, string $className, $value, string $format = null, array $context)
189180
{
@@ -194,24 +185,26 @@ protected function denormalizeRelation(string $attributeName, PropertyMetadata $
194185
if ($this->serializer instanceof DenormalizerInterface) {
195186
return $this->serializer->denormalize($value, $className, $format, $context);
196187
}
197-
throw new InvalidArgumentException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
188+
throw new RuntimeException(sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class));
198189
}
199190

200191
if (!\is_array($value) || !isset($value['id'], $value['type'])) {
201-
throw new InvalidArgumentException('Only resource linkage supported currently, see: http://jsonapi.org/format/#document-resource-object-linkage.');
192+
throw new NotNormalizableValueException('Only resource linkage supported currently, see: http://jsonapi.org/format/#document-resource-object-linkage.');
202193
}
203194

204195
try {
205196
return $this->iriConverter->getItemFromIri($value['id'], $context + ['fetch_data' => true]);
206197
} catch (ItemNotFoundException $e) {
207-
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
198+
throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
208199
}
209200
}
210201

211202
/**
212203
* {@inheritdoc}
213204
*
214205
* @see http://jsonapi.org/format/#document-resource-object-linkage
206+
*
207+
* @throws RuntimeException
215208
*/
216209
protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedObject, string $resourceClass, string $format = null, array $context)
217210
{
@@ -224,7 +217,7 @@ protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relate
224217
if ($this->serializer instanceof NormalizerInterface) {
225218
return $this->serializer->normalize($relatedObject, $format, $context);
226219
}
227-
throw new InvalidArgumentException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
220+
throw new RuntimeException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
228221
}
229222
} else {
230223
$iri = $this->iriConverter->getIriFromItem($relatedObject);
@@ -236,7 +229,7 @@ protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relate
236229
$context['api_sub_level'] = true;
237230

238231
if (!$this->serializer instanceof NormalizerInterface) {
239-
throw new InvalidArgumentException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
232+
throw new RuntimeException(sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class));
240233
}
241234
$data = $this->serializer->normalize($relatedObject, $format, $context);
242235
unset($context['api_sub_level']);
@@ -328,7 +321,7 @@ private function getComponents($object, string $format = null, array $context)
328321
*
329322
* @param object $object
330323
*
331-
* @throws InvalidArgumentException
324+
* @throws UnexpectedValueException
332325
*/
333326
private function getPopulatedRelations($object, string $format = null, array $context, array $relationships): array
334327
{
@@ -367,7 +360,7 @@ private function getPopulatedRelations($object, string $format = null, array $co
367360
// Many to many relationship
368361
foreach ($attributeValue as $attributeValueElement) {
369362
if (!isset($attributeValueElement['data'])) {
370-
throw new InvalidArgumentException(sprintf('The JSON API attribute \'%s\' must contain a "data" key.', $relationshipName));
363+
throw new UnexpectedValueException(sprintf('The JSON API attribute \'%s\' must contain a "data" key.', $relationshipName));
371364
}
372365
unset($attributeValueElement['data']['attributes']);
373366
$data[$relationshipName]['data'][] = $attributeValueElement['data'];

0 commit comments

Comments
 (0)