1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the API Platform project. |
5
|
|
|
* |
6
|
|
|
* (c) Kévin Dunglas <[email protected]> |
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\Bridge\Graphql\Type; |
15
|
|
|
|
16
|
|
|
use ApiPlatform\Core\Api\IdentifiersExtractorInterface; |
17
|
|
|
use ApiPlatform\Core\Bridge\Graphql\Resolver\CollectionResolverFactoryInterface; |
18
|
|
|
use ApiPlatform\Core\Bridge\Graphql\Resolver\ItemResolverFactoryInterface; |
19
|
|
|
use ApiPlatform\Core\Exception\ResourceClassNotFoundException; |
20
|
|
|
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; |
21
|
|
|
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; |
22
|
|
|
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; |
23
|
|
|
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; |
24
|
|
|
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; |
25
|
|
|
use Doctrine\Common\Util\Inflector; |
26
|
|
|
use GraphQL\Type\Definition\InputObjectType; |
27
|
|
|
use GraphQL\Type\Definition\ObjectType; |
28
|
|
|
use GraphQL\Type\Definition\Type as GraphQLType; |
29
|
|
|
use GraphQL\Type\Definition\WrappingType; |
30
|
|
|
use GraphQL\Type\Schema; |
31
|
|
|
use Symfony\Component\Config\Definition\Exception\InvalidTypeException; |
32
|
|
|
use Symfony\Component\PropertyInfo\Type; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Builder of the GraphQL schema. |
36
|
|
|
* |
37
|
|
|
* @author Raoul Clais <[email protected]> |
38
|
|
|
* @author Alan Poulain <[email protected]> |
39
|
|
|
* |
40
|
|
|
* @internal |
41
|
|
|
*/ |
42
|
|
|
final class SchemaBuilder implements SchemaBuilderInterface |
43
|
|
|
{ |
44
|
|
|
private $propertyNameCollectionFactory; |
45
|
|
|
private $propertyMetadataFactory; |
46
|
|
|
private $resourceNameCollectionFactory; |
47
|
|
|
private $resourceMetadataFactory; |
48
|
|
|
private $collectionResolverFactory; |
49
|
|
|
private $itemResolverFactory; |
50
|
|
|
private $identifiersExtractor; |
51
|
|
|
private $paginationEnabled; |
52
|
|
|
private $resourceTypesCache = []; |
53
|
|
|
|
54
|
|
View Code Duplication |
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, CollectionResolverFactoryInterface $collectionResolverFactory, ItemResolverFactoryInterface $itemResolverFactory, IdentifiersExtractorInterface $identifiersExtractor, bool $paginationEnabled) |
|
|
|
|
55
|
|
|
{ |
56
|
|
|
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory; |
57
|
|
|
$this->propertyMetadataFactory = $propertyMetadataFactory; |
58
|
|
|
$this->resourceNameCollectionFactory = $resourceNameCollectionFactory; |
59
|
|
|
$this->resourceMetadataFactory = $resourceMetadataFactory; |
60
|
|
|
$this->collectionResolverFactory = $collectionResolverFactory; |
61
|
|
|
$this->itemResolverFactory = $itemResolverFactory; |
62
|
|
|
$this->identifiersExtractor = $identifiersExtractor; |
63
|
|
|
$this->paginationEnabled = $paginationEnabled; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
public function getSchema(): Schema |
67
|
|
|
{ |
68
|
|
|
$queryFields = []; |
69
|
|
|
foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) { |
70
|
|
|
$queryFields += $this->getQueryFields($resourceClass); |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
return new Schema([ |
74
|
|
|
'query' => new ObjectType([ |
75
|
|
|
'name' => 'Query', |
76
|
|
|
'fields' => $queryFields, |
77
|
|
|
]), |
78
|
|
|
]); |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* Gets the query fields of the schema. |
83
|
|
|
*/ |
84
|
|
|
private function getQueryFields(string $resourceClass): array |
85
|
|
|
{ |
86
|
|
|
$queryFields = []; |
87
|
|
|
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); |
88
|
|
|
$shortName = $resourceMetadata->getShortName(); |
89
|
|
|
|
90
|
|
|
if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass)) { |
91
|
|
|
$fieldConfiguration['args'] += $this->getResourceIdentifiersArgumentsConfiguration($resourceClass); |
92
|
|
|
$queryFields[lcfirst($shortName)] = $fieldConfiguration; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass)) { |
96
|
|
|
$queryFields[lcfirst(Inflector::pluralize($shortName))] = $fieldConfiguration; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
return $queryFields; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Gets the field configuration of a resource. |
104
|
|
|
* |
105
|
|
|
* @see http://webonyx.github.io/graphql-php/type-system/object-types/ |
106
|
|
|
* |
107
|
|
|
* @return array|null |
108
|
|
|
*/ |
109
|
|
|
private function getResourceFieldConfiguration(string $fieldDescription = null, Type $type, string $rootResource, bool $isInput = false) |
110
|
|
|
{ |
111
|
|
|
try { |
112
|
|
|
$graphqlType = $this->convertType($type, $isInput); |
113
|
|
|
$graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType() : $graphqlType; |
114
|
|
|
$isInternalGraphqlType = in_array($graphqlWrappedType, GraphQLType::getInternalTypes(), true); |
115
|
|
|
if ($isInternalGraphqlType) { |
116
|
|
|
$className = ''; |
117
|
|
|
} else { |
118
|
|
|
$className = $type->isCollection() ? $type->getCollectionValueType()->getClassName() : $type->getClassName(); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
$args = []; |
122
|
|
|
if ($this->paginationEnabled && !$isInternalGraphqlType && $type->isCollection() && !$isInput) { |
123
|
|
|
$args = [ |
124
|
|
|
'first' => [ |
125
|
|
|
'type' => GraphQLType::int(), |
126
|
|
|
'description' => 'Returns the first n elements from the list.', |
127
|
|
|
], |
128
|
|
|
'after' => [ |
129
|
|
|
'type' => GraphQLType::string(), |
130
|
|
|
'description' => 'Returns the elements in the list that come after the specified cursor.', |
131
|
|
|
], |
132
|
|
|
]; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
if ($isInternalGraphqlType || $isInput) { |
136
|
|
|
$resolve = null; |
137
|
|
|
} else { |
138
|
|
|
$resolve = $type->isCollection() ? $this->collectionResolverFactory->createCollectionResolver($className, $rootResource) : $this->itemResolverFactory->createItemResolver($className, $rootResource); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
return [ |
142
|
|
|
'type' => $graphqlType, |
143
|
|
|
'description' => $fieldDescription, |
144
|
|
|
'args' => $args, |
145
|
|
|
'resolve' => $resolve, |
146
|
|
|
]; |
147
|
|
|
} catch (InvalidTypeException $e) { |
148
|
|
|
return null; |
149
|
|
|
} |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Gets the field arguments of the identifier of a given resource. |
154
|
|
|
* |
155
|
|
|
* @throws \LogicException |
156
|
|
|
*/ |
157
|
|
View Code Duplication |
private function getResourceIdentifiersArgumentsConfiguration(string $resourceClass): array |
|
|
|
|
158
|
|
|
{ |
159
|
|
|
$arguments = []; |
160
|
|
|
$identifiers = $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass); |
161
|
|
|
foreach ($identifiers as $identifier) { |
162
|
|
|
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $identifier); |
163
|
|
|
$propertyType = $propertyMetadata->getType(); |
164
|
|
|
if (null === $propertyType) { |
165
|
|
|
continue; |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
$arguments[$identifier] = $this->getResourceFieldConfiguration($propertyMetadata->getDescription(), $propertyType, $resourceClass, true); |
169
|
|
|
} |
170
|
|
|
if (!$arguments) { |
|
|
|
|
171
|
|
|
throw new \LogicException("Missing identifier field for resource \"$resourceClass\"."); |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
return $arguments; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Converts a built-in type to its GraphQL equivalent. |
179
|
|
|
* |
180
|
|
|
* @throws InvalidTypeException |
181
|
|
|
*/ |
182
|
|
|
private function convertType(Type $type, bool $isInput = false): GraphQLType |
183
|
|
|
{ |
184
|
|
|
switch ($type->getBuiltinType()) { |
185
|
|
|
case Type::BUILTIN_TYPE_BOOL: |
186
|
|
|
$graphqlType = GraphQLType::boolean(); |
187
|
|
|
break; |
188
|
|
|
case Type::BUILTIN_TYPE_INT: |
189
|
|
|
$graphqlType = GraphQLType::int(); |
190
|
|
|
break; |
191
|
|
|
case Type::BUILTIN_TYPE_FLOAT: |
192
|
|
|
$graphqlType = GraphQLType::float(); |
193
|
|
|
break; |
194
|
|
|
case Type::BUILTIN_TYPE_STRING: |
195
|
|
|
$graphqlType = GraphQLType::string(); |
196
|
|
|
break; |
197
|
|
|
case Type::BUILTIN_TYPE_OBJECT: |
198
|
|
|
if (is_a($type->getClassName(), \DateTimeInterface::class, true)) { |
199
|
|
|
$graphqlType = GraphQLType::string(); |
200
|
|
|
break; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
try { |
204
|
|
|
$className = $type->isCollection() ? $type->getCollectionValueType()->getClassName() : $type->getClassName(); |
205
|
|
|
$resourceMetadata = $this->resourceMetadataFactory->create($className); |
206
|
|
|
} catch (ResourceClassNotFoundException $e) { |
207
|
|
|
throw new InvalidTypeException(); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
$graphqlType = $this->getResourceObjectType($className, $resourceMetadata, $isInput); |
211
|
|
|
break; |
212
|
|
|
default: |
213
|
|
|
throw new InvalidTypeException(); |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
if ($type->isCollection()) { |
217
|
|
|
return $this->paginationEnabled ? $this->getResourcePaginatedCollectionType($graphqlType, $isInput) : GraphQLType::listOf($graphqlType); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
return $type->isNullable() ? $graphqlType : GraphQLType::nonNull($graphqlType); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* Gets the object type of the given resource. |
225
|
|
|
* |
226
|
|
|
* @return ObjectType|InputObjectType |
227
|
|
|
*/ |
228
|
|
|
private function getResourceObjectType(string $resource, ResourceMetadata $resourceMetadata, bool $isInput = false) |
229
|
|
|
{ |
230
|
|
|
$shortName = $resourceMetadata->getShortName().($isInput ? 'Input' : ''); |
231
|
|
|
if (isset($this->resourceTypesCache[$shortName])) { |
232
|
|
|
return $this->resourceTypesCache[$shortName]; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
$configuration = [ |
236
|
|
|
'name' => $shortName, |
237
|
|
|
'description' => $resourceMetadata->getDescription(), |
238
|
|
|
'fields' => function () use ($resource, $isInput) { |
239
|
|
|
return $this->getResourceObjectTypeFields($resource, $isInput); |
240
|
|
|
}, |
241
|
|
|
]; |
242
|
|
|
|
243
|
|
|
return $this->resourceTypesCache[$shortName] = $isInput ? new InputObjectType($configuration) : new ObjectType($configuration); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* Gets the fields of the type of the given resource. |
248
|
|
|
*/ |
249
|
|
View Code Duplication |
private function getResourceObjectTypeFields(string $resource, bool $isInput = false): array |
|
|
|
|
250
|
|
|
{ |
251
|
|
|
$fields = []; |
252
|
|
|
foreach ($this->propertyNameCollectionFactory->create($resource) as $property) { |
253
|
|
|
$propertyMetadata = $this->propertyMetadataFactory->create($resource, $property); |
254
|
|
|
if (null === ($propertyType = $propertyMetadata->getType()) || !$propertyMetadata->isReadable()) { |
255
|
|
|
continue; |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
if ($fieldConfiguration = $this->getResourceFieldConfiguration($propertyMetadata->getDescription(), $propertyType, $resource, $isInput)) { |
259
|
|
|
$fields[$property] = $fieldConfiguration; |
260
|
|
|
} |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
return $fields; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Gets the type of a paginated collection of the given resource type. |
268
|
|
|
* |
269
|
|
|
* @param ObjectType|InputObjectType $resourceType |
270
|
|
|
* |
271
|
|
|
* @return ObjectType|InputObjectType |
272
|
|
|
*/ |
273
|
|
|
private function getResourcePaginatedCollectionType($resourceType, bool $isInput = false) |
274
|
|
|
{ |
275
|
|
|
$shortName = $resourceType->name.($isInput ? 'Input' : ''); |
276
|
|
|
|
277
|
|
|
if (isset($this->resourceTypesCache["{$shortName}Connection"])) { |
278
|
|
|
return $this->resourceTypesCache["{$shortName}Connection"]; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
$edgeObjectTypeConfiguration = [ |
282
|
|
|
'name' => "{$shortName}Edge", |
283
|
|
|
'description' => "Edge of $shortName.", |
284
|
|
|
'fields' => [ |
285
|
|
|
'node' => $resourceType, |
286
|
|
|
'cursor' => GraphQLType::nonNull(GraphQLType::string()), |
287
|
|
|
], |
288
|
|
|
]; |
289
|
|
|
$edgeObjectType = $isInput ? new InputObjectType($edgeObjectTypeConfiguration) : new ObjectType($edgeObjectTypeConfiguration); |
290
|
|
|
$pageInfoObjectTypeConfiguration = [ |
291
|
|
|
'name' => "{$shortName}PageInfo", |
292
|
|
|
'description' => 'Information about the current page.', |
293
|
|
|
'fields' => [ |
294
|
|
|
'endCursor' => GraphQLType::string(), |
295
|
|
|
'hasNextPage' => GraphQLType::nonNull(GraphQLType::boolean()), |
296
|
|
|
], |
297
|
|
|
]; |
298
|
|
|
$pageInfoObjectType = $isInput ? new InputObjectType($pageInfoObjectTypeConfiguration) : new ObjectType($pageInfoObjectTypeConfiguration); |
299
|
|
|
|
300
|
|
|
$configuration = [ |
301
|
|
|
'name' => "{$shortName}Connection", |
302
|
|
|
'description' => "Connection for $shortName.", |
303
|
|
|
'fields' => [ |
304
|
|
|
'edges' => GraphQLType::listOf($edgeObjectType), |
305
|
|
|
'pageInfo' => GraphQLType::nonNull($pageInfoObjectType), |
306
|
|
|
], |
307
|
|
|
]; |
308
|
|
|
|
309
|
|
|
return $this->resourceTypesCache["{$shortName}Connection"] = $isInput ? new InputObjectType($configuration) : new ObjectType($configuration); |
310
|
|
|
} |
311
|
|
|
} |
312
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.