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); |
||||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
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); |
||||
0 ignored issues
–
show
It seems like
$shortName can also be of type null ; however, parameter $shortName of ApiPlatform\Core\GraphQl...::resolveResourceArgs() 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...
|
|||||
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) { |
||||
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) { |
||||
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...
|
|||||
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 |