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\GraphQl\Type; |
||
15 | |||
16 | use ApiPlatform\Core\DataProvider\Pagination; |
||
17 | use ApiPlatform\Core\Exception\ResourceClassNotFoundException; |
||
18 | use ApiPlatform\Core\GraphQl\Resolver\Factory\ResolverFactoryInterface; |
||
19 | use ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface; |
||
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\ResourceMetadata; |
||
24 | use Doctrine\Common\Inflector\Inflector; |
||
25 | use GraphQL\Type\Definition\InputObjectType; |
||
26 | use GraphQL\Type\Definition\NullableType; |
||
27 | use GraphQL\Type\Definition\Type as GraphQLType; |
||
28 | use GraphQL\Type\Definition\WrappingType; |
||
29 | use Psr\Container\ContainerInterface; |
||
30 | use Symfony\Component\Config\Definition\Exception\InvalidTypeException; |
||
31 | use Symfony\Component\PropertyInfo\Type; |
||
32 | |||
33 | /** |
||
34 | * Builds the GraphQL fields. |
||
35 | * |
||
36 | * @experimental |
||
37 | * |
||
38 | * @author Alan Poulain <[email protected]> |
||
39 | */ |
||
40 | final class FieldsBuilder implements FieldsBuilderInterface |
||
41 | { |
||
42 | private $propertyNameCollectionFactory; |
||
43 | private $propertyMetadataFactory; |
||
44 | private $resourceMetadataFactory; |
||
45 | private $typesContainer; |
||
46 | private $typeBuilder; |
||
47 | private $typeConverter; |
||
48 | private $itemResolverFactory; |
||
49 | private $collectionResolverFactory; |
||
50 | private $itemMutationResolverFactory; |
||
51 | private $filterLocator; |
||
52 | private $pagination; |
||
53 | |||
54 | public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, TypesContainerInterface $typesContainer, TypeBuilderInterface $typeBuilder, TypeConverterInterface $typeConverter, ResolverFactoryInterface $itemResolverFactory, ResolverFactoryInterface $collectionResolverFactory, ResolverFactoryInterface $itemMutationResolverFactory, ContainerInterface $filterLocator, Pagination $pagination) |
||
55 | { |
||
56 | $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; |
||
57 | $this->propertyMetadataFactory = $propertyMetadataFactory; |
||
58 | $this->resourceMetadataFactory = $resourceMetadataFactory; |
||
59 | $this->typesContainer = $typesContainer; |
||
60 | $this->typeBuilder = $typeBuilder; |
||
61 | $this->typeConverter = $typeConverter; |
||
62 | $this->itemResolverFactory = $itemResolverFactory; |
||
63 | $this->collectionResolverFactory = $collectionResolverFactory; |
||
64 | $this->itemMutationResolverFactory = $itemMutationResolverFactory; |
||
65 | $this->filterLocator = $filterLocator; |
||
66 | $this->pagination = $pagination; |
||
67 | } |
||
68 | |||
69 | /** |
||
70 | * {@inheritdoc} |
||
71 | */ |
||
72 | public function getNodeQueryFields(): array |
||
73 | { |
||
74 | return [ |
||
75 | 'type' => $this->typeBuilder->getNodeInterface(), |
||
76 | 'args' => [ |
||
77 | 'id' => ['type' => GraphQLType::nonNull(GraphQLType::id())], |
||
78 | ], |
||
79 | 'resolve' => ($this->itemResolverFactory)(), |
||
80 | ]; |
||
81 | } |
||
82 | |||
83 | /** |
||
84 | * {@inheritdoc} |
||
85 | */ |
||
86 | public function getItemQueryFields(string $resourceClass, ResourceMetadata $resourceMetadata, string $queryName, array $configuration): array |
||
87 | { |
||
88 | $shortName = $resourceMetadata->getShortName(); |
||
89 | $fieldName = lcfirst('item_query' === $queryName ? $shortName : $queryName.$shortName); |
||
90 | |||
91 | $deprecationReason = (string) $resourceMetadata->getGraphqlAttribute($queryName, 'deprecation_reason', '', true); |
||
92 | |||
93 | if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, null, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass, false, $queryName, null)) { |
||
94 | $args = $this->resolveResourceArgs($configuration['args'] ?? [], $queryName, $shortName); |
||
95 | $configuration['args'] = $args ?: $configuration['args'] ?? ['id' => ['type' => GraphQLType::nonNull(GraphQLType::id())]]; |
||
96 | |||
97 | return [$fieldName => array_merge($fieldConfiguration, $configuration)]; |
||
98 | } |
||
99 | |||
100 | return []; |
||
101 | } |
||
102 | |||
103 | /** |
||
104 | * {@inheritdoc} |
||
105 | */ |
||
106 | public function getCollectionQueryFields(string $resourceClass, ResourceMetadata $resourceMetadata, string $queryName, array $configuration): array |
||
107 | { |
||
108 | $shortName = $resourceMetadata->getShortName(); |
||
109 | $fieldName = lcfirst('collection_query' === $queryName ? $shortName : $queryName.$shortName); |
||
110 | |||
111 | $deprecationReason = (string) $resourceMetadata->getGraphqlAttribute($queryName, 'deprecation_reason', '', true); |
||
112 | |||
113 | if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, null, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass, false, $queryName, null)) { |
||
114 | $args = $this->resolveResourceArgs($configuration['args'] ?? [], $queryName, $shortName); |
||
115 | $configuration['args'] = $args ?: $configuration['args'] ?? $fieldConfiguration['args']; |
||
116 | |||
117 | return [Inflector::pluralize($fieldName) => array_merge($fieldConfiguration, $configuration)]; |
||
118 | } |
||
119 | |||
120 | return []; |
||
121 | } |
||
122 | |||
123 | /** |
||
124 | * {@inheritdoc} |
||
125 | */ |
||
126 | public function getMutationFields(string $resourceClass, ResourceMetadata $resourceMetadata, string $mutationName): array |
||
127 | { |
||
128 | $mutationFields = []; |
||
129 | $shortName = $resourceMetadata->getShortName(); |
||
130 | $resourceType = new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass); |
||
131 | $deprecationReason = $resourceMetadata->getGraphqlAttribute($mutationName, 'deprecation_reason', '', true); |
||
132 | |||
133 | if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, ucfirst("{$mutationName}s a $shortName."), $deprecationReason, $resourceType, $resourceClass, false, null, $mutationName)) { |
||
134 | $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, null, $deprecationReason, $resourceType, $resourceClass, true, null, $mutationName)]; |
||
135 | |||
136 | if (!$this->typeBuilder->isCollection($resourceType)) { |
||
137 | $fieldConfiguration['resolve'] = ($this->itemMutationResolverFactory)($resourceClass, null, $mutationName); |
||
138 | } |
||
139 | } |
||
140 | |||
141 | $mutationFields[$mutationName.$resourceMetadata->getShortName()] = $fieldConfiguration ?? []; |
||
142 | |||
143 | return $mutationFields; |
||
144 | } |
||
145 | |||
146 | /** |
||
147 | * {@inheritdoc} |
||
148 | */ |
||
149 | public function getResourceObjectTypeFields(?string $resourceClass, ResourceMetadata $resourceMetadata, bool $input, ?string $queryName, ?string $mutationName, int $depth = 0, ?array $ioMetadata = null): array |
||
150 | { |
||
151 | $fields = []; |
||
152 | $idField = ['type' => GraphQLType::nonNull(GraphQLType::id())]; |
||
153 | $clientMutationId = GraphQLType::string(); |
||
154 | |||
155 | if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null === $ioMetadata['class']) { |
||
156 | if ($input) { |
||
157 | return ['clientMutationId' => $clientMutationId]; |
||
158 | } |
||
159 | |||
160 | return []; |
||
161 | } |
||
162 | |||
163 | if ('delete' === $mutationName) { |
||
164 | $fields = [ |
||
165 | 'id' => $idField, |
||
166 | ]; |
||
167 | |||
168 | if ($input) { |
||
169 | $fields['clientMutationId'] = $clientMutationId; |
||
170 | } |
||
171 | |||
172 | return $fields; |
||
173 | } |
||
174 | |||
175 | if (!$input || 'create' !== $mutationName) { |
||
176 | $fields['id'] = $idField; |
||
177 | } |
||
178 | |||
179 | ++$depth; // increment the depth for the call to getResourceFieldConfiguration. |
||
180 | |||
181 | if (null !== $resourceClass) { |
||
182 | foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) { |
||
183 | $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, ['graphql_operation_name' => $mutationName ?? $queryName]); |
||
184 | if ( |
||
185 | null === ($propertyType = $propertyMetadata->getType()) |
||
186 | || (!$input && false === $propertyMetadata->isReadable()) |
||
187 | || ($input && null !== $mutationName && false === $propertyMetadata->isWritable()) |
||
188 | ) { |
||
189 | continue; |
||
190 | } |
||
191 | |||
192 | if ($fieldConfiguration = $this->getResourceFieldConfiguration($property, $propertyMetadata->getDescription(), $propertyMetadata->getAttribute('deprecation_reason', ''), $propertyType, $resourceClass, $input, $queryName, $mutationName, $depth)) { |
||
193 | $fields['id' === $property ? '_id' : $property] = $fieldConfiguration; |
||
194 | } |
||
195 | } |
||
196 | } |
||
197 | |||
198 | if (null !== $mutationName && $input) { |
||
199 | $fields['clientMutationId'] = $clientMutationId; |
||
200 | } |
||
201 | |||
202 | return $fields; |
||
203 | } |
||
204 | |||
205 | /** |
||
206 | * {@inheritdoc} |
||
207 | */ |
||
208 | public function resolveResourceArgs(array $args, string $operationName, string $shortName): array |
||
209 | { |
||
210 | foreach ($args as $id => $arg) { |
||
211 | if (!isset($arg['type'])) { |
||
212 | throw new \InvalidArgumentException(sprintf('The argument "%s" of the custom operation "%s" in %s needs a "type" option.', $id, $operationName, $shortName)); |
||
213 | } |
||
214 | |||
215 | $args[$id]['type'] = $this->typeConverter->resolveType($arg['type']); |
||
216 | } |
||
217 | |||
218 | return $args; |
||
219 | } |
||
220 | |||
221 | /** |
||
222 | * Get the field configuration of a resource. |
||
223 | * |
||
224 | * @see http://webonyx.github.io/graphql-php/type-system/object-types/ |
||
225 | */ |
||
226 | private function getResourceFieldConfiguration(?string $property, ?string $fieldDescription, string $deprecationReason, Type $type, string $rootResource, bool $input, ?string $queryName, ?string $mutationName, int $depth = 0): ?array |
||
227 | { |
||
228 | try { |
||
229 | $resourceClass = $this->typeBuilder->isCollection($type) && ($collectionValueType = $type->getCollectionValueType()) ? $collectionValueType->getClassName() : $type->getClassName(); |
||
230 | |||
231 | if (null === $graphqlType = $this->convertType($type, $input, $queryName, $mutationName, $resourceClass ?? '', $rootResource, $property, $depth)) { |
||
232 | return null; |
||
233 | } |
||
234 | |||
235 | $graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType() : $graphqlType; |
||
236 | $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true); |
||
237 | if ($isStandardGraphqlType) { |
||
238 | $resourceClass = ''; |
||
239 | } |
||
240 | |||
241 | $resourceMetadata = null; |
||
242 | if (!empty($resourceClass)) { |
||
243 | try { |
||
244 | $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); |
||
245 | } catch (ResourceClassNotFoundException $e) { |
||
0 ignored issues
–
show
Coding Style
Comprehensibility
introduced
by
Loading history...
|
|||
246 | } |
||
247 | } |
||
248 | |||
249 | $args = []; |
||
250 | if (!$input && null === $mutationName && !$isStandardGraphqlType && $this->typeBuilder->isCollection($type)) { |
||
251 | if ($this->pagination->isGraphQlEnabled($resourceClass, $queryName)) { |
||
252 | $args = [ |
||
253 | 'first' => [ |
||
254 | 'type' => GraphQLType::int(), |
||
255 | 'description' => 'Returns the first n elements from the list.', |
||
256 | ], |
||
257 | 'last' => [ |
||
258 | 'type' => GraphQLType::int(), |
||
259 | 'description' => 'Returns the last n elements from the list.', |
||
260 | ], |
||
261 | 'before' => [ |
||
262 | 'type' => GraphQLType::string(), |
||
263 | 'description' => 'Returns the elements in the list that come before the specified cursor.', |
||
264 | ], |
||
265 | 'after' => [ |
||
266 | 'type' => GraphQLType::string(), |
||
267 | 'description' => 'Returns the elements in the list that come after the specified cursor.', |
||
268 | ], |
||
269 | ]; |
||
270 | } |
||
271 | |||
272 | $args = $this->getFilterArgs($args, $resourceClass, $resourceMetadata, $rootResource, $property, $queryName, $mutationName, $depth); |
||
273 | } |
||
274 | |||
275 | if ($isStandardGraphqlType || $input) { |
||
276 | $resolve = null; |
||
277 | } elseif ($this->typeBuilder->isCollection($type)) { |
||
278 | $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $queryName); |
||
279 | } else { |
||
280 | $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $queryName); |
||
281 | } |
||
282 | |||
283 | return [ |
||
284 | 'type' => $graphqlType, |
||
285 | 'description' => $fieldDescription, |
||
286 | 'args' => $args, |
||
287 | 'resolve' => $resolve, |
||
288 | 'deprecationReason' => $deprecationReason, |
||
289 | ]; |
||
290 | } catch (InvalidTypeException $e) { |
||
291 | // just ignore invalid types |
||
292 | } |
||
293 | |||
294 | return null; |
||
295 | } |
||
296 | |||
297 | private function getFilterArgs(array $args, ?string $resourceClass, ?ResourceMetadata $resourceMetadata, string $rootResource, ?string $property, ?string $queryName, ?string $mutationName, int $depth): array |
||
298 | { |
||
299 | if (null === $resourceMetadata || null === $resourceClass) { |
||
300 | return $args; |
||
301 | } |
||
302 | |||
303 | foreach ($resourceMetadata->getGraphqlAttribute($queryName, 'filters', [], true) as $filterId) { |
||
304 | if (null === $this->filterLocator || !$this->filterLocator->has($filterId)) { |
||
305 | continue; |
||
306 | } |
||
307 | |||
308 | foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) { |
||
309 | $nullable = isset($value['required']) ? !$value['required'] : true; |
||
310 | $filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']); |
||
311 | $graphqlFilterType = $this->convertType($filterType, false, $queryName, $mutationName, $resourceClass, $rootResource, $property, $depth); |
||
312 | |||
313 | if ('[]' === substr($key, -2)) { |
||
314 | $graphqlFilterType = GraphQLType::listOf($graphqlFilterType); |
||
315 | $key = substr($key, 0, -2).'_list'; |
||
316 | } |
||
317 | |||
318 | parse_str($key, $parsed); |
||
319 | if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) { |
||
320 | $parsed = [$key => '']; |
||
321 | } |
||
322 | array_walk_recursive($parsed, function (&$value) use ($graphqlFilterType) { |
||
323 | $value = $graphqlFilterType; |
||
324 | }); |
||
325 | $args = $this->mergeFilterArgs($args, $parsed, $resourceMetadata, $key); |
||
326 | } |
||
327 | } |
||
328 | |||
329 | return $this->convertFilterArgsToTypes($args); |
||
330 | } |
||
331 | |||
332 | private function mergeFilterArgs(array $args, array $parsed, ResourceMetadata $resourceMetadata = null, $original = ''): array |
||
333 | { |
||
334 | foreach ($parsed as $key => $value) { |
||
335 | // Never override keys that cannot be merged |
||
336 | if (isset($args[$key]) && !\is_array($args[$key])) { |
||
337 | continue; |
||
338 | } |
||
339 | |||
340 | if (\is_array($value)) { |
||
341 | $value = $this->mergeFilterArgs($args[$key] ?? [], $value); |
||
342 | if (!isset($value['#name'])) { |
||
343 | $name = (false === $pos = strrpos($original, '[')) ? $original : substr($original, 0, (int) $pos); |
||
344 | $value['#name'] = ($resourceMetadata ? $resourceMetadata->getShortName() : '').'Filter_'.strtr($name, ['[' => '_', ']' => '', '.' => '__']); |
||
345 | } |
||
346 | } |
||
347 | |||
348 | $args[$key] = $value; |
||
349 | } |
||
350 | |||
351 | return $args; |
||
352 | } |
||
353 | |||
354 | private function convertFilterArgsToTypes(array $args): array |
||
355 | { |
||
356 | foreach ($args as $key => $value) { |
||
357 | if (strpos($key, '.')) { |
||
358 | // Declare relations/nested fields in a GraphQL compatible syntax. |
||
359 | $args[str_replace('.', '_', $key)] = $value; |
||
360 | unset($args[$key]); |
||
361 | } |
||
362 | } |
||
363 | |||
364 | foreach ($args as $key => $value) { |
||
365 | if (!\is_array($value) || !isset($value['#name'])) { |
||
366 | continue; |
||
367 | } |
||
368 | |||
369 | $name = $value['#name']; |
||
370 | |||
371 | if ($this->typesContainer->has($name)) { |
||
372 | $args[$key] = $this->typesContainer->get($name); |
||
373 | continue; |
||
374 | } |
||
375 | |||
376 | unset($value['#name']); |
||
377 | |||
378 | $filterArgType = new InputObjectType([ |
||
379 | 'name' => $name, |
||
380 | 'fields' => $this->convertFilterArgsToTypes($value), |
||
381 | ]); |
||
382 | |||
383 | $this->typesContainer->set($name, $filterArgType); |
||
384 | |||
385 | $args[$key] = $filterArgType; |
||
386 | } |
||
387 | |||
388 | return $args; |
||
389 | } |
||
390 | |||
391 | /** |
||
392 | * Converts a built-in type to its GraphQL equivalent. |
||
393 | * |
||
394 | * @throws InvalidTypeException |
||
395 | */ |
||
396 | private function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, string $rootResource, ?string $property, int $depth) |
||
397 | { |
||
398 | $graphqlType = $this->typeConverter->convertType($type, $input, $queryName, $mutationName, $resourceClass, $rootResource, $property, $depth); |
||
399 | |||
400 | if (null === $graphqlType) { |
||
401 | throw new InvalidTypeException(sprintf('The type "%s" is not supported.', $type->getBuiltinType())); |
||
402 | } |
||
403 | |||
404 | if (\is_string($graphqlType)) { |
||
405 | if (!$this->typesContainer->has($graphqlType)) { |
||
406 | throw new InvalidTypeException(sprintf('The GraphQL type %s is not valid. Valid types are: %s. Have you registered this type by implementing %s?', $graphqlType, implode(', ', array_keys($this->typesContainer->all())), TypeInterface::class)); |
||
407 | } |
||
408 | |||
409 | $graphqlType = $this->typesContainer->get($graphqlType); |
||
410 | } |
||
411 | |||
412 | if ($this->typeBuilder->isCollection($type)) { |
||
413 | return $this->pagination->isGraphQlEnabled($resourceClass, $queryName ?? $mutationName) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType) : GraphQLType::listOf($graphqlType); |
||
414 | } |
||
415 | |||
416 | return !$graphqlType instanceof NullableType || $type->isNullable() || (null !== $mutationName && 'update' === $mutationName) |
||
417 | ? $graphqlType |
||
418 | : GraphQLType::nonNull($graphqlType); |
||
419 | } |
||
420 | } |
||
421 |