Skip to content

Commit e9826b7

Browse files
committed
refactor(graphql): split ItemNormalizer/ItemDenormalizer
Mirrors the split done for Serializer/JsonApi/JsonLd: introduces GraphQl\Serializer\ItemDenormalizer registered at priority -889 so Symfony's serializer dispatches mutation denormalization to it, avoiding the legacy ItemNormalizer::denormalize() deprecation.
1 parent e0e54e0 commit e9826b7

6 files changed

Lines changed: 178 additions & 36 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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\GraphQl\Serializer;
15+
16+
use ApiPlatform\Serializer\ItemDenormalizer as BaseItemDenormalizer;
17+
18+
/**
19+
* Converts GraphQL inputs to objects (denormalization only).
20+
*
21+
* @author Kévin Dunglas <dunglas@gmail.com>
22+
*/
23+
final class ItemDenormalizer extends BaseItemDenormalizer
24+
{
25+
public const FORMAT = 'graphql';
26+
27+
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
28+
{
29+
return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format, $context);
30+
}
31+
32+
public function getSupportedTypes(?string $format): array
33+
{
34+
return self::FORMAT === $format ? parent::getSupportedTypes($format) : [];
35+
}
36+
37+
protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
38+
{
39+
$allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
40+
41+
if (($context['api_denormalize'] ?? false) && \is_array($allowedAttributes) && false !== ($indexId = array_search('id', $allowedAttributes, true))) {
42+
$allowedAttributes[] = '_id';
43+
array_splice($allowedAttributes, (int) $indexId, 1);
44+
}
45+
46+
return $allowedAttributes;
47+
}
48+
49+
protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = []): void
50+
{
51+
if ('_id' === $attribute) {
52+
$attribute = 'id';
53+
}
54+
55+
parent::setAttributeValue($object, $attribute, $value, $format, $context);
56+
}
57+
}

src/GraphQl/State/Provider/DenormalizeProvider.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
namespace ApiPlatform\GraphQl\State\Provider;
1515

16-
use ApiPlatform\GraphQl\Serializer\ItemNormalizer;
16+
use ApiPlatform\GraphQl\Serializer\ItemDenormalizer;
1717
use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface;
1818
use ApiPlatform\Metadata\GraphQl\Mutation;
1919
use ApiPlatform\Metadata\Operation;
@@ -47,7 +47,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
4747
$denormalizationContext[AbstractNormalizer::OBJECT_TO_POPULATE] = $data;
4848
}
4949

50-
$item = $this->denormalizer->denormalize($context['args']['input'], $operation->getClass(), ItemNormalizer::FORMAT, $denormalizationContext);
50+
$item = $this->denormalizer->denormalize($context['args']['input'], $operation->getClass(), ItemDenormalizer::FORMAT, $denormalizationContext);
5151

5252
if (!\is_object($item)) {
5353
throw new \UnexpectedValueException('Expected item to be an object.');
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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\GraphQl\Tests\Serializer;
15+
16+
use ApiPlatform\GraphQl\Serializer\ItemDenormalizer;
17+
use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\Dummy;
18+
use ApiPlatform\Metadata\ApiProperty;
19+
use ApiPlatform\Metadata\IriConverterInterface;
20+
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
21+
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
22+
use ApiPlatform\Metadata\Property\PropertyNameCollection;
23+
use ApiPlatform\Metadata\ResourceClassResolverInterface;
24+
use PHPUnit\Framework\TestCase;
25+
use Prophecy\Argument;
26+
use Prophecy\PhpUnit\ProphecyTrait;
27+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
28+
use Symfony\Component\Serializer\SerializerInterface;
29+
30+
class ItemDenormalizerTest extends TestCase
31+
{
32+
use ProphecyTrait;
33+
34+
public function testSupportsDenormalizationOnlyForGraphQlFormat(): void
35+
{
36+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
37+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
38+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
39+
40+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
41+
$resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
42+
43+
$denormalizer = new ItemDenormalizer(
44+
$propertyNameCollectionFactoryProphecy->reveal(),
45+
$propertyMetadataFactoryProphecy->reveal(),
46+
$iriConverterProphecy->reveal(),
47+
$resourceClassResolverProphecy->reveal()
48+
);
49+
50+
$this->assertFalse($denormalizer->supportsNormalization(new Dummy(), ItemDenormalizer::FORMAT));
51+
$this->assertTrue($denormalizer->supportsDenormalization([], Dummy::class, ItemDenormalizer::FORMAT));
52+
$this->assertFalse($denormalizer->supportsDenormalization([], Dummy::class, 'jsonld'));
53+
}
54+
55+
public function testDenormalize(): void
56+
{
57+
$context = ['resource_class' => Dummy::class, 'api_allow_update' => true];
58+
59+
$propertyNameCollection = new PropertyNameCollection(['name']);
60+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
61+
$propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::type('array'))->willReturn($propertyNameCollection)->shouldBeCalled();
62+
63+
$propertyMetadata = (new ApiProperty())->withWritable(true)->withReadable(true);
64+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
65+
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::type('array'))->willReturn($propertyMetadata)->shouldBeCalled();
66+
67+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
68+
69+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
70+
$resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class);
71+
$resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
72+
73+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
74+
$serializerProphecy->willImplement(DenormalizerInterface::class);
75+
76+
$denormalizer = new ItemDenormalizer(
77+
$propertyNameCollectionFactoryProphecy->reveal(),
78+
$propertyMetadataFactoryProphecy->reveal(),
79+
$iriConverterProphecy->reveal(),
80+
$resourceClassResolverProphecy->reveal()
81+
);
82+
$denormalizer->setSerializer($serializerProphecy->reveal());
83+
84+
$this->assertInstanceOf(Dummy::class, $denormalizer->denormalize(['name' => 'hello'], Dummy::class, ItemDenormalizer::FORMAT, $context));
85+
}
86+
}

src/GraphQl/Tests/Serializer/ItemNormalizerTest.php

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -254,38 +254,4 @@ public function testNormalizeNoResolverData(): void
254254
]));
255255
}
256256

257-
public function testDenormalize(): void
258-
{
259-
$context = ['resource_class' => Dummy::class, 'api_allow_update' => true];
260-
261-
$propertyNameCollection = new PropertyNameCollection(['name']);
262-
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
263-
$propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::type('array'))->willReturn($propertyNameCollection)->shouldBeCalled();
264-
265-
$propertyMetadata = (new ApiProperty())->withWritable(true)->withReadable(true);
266-
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
267-
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::type('array'))->willReturn($propertyMetadata)->shouldBeCalled();
268-
269-
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
270-
271-
$identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
272-
273-
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
274-
$resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class);
275-
$resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
276-
277-
$serializerProphecy = $this->prophesize(SerializerInterface::class);
278-
$serializerProphecy->willImplement(DenormalizerInterface::class);
279-
280-
$normalizer = new ItemNormalizer(
281-
$propertyNameCollectionFactoryProphecy->reveal(),
282-
$propertyMetadataFactoryProphecy->reveal(),
283-
$iriConverterProphecy->reveal(),
284-
$identifiersExtractorProphecy->reveal(),
285-
$resourceClassResolverProphecy->reveal()
286-
);
287-
$normalizer->setSerializer($serializerProphecy->reveal());
288-
289-
$this->assertInstanceOf(Dummy::class, $normalizer->denormalize(['name' => 'hello'], Dummy::class, ItemNormalizer::FORMAT, $context));
290-
}
291257
}

src/Laravel/ApiPlatformProvider.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use ApiPlatform\GraphQl\Serializer\Exception\HttpExceptionNormalizer as GraphQlHttpExceptionNormalizer;
2828
use ApiPlatform\GraphQl\Serializer\Exception\RuntimeExceptionNormalizer as GraphQlRuntimeExceptionNormalizer;
2929
use ApiPlatform\GraphQl\Serializer\Exception\ValidationExceptionNormalizer as GraphQlValidationExceptionNormalizer;
30+
use ApiPlatform\GraphQl\Serializer\ItemDenormalizer as GraphQlItemDenormalizer;
3031
use ApiPlatform\GraphQl\Serializer\ItemNormalizer as GraphQlItemNormalizer;
3132
use ApiPlatform\GraphQl\Serializer\ObjectNormalizer as GraphQlObjectNormalizer;
3233
use ApiPlatform\GraphQl\Serializer\SerializerContextBuilder as GraphQlSerializerContextBuilder;
@@ -1080,6 +1081,7 @@ public function register(): void
10801081

10811082
if (interface_exists(FieldsBuilderEnumInterface::class)) {
10821083
$list->insert($app->make(GraphQlItemNormalizer::class), -890);
1084+
$list->insert($app->make(GraphQlItemDenormalizer::class), -889);
10831085
$list->insert($app->make(GraphQlObjectNormalizer::class), -995);
10841086
$list->insert($app->make(GraphQlErrorNormalizer::class), -790);
10851087
$list->insert($app->make(GraphQlValidationExceptionNormalizer::class), -780);
@@ -1310,6 +1312,21 @@ private function registerGraphQl(): void
13101312
);
13111313
});
13121314

1315+
$this->app->singleton(GraphQlItemDenormalizer::class, static function (Application $app) {
1316+
return new GraphQlItemDenormalizer(
1317+
$app->make(PropertyNameCollectionFactoryInterface::class),
1318+
$app->make(PropertyMetadataFactoryInterface::class),
1319+
$app->make(IriConverterInterface::class),
1320+
$app->make(ResourceClassResolverInterface::class),
1321+
$app->make(PropertyAccessorInterface::class),
1322+
$app->make(NameConverterInterface::class),
1323+
$app->make(SerializerClassMetadataFactory::class),
1324+
null,
1325+
$app->make(ResourceMetadataCollectionFactoryInterface::class),
1326+
$app->make(ResourceAccessCheckerInterface::class)
1327+
);
1328+
});
1329+
13131330
$this->app->singleton(GraphQlObjectNormalizer::class, static function (Application $app) {
13141331
return new GraphQlObjectNormalizer(
13151332
$app->make(ObjectNormalizer::class),

src/Symfony/Bundle/Resources/config/graphql.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use ApiPlatform\GraphQl\Serializer\Exception\HttpExceptionNormalizer;
2525
use ApiPlatform\GraphQl\Serializer\Exception\RuntimeExceptionNormalizer;
2626
use ApiPlatform\GraphQl\Serializer\Exception\ValidationExceptionNormalizer;
27+
use ApiPlatform\GraphQl\Serializer\ItemDenormalizer;
2728
use ApiPlatform\GraphQl\Serializer\ItemNormalizer;
2829
use ApiPlatform\GraphQl\Serializer\ObjectNormalizer;
2930
use ApiPlatform\GraphQl\Serializer\SerializerContextBuilder;
@@ -250,6 +251,21 @@
250251
])
251252
->tag('serializer.normalizer', ['priority' => -890]);
252253

254+
$services->set('api_platform.graphql.denormalizer.item', ItemDenormalizer::class)
255+
->args([
256+
service('api_platform.metadata.property.name_collection_factory'),
257+
service('api_platform.metadata.property.metadata_factory'),
258+
service('api_platform.symfony.iri_converter'),
259+
service('api_platform.resource_class_resolver'),
260+
service('api_platform.property_accessor'),
261+
service('api_platform.name_converter')->ignoreOnInvalid(),
262+
service('serializer.mapping.class_metadata_factory')->ignoreOnInvalid(),
263+
null,
264+
service('api_platform.metadata.resource.metadata_collection_factory')->ignoreOnInvalid(),
265+
service('api_platform.security.resource_access_checker')->ignoreOnInvalid(),
266+
])
267+
->tag('serializer.normalizer', ['priority' => -889]);
268+
253269
$services->set('api_platform.graphql.normalizer.object', ObjectNormalizer::class)
254270
->args([
255271
service('api_platform.normalizer.object'),

0 commit comments

Comments
 (0)