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