Skip to content

Commit 8d7290e

Browse files
authored
[GraphQL] Support serialized name (#3516)
1 parent 7e69666 commit 8d7290e

File tree

10 files changed

+168
-33
lines changed

10 files changed

+168
-33
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* GraphQL: Allow to format GraphQL errors based on exceptions (#3063)
99
* GraphQL: Add page-based pagination (#3175)
1010
* GraphQL: Possibility to add a custom description for queries, mutations and subscriptions (#3477, #3514)
11+
* GraphQL: Support for field name conversion (serialized name) (#3455, #3516)
1112
* OpenAPI: Add PHP default values to the documentation (#2386)
1213
* Deprecate using a validation groups generator service not implementing `ApiPlatform\Core\Bridge\Symfony\Validator\ValidationGroupsGeneratorInterface` (#3346)
1314

features/doctrine/search_filter.feature

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ Feature: Search filter on collections
6565
"prop": "blue"
6666
}
6767
],
68-
"uuid": []
68+
"uuid": [],
69+
"carBrand": "DummyBrand"
6970
}
7071
],
7172
"hydra:totalItems": 1,

features/graphql/query.feature

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,18 @@ Feature: GraphQL query support
126126
And the header "Content-Type" should be equal to "application/json"
127127
And the JSON node "data.dummyGroup.foo" should be equal to "Foo #1"
128128

129+
Scenario: Query a serialized name
130+
Given there is a DummyCar entity with related colors
131+
When I send the following GraphQL request:
132+
"""
133+
{
134+
dummyCar(id: "/dummy_cars/1") {
135+
carBrand
136+
}
137+
}
138+
"""
139+
Then the JSON node "data.dummyCar.carBrand" should be equal to "DummyBrand"
140+
129141
Scenario: Fetch only the internal id
130142
When I send the following GraphQL request:
131143
"""

src/GraphQl/Serializer/SerializerContextBuilder.php

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1717
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
1818
use GraphQL\Type\Definition\ResolveInfo;
19+
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
1920
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
2021

2122
/**
@@ -45,10 +46,6 @@ public function create(?string $resourceClass, string $operationName, array $res
4546
'graphql_operation_name' => $operationName,
4647
];
4748

48-
if ($normalization) {
49-
$context['attributes'] = $this->fieldsToAttributes($resourceMetadata, $resolverContext);
50-
}
51-
5249
if (isset($resolverContext['fields'])) {
5350
$context['no_resolver_data'] = true;
5451
}
@@ -61,25 +58,29 @@ public function create(?string $resourceClass, string $operationName, array $res
6158
$context = array_merge($resourceMetadata->getGraphqlAttribute($operationName, $key, [], true), $context);
6259
}
6360

61+
if ($normalization) {
62+
$context['attributes'] = $this->fieldsToAttributes($resourceClass, $resourceMetadata, $resolverContext, $context);
63+
}
64+
6465
return $context;
6566
}
6667

6768
/**
6869
* Retrieves fields, recursively replaces the "_id" key (the raw id) by "id" (the name of the property expected by the Serializer) and flattens edge and node structures (pagination).
6970
*/
70-
private function fieldsToAttributes(?ResourceMetadata $resourceMetadata, array $context): array
71+
private function fieldsToAttributes(?string $resourceClass, ?ResourceMetadata $resourceMetadata, array $resolverContext, array $context): array
7172
{
72-
if (isset($context['fields'])) {
73-
$fields = $context['fields'];
73+
if (isset($resolverContext['fields'])) {
74+
$fields = $resolverContext['fields'];
7475
} else {
7576
/** @var ResolveInfo $info */
76-
$info = $context['info'];
77+
$info = $resolverContext['info'];
7778
$fields = $info->getFieldSelection(PHP_INT_MAX);
7879
}
7980

80-
$attributes = $this->replaceIdKeys($fields['edges']['node'] ?? $fields);
81+
$attributes = $this->replaceIdKeys($fields['edges']['node'] ?? $fields, $resourceClass, $context);
8182

82-
if ($context['is_mutation'] || $context['is_subscription']) {
83+
if ($resolverContext['is_mutation'] || $resolverContext['is_subscription']) {
8384
if (!$resourceMetadata) {
8485
throw new \LogicException('ResourceMetadata should always exist for a mutation or a subscription.');
8586
}
@@ -92,7 +93,7 @@ private function fieldsToAttributes(?ResourceMetadata $resourceMetadata, array $
9293
return $attributes;
9394
}
9495

95-
private function replaceIdKeys(array $fields): array
96+
private function replaceIdKeys(array $fields, ?string $resourceClass, array $context): array
9697
{
9798
$denormalizedFields = [];
9899

@@ -103,14 +104,21 @@ private function replaceIdKeys(array $fields): array
103104
continue;
104105
}
105106

106-
$denormalizedFields[$this->denormalizePropertyName((string) $key)] = \is_array($fields[$key]) ? $this->replaceIdKeys($fields[$key]) : $value;
107+
$denormalizedFields[$this->denormalizePropertyName((string) $key, $resourceClass, $context)] = \is_array($fields[$key]) ? $this->replaceIdKeys($fields[$key], $resourceClass, $context) : $value;
107108
}
108109

109110
return $denormalizedFields;
110111
}
111112

112-
private function denormalizePropertyName(string $property): string
113+
private function denormalizePropertyName(string $property, ?string $resourceClass, array $context): string
113114
{
114-
return null !== $this->nameConverter ? $this->nameConverter->denormalize($property) : $property;
115+
if (null === $this->nameConverter) {
116+
return $property;
117+
}
118+
if ($this->nameConverter instanceof AdvancedNameConverterInterface) {
119+
return $this->nameConverter->denormalize($property, $resourceClass, null, $context);
120+
}
121+
122+
return $this->nameConverter->denormalize($property);
115123
}
116124
}

src/GraphQl/Type/FieldsBuilder.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use Psr\Container\ContainerInterface;
3030
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
3131
use Symfony\Component\PropertyInfo\Type;
32+
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
3233
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
3334

3435
/**
@@ -497,6 +498,13 @@ private function convertType(Type $type, bool $input, ?string $queryName, ?strin
497498

498499
private function normalizePropertyName(string $property, string $resourceClass): string
499500
{
500-
return null !== $this->nameConverter ? $this->nameConverter->normalize($property, $resourceClass) : $property;
501+
if (null === $this->nameConverter) {
502+
return $property;
503+
}
504+
if ($this->nameConverter instanceof AdvancedNameConverterInterface) {
505+
return $this->nameConverter->normalize($property, $resourceClass);
506+
}
507+
508+
return $this->nameConverter->normalize($property);
501509
}
502510
}

tests/Fixtures/TestBundle/Document/DummyCar.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ class DummyCar
110110
*/
111111
private $availableAt;
112112

113+
/**
114+
* @var string
115+
*
116+
* @Serializer\Groups({"colors"})
117+
* @Serializer\SerializedName("carBrand")
118+
*
119+
* @ODM\Field
120+
*/
121+
private $brand = 'DummyBrand';
122+
113123
public function __construct()
114124
{
115125
$this->colors = new ArrayCollection();
@@ -191,4 +201,14 @@ public function setAvailableAt(\DateTime $availableAt)
191201
{
192202
$this->availableAt = $availableAt;
193203
}
204+
205+
public function getBrand(): string
206+
{
207+
return $this->brand;
208+
}
209+
210+
public function setBrand(string $brand): void
211+
{
212+
$this->brand = $brand;
213+
}
194214
}

tests/Fixtures/TestBundle/Entity/DummyCar.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,16 @@ class DummyCar
115115
*/
116116
private $availableAt;
117117

118+
/**
119+
* @var string
120+
*
121+
* @Serializer\Groups({"colors"})
122+
* @Serializer\SerializedName("carBrand")
123+
*
124+
* @ORM\Column
125+
*/
126+
private $brand = 'DummyBrand';
127+
118128
public function __construct()
119129
{
120130
$this->colors = new ArrayCollection();
@@ -199,4 +209,14 @@ public function setAvailableAt(\DateTime $availableAt)
199209
{
200210
$this->availableAt = $availableAt;
201211
}
212+
213+
public function getBrand(): string
214+
{
215+
return $this->brand;
216+
}
217+
218+
public function setBrand(string $brand): void
219+
{
220+
$this->brand = $brand;
221+
}
202222
}

tests/GraphQl/Serializer/SerializerContextBuilderTest.php

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Serializer\NameConverter\CustomConverter;
2020
use GraphQL\Type\Definition\ResolveInfo;
2121
use PHPUnit\Framework\TestCase;
22+
use Prophecy\Argument;
23+
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
2224

2325
/**
2426
* @author Alan Poulain <[email protected]>
@@ -36,16 +38,18 @@ protected function setUp(): void
3638
{
3739
$this->resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
3840

39-
$this->serializerContextBuilder = new SerializerContextBuilder(
40-
$this->resourceMetadataFactoryProphecy->reveal(),
41-
new CustomConverter()
42-
);
41+
$this->serializerContextBuilder = $this->buildSerializerContextBuilder();
42+
}
43+
44+
private function buildSerializerContextBuilder(?AdvancedNameConverterInterface $advancedNameConverter = null): SerializerContextBuilder
45+
{
46+
return new SerializerContextBuilder($this->resourceMetadataFactoryProphecy->reveal(), $advancedNameConverter ?? new CustomConverter());
4347
}
4448

4549
/**
4650
* @dataProvider createNormalizationContextProvider
4751
*/
48-
public function testCreateNormalizationContext(?string $resourceClass, string $operationName, array $fields, bool $isMutation, bool $isSubscription, bool $noInfo, array $expectedContext, ?string $expectedExceptionClass = null, ?string $expectedExceptionMessage = null): void
52+
public function testCreateNormalizationContext(?string $resourceClass, string $operationName, array $fields, bool $isMutation, bool $isSubscription, bool $noInfo, array $expectedContext, ?AdvancedNameConverterInterface $advancedNameConverter = null, ?string $expectedExceptionClass = null, ?string $expectedExceptionMessage = null): void
4953
{
5054
$resolverContext = [
5155
'is_mutation' => $isMutation,
@@ -76,13 +80,21 @@ public function testCreateNormalizationContext(?string $resourceClass, string $o
7680
$this->expectExceptionMessage($expectedExceptionMessage);
7781
}
7882

79-
$context = $this->serializerContextBuilder->create($resourceClass, $operationName, $resolverContext, true);
83+
$serializerContextBuilder = $this->serializerContextBuilder;
84+
if ($advancedNameConverter) {
85+
$serializerContextBuilder = $this->buildSerializerContextBuilder($advancedNameConverter);
86+
}
87+
88+
$context = $serializerContextBuilder->create($resourceClass, $operationName, $resolverContext, true);
8089

8190
$this->assertSame($expectedContext, $context);
8291
}
8392

8493
public function createNormalizationContextProvider(): array
8594
{
95+
$advancedNameConverter = $this->prophesize(AdvancedNameConverterInterface::class);
96+
$advancedNameConverter->denormalize('field', 'myResource', null, Argument::type('array'))->willReturn('denormalizedField');
97+
8698
return [
8799
'nominal' => [
88100
$resourceClass = 'myResource',
@@ -95,13 +107,33 @@ public function createNormalizationContextProvider(): array
95107
'groups' => ['normalization_group'],
96108
'resource_class' => $resourceClass,
97109
'graphql_operation_name' => $operationName,
110+
'input' => ['class' => 'inputClass'],
111+
'output' => ['class' => 'outputClass'],
98112
'attributes' => [
99113
'id' => 3,
100114
'field' => 'foo',
101115
],
116+
],
117+
],
118+
'nominal with advanced name converter' => [
119+
$resourceClass = 'myResource',
120+
$operationName = 'item_query',
121+
['_id' => 3, 'field' => 'foo'],
122+
false,
123+
false,
124+
false,
125+
[
126+
'groups' => ['normalization_group'],
127+
'resource_class' => $resourceClass,
128+
'graphql_operation_name' => $operationName,
102129
'input' => ['class' => 'inputClass'],
103130
'output' => ['class' => 'outputClass'],
131+
'attributes' => [
132+
'id' => 3,
133+
'denormalizedField' => 'foo',
134+
],
104135
],
136+
$advancedNameConverter->reveal(),
105137
],
106138
'nominal collection' => [
107139
$resourceClass = 'myResource',
@@ -114,11 +146,11 @@ public function createNormalizationContextProvider(): array
114146
'groups' => ['normalization_group'],
115147
'resource_class' => $resourceClass,
116148
'graphql_operation_name' => $operationName,
149+
'input' => ['class' => 'inputClass'],
150+
'output' => ['class' => 'outputClass'],
117151
'attributes' => [
118152
'nodeField' => 'baz',
119153
],
120-
'input' => ['class' => 'inputClass'],
121-
'output' => ['class' => 'outputClass'],
122154
],
123155
],
124156
'no resource class' => [
@@ -147,12 +179,12 @@ public function createNormalizationContextProvider(): array
147179
'groups' => ['normalization_group'],
148180
'resource_class' => $resourceClass,
149181
'graphql_operation_name' => $operationName,
182+
'input' => ['class' => 'inputClass'],
183+
'output' => ['class' => 'outputClass'],
150184
'attributes' => [
151185
'id' => 7,
152186
'related' => ['field' => 'bar'],
153187
],
154-
'input' => ['class' => 'inputClass'],
155-
'output' => ['class' => 'outputClass'],
156188
],
157189
],
158190
'mutation without resource class' => [
@@ -163,6 +195,7 @@ public function createNormalizationContextProvider(): array
163195
false,
164196
false,
165197
[],
198+
null,
166199
\LogicException::class,
167200
'ResourceMetadata should always exist for a mutation or a subscription.',
168201
],
@@ -177,13 +210,13 @@ public function createNormalizationContextProvider(): array
177210
'groups' => ['normalization_group'],
178211
'resource_class' => $resourceClass,
179212
'graphql_operation_name' => $operationName,
213+
'no_resolver_data' => true,
214+
'input' => ['class' => 'inputClass'],
215+
'output' => ['class' => 'outputClass'],
180216
'attributes' => [
181217
'id' => 7,
182218
'related' => ['field' => 'bar'],
183219
],
184-
'no_resolver_data' => true,
185-
'input' => ['class' => 'inputClass'],
186-
'output' => ['class' => 'outputClass'],
187220
],
188221
],
189222
];

0 commit comments

Comments
 (0)