Passed
Push — master ( 8bd912...d93388 )
by Alan
06:58 queued 02:20
created

src/GraphQl/Type/FieldsBuilder.php (3 issues)

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
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 ignore-type  annotation

94
            $args = $this->resolveResourceArgs($configuration['args'] ?? [], $queryName, /** @scrutinizer ignore-type */ $shortName);
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 ignore-type  annotation

114
            $args = $this->resolveResourceArgs($configuration['args'] ?? [], $queryName, /** @scrutinizer ignore-type */ $shortName);
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 ignore-type  annotation

303
        foreach ($resourceMetadata->getGraphqlAttribute(/** @scrutinizer ignore-type */ $queryName, 'filters', [], true) as $filterId) {
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