Skip to content

Commit d36c14c

Browse files
authored
refactor: split normalizer/denormalizer (#7713)
1 parent d57200a commit d36c14c

25 files changed

Lines changed: 1626 additions & 558 deletions
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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\AbstractItemNormalizer;
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 AbstractItemNormalizer
24+
{
25+
public const FORMAT = 'graphql';
26+
27+
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
28+
{
29+
return false;
30+
}
31+
32+
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
33+
{
34+
return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format, $context);
35+
}
36+
37+
public function getSupportedTypes(?string $format): array
38+
{
39+
return self::FORMAT === $format ? parent::getSupportedTypes($format) : [];
40+
}
41+
42+
protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
43+
{
44+
$allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
45+
46+
if (($context['api_denormalize'] ?? false) && \is_array($allowedAttributes) && false !== ($indexId = array_search('id', $allowedAttributes, true))) {
47+
$allowedAttributes[] = '_id';
48+
array_splice($allowedAttributes, (int) $indexId, 1);
49+
}
50+
51+
return $allowedAttributes;
52+
}
53+
54+
protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = []): void
55+
{
56+
if ('_id' === $attribute) {
57+
$attribute = 'id';
58+
}
59+
60+
parent::setAttributeValue($object, $attribute, $value, $format, $context);
61+
}
62+
}

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 & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
use PHPUnit\Framework\TestCase;
2929
use Prophecy\Argument;
3030
use Prophecy\PhpUnit\ProphecyTrait;
31-
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
3231
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
3332
use Symfony\Component\Serializer\SerializerInterface;
3433

@@ -253,39 +252,4 @@ public function testNormalizeNoResolverData(): void
253252
'no_resolver_data' => true,
254253
]));
255254
}
256-
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-
}
291255
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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\JsonApi\Serializer;
15+
16+
use ApiPlatform\Metadata\IriConverterInterface;
17+
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
18+
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
19+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
20+
use ApiPlatform\Metadata\ResourceAccessCheckerInterface;
21+
use ApiPlatform\Metadata\ResourceClassResolverInterface;
22+
use ApiPlatform\Serializer\AbstractItemNormalizer;
23+
use ApiPlatform\Serializer\OperationResourceClassResolverInterface;
24+
use ApiPlatform\Serializer\TagCollectorInterface;
25+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
26+
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
27+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
28+
29+
/**
30+
* Converts JSON:API documents to objects (denormalization only).
31+
*
32+
* @author Kévin Dunglas <dunglas@gmail.com>
33+
* @author Amrouche Hamza <hamza.simperfit@gmail.com>
34+
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
35+
*/
36+
final class ItemDenormalizer extends AbstractItemNormalizer
37+
{
38+
use ItemNormalizerTrait;
39+
40+
public const FORMAT = 'jsonapi';
41+
42+
public function __construct(
43+
PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory,
44+
PropertyMetadataFactoryInterface $propertyMetadataFactory,
45+
IriConverterInterface $iriConverter,
46+
ResourceClassResolverInterface $resourceClassResolver,
47+
?PropertyAccessorInterface $propertyAccessor = null,
48+
?NameConverterInterface $nameConverter = null,
49+
?ClassMetadataFactoryInterface $classMetadataFactory = null,
50+
array $defaultContext = [],
51+
?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null,
52+
?ResourceAccessCheckerInterface $resourceAccessChecker = null,
53+
protected ?TagCollectorInterface $tagCollector = null,
54+
?OperationResourceClassResolverInterface $operationResourceResolver = null,
55+
private readonly bool $useIriAsId = true,
56+
) {
57+
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataCollectionFactory, $resourceAccessChecker, $tagCollector, $operationResourceResolver);
58+
}
59+
60+
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
61+
{
62+
return false;
63+
}
64+
}

0 commit comments

Comments
 (0)