Completed
Push — master ( d0bde7...345612 )
by Antoine
26s queued 11s
created

SchemaBuilder::getResourceObjectType()   F

Complexity

Conditions 19
Paths 528

Size

Total Lines 57
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 34
nc 528
nop 6
dl 0
loc 57
rs 1.0055
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Serializer\ItemNormalizer;
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\Factory\ResourceNameCollectionFactoryInterface;
23
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
24
use ApiPlatform\Core\Util\ClassInfoTrait;
25
use Doctrine\Common\Inflector\Inflector;
26
use GraphQL\Type\Definition\InputObjectType;
27
use GraphQL\Type\Definition\InterfaceType;
28
use GraphQL\Type\Definition\ObjectType;
29
use GraphQL\Type\Definition\Type as GraphQLType;
30
use GraphQL\Type\Definition\WrappingType;
31
use GraphQL\Type\Schema;
32
use Psr\Container\ContainerInterface;
33
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
34
use Symfony\Component\PropertyInfo\Type;
35
36
/**
37
 * Builds the GraphQL schema.
38
 *
39
 * @experimental
40
 *
41
 * @author Raoul Clais <[email protected]>
42
 * @author Alan Poulain <[email protected]>
43
 * @author Kévin Dunglas <[email protected]>
44
 */
45
final class SchemaBuilder implements SchemaBuilderInterface
46
{
47
    use ClassInfoTrait;
48
49
    private $propertyNameCollectionFactory;
50
    private $propertyMetadataFactory;
51
    private $resourceNameCollectionFactory;
52
    private $resourceMetadataFactory;
53
    private $collectionResolverFactory;
54
    private $itemResolver;
55
    private $itemMutationResolverFactory;
56
    private $defaultFieldResolver;
57
    private $queryResolverLocator;
58
    private $filterLocator;
59
    private $typesFactory;
60
    private $paginationEnabled;
61
    private $graphqlTypes = [];
62
63
    public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResolverFactoryInterface $collectionResolverFactory, ResolverFactoryInterface $itemMutationResolverFactory, callable $itemResolver, callable $defaultFieldResolver, ContainerInterface $queryResolverLocator, TypesFactoryInterface $typesFactory, ContainerInterface $filterLocator = null, bool $paginationEnabled = true)
64
    {
65
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
66
        $this->propertyMetadataFactory = $propertyMetadataFactory;
67
        $this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
68
        $this->resourceMetadataFactory = $resourceMetadataFactory;
69
        $this->collectionResolverFactory = $collectionResolverFactory;
70
        $this->itemResolver = $itemResolver;
71
        $this->itemMutationResolverFactory = $itemMutationResolverFactory;
72
        $this->defaultFieldResolver = $defaultFieldResolver;
73
        $this->queryResolverLocator = $queryResolverLocator;
74
        $this->typesFactory = $typesFactory;
75
        $this->filterLocator = $filterLocator;
76
        $this->paginationEnabled = $paginationEnabled;
77
    }
78
79
    public function getSchema(): Schema
80
    {
81
        $this->graphqlTypes += $this->typesFactory->getTypes();
82
83
        $queryFields = ['node' => $this->getNodeQueryField()];
84
        $mutationFields = [];
85
86
        foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
87
            $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
88
            $graphqlConfiguration = $resourceMetadata->getGraphql() ?? [];
89
            foreach ($graphqlConfiguration as $operationName => $value) {
90
                if ('query' === $operationName) {
91
                    $queryFields += $this->getQueryFields($resourceClass, $resourceMetadata, $operationName, ['args' => ['id' => ['type' => GraphQLType::id()]]], []);
92
93
                    continue;
94
                }
95
96
                if ($itemQuery = $resourceMetadata->getGraphqlAttribute($operationName, 'item_query')) {
97
                    $value['resolve'] = $this->queryResolverLocator->get($itemQuery);
98
99
                    $queryFields += $this->getQueryFields($resourceClass, $resourceMetadata, $operationName, $value, false);
100
101
                    continue;
102
                }
103
104
                if ($collectionQuery = $resourceMetadata->getGraphqlAttribute($operationName, 'collection_query')) {
105
                    $value['resolve'] = $this->queryResolverLocator->get($collectionQuery);
106
107
                    $queryFields += $this->getQueryFields($resourceClass, $resourceMetadata, $operationName, false, $value);
108
109
                    continue;
110
                }
111
112
                $mutationFields[$operationName.$resourceMetadata->getShortName()] = $this->getMutationField($resourceClass, $resourceMetadata, $operationName);
113
            }
114
        }
115
116
        $schema = [
117
            'query' => new ObjectType([
118
                'name' => 'Query',
119
                'fields' => $queryFields,
120
            ]),
121
            'typeLoader' => function ($name) {
122
                return $this->graphqlTypes[$name];
123
            },
124
        ];
125
126
        if ($mutationFields) {
127
            $schema['mutation'] = new ObjectType([
128
                'name' => 'Mutation',
129
                'fields' => $mutationFields,
130
            ]);
131
        }
132
133
        return new Schema($schema);
134
    }
135
136
    private function getNodeInterface(): InterfaceType
137
    {
138
        if (isset($this->graphqlTypes['Node'])) {
139
            return $this->graphqlTypes['Node'];
140
        }
141
142
        return $this->graphqlTypes['Node'] = new InterfaceType([
143
            'name' => 'Node',
144
            'description' => 'A node, according to the Relay specification.',
145
            'fields' => [
146
                'id' => [
147
                    'type' => GraphQLType::nonNull(GraphQLType::id()),
148
                    'description' => 'The id of this node.',
149
                ],
150
            ],
151
            'resolveType' => function ($value) {
152
                if (!isset($value[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY])) {
153
                    return null;
154
                }
155
156
                $shortName = (new \ReflectionClass($value[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY]))->getShortName();
157
158
                return $this->graphqlTypes[$shortName] ?? null;
159
            },
160
        ]);
161
    }
162
163
    private function getNodeQueryField(): array
164
    {
165
        return [
166
            'type' => $this->getNodeInterface(),
167
            'args' => [
168
                'id' => ['type' => GraphQLType::nonNull(GraphQLType::id())],
169
            ],
170
            'resolve' => $this->itemResolver,
171
        ];
172
    }
173
174
    /**
175
     * Gets the query fields of the schema.
176
     *
177
     * @param array|false $itemConfiguration       false if not configured
178
     * @param array|false $collectionConfiguration false if not configured
179
     */
180
    private function getQueryFields(string $resourceClass, ResourceMetadata $resourceMetadata, string $operationName, $itemConfiguration, $collectionConfiguration): array
181
    {
182
        $queryFields = [];
183
        $shortName = $resourceMetadata->getShortName();
184
        $fieldName = lcfirst('query' === $operationName ? $shortName : $operationName.$shortName);
185
186
        $deprecationReason = $resourceMetadata->getGraphqlAttribute($operationName, 'deprecation_reason', '', true);
187
188
        if (false !== $itemConfiguration && $fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, null, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass)) {
189
            $queryFields[$fieldName] = array_merge($fieldConfiguration, $itemConfiguration);
190
        }
191
192
        if (false !== $collectionConfiguration && $fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, null, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass)) {
193
            $queryFields[Inflector::pluralize($fieldName)] = array_merge($fieldConfiguration, $collectionConfiguration);
194
        }
195
196
        return $queryFields;
197
    }
198
199
    /**
200
     * Gets the mutation field for the given operation name.
201
     */
202
    private function getMutationField(string $resourceClass, ResourceMetadata $resourceMetadata, string $mutationName): array
203
    {
204
        $shortName = $resourceMetadata->getShortName();
205
        $resourceType = new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass);
206
        $deprecationReason = $resourceMetadata->getGraphqlAttribute($mutationName, 'deprecation_reason', '', true);
207
208
        if ($fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, ucfirst("{$mutationName}s a $shortName."), $deprecationReason, $resourceType, $resourceClass, false, $mutationName)) {
209
            $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, null, $deprecationReason, $resourceType, $resourceClass, true, $mutationName)];
210
211
            if (!$this->isCollection($resourceType)) {
212
                $itemMutationResolverFactory = $this->itemMutationResolverFactory;
213
                $fieldConfiguration['resolve'] = $itemMutationResolverFactory($resourceClass, null, $mutationName);
214
            }
215
        }
216
217
        return $fieldConfiguration ?? [];
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 $resourceClass, ResourceMetadata $resourceMetadata, ?string $fieldDescription, string $deprecationReason, Type $type, string $rootResource, bool $input = false, string $mutationName = null, int $depth = 0): ?array
226
    {
227
        try {
228
            if (null === $graphqlType = $this->convertType($type, $input, $mutationName, $depth)) {
229
                return null;
230
            }
231
232
            $graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType() : $graphqlType;
233
            $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true);
234
            if ($isStandardGraphqlType) {
235
                $className = '';
236
            } else {
237
                $className = $this->isCollection($type) && ($collectionValueType = $type->getCollectionValueType()) ? $collectionValueType->getClassName() : $type->getClassName();
238
            }
239
240
            $args = [];
241
            if (!$input && null === $mutationName && !$isStandardGraphqlType && $this->isCollection($type)) {
242
                if ($this->paginationEnabled) {
243
                    $args = [
244
                        'first' => [
245
                            'type' => GraphQLType::int(),
246
                            'description' => 'Returns the first n elements from the list.',
247
                        ],
248
                        'last' => [
249
                            'type' => GraphQLType::int(),
250
                            'description' => 'Returns the last n elements from the list.',
251
                        ],
252
                        'before' => [
253
                            'type' => GraphQLType::string(),
254
                            'description' => 'Returns the elements in the list that come before the specified cursor.',
255
                        ],
256
                        'after' => [
257
                            'type' => GraphQLType::string(),
258
                            'description' => 'Returns the elements in the list that come after the specified cursor.',
259
                        ],
260
                    ];
261
                }
262
263
                foreach ($resourceMetadata->getGraphqlAttribute('query', 'filters', [], true) as $filterId) {
264
                    if (null === $this->filterLocator || !$this->filterLocator->has($filterId)) {
265
                        continue;
266
                    }
267
268
                    foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) {
269
                        $nullable = isset($value['required']) ? !$value['required'] : true;
270
                        $filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']);
271
                        $graphqlFilterType = $this->convertType($filterType, false, null, $depth);
272
273
                        if ('[]' === substr($key, -2)) {
274
                            $graphqlFilterType = GraphQLType::listOf($graphqlFilterType);
275
                            $key = substr($key, 0, -2).'_list';
276
                        }
277
278
                        parse_str($key, $parsed);
279
                        if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) {
280
                            $parsed = [$key => ''];
281
                        }
282
                        array_walk_recursive($parsed, function (&$value) use ($graphqlFilterType) {
283
                            $value = $graphqlFilterType;
284
                        });
285
                        $args = $this->mergeFilterArgs($args, $parsed, $resourceMetadata, $key);
286
                    }
287
                }
288
                $args = $this->convertFilterArgsToTypes($args);
289
            }
290
291
            if ($isStandardGraphqlType || $input) {
292
                $resolve = null;
293
            } elseif ($this->isCollection($type)) {
294
                $resolverFactory = $this->collectionResolverFactory;
295
                $resolve = $resolverFactory($className, $rootResource, $mutationName);
296
            } else {
297
                $resolve = $this->itemResolver;
298
            }
299
300
            return [
301
                'type' => $graphqlType,
302
                'description' => $fieldDescription,
303
                'args' => $args,
304
                'resolve' => $resolve,
305
                'deprecationReason' => $deprecationReason,
306
            ];
307
        } catch (InvalidTypeException $e) {
308
            // just ignore invalid types
309
        }
310
311
        return null;
312
    }
313
314
    private function mergeFilterArgs(array $args, array $parsed, ResourceMetadata $resourceMetadata = null, $original = ''): array
315
    {
316
        foreach ($parsed as $key => $value) {
317
            // Never override keys that cannot be merged
318
            if (isset($args[$key]) && !\is_array($args[$key])) {
319
                continue;
320
            }
321
322
            if (\is_array($value)) {
323
                $value = $this->mergeFilterArgs($args[$key] ?? [], $value);
324
                if (!isset($value['#name'])) {
325
                    $name = (false === $pos = strrpos($original, '[')) ? $original : substr($original, 0, (int) $pos);
326
                    $value['#name'] = ($resourceMetadata ? $resourceMetadata->getShortName() : '').'Filter_'.strtr($name, ['[' => '_', ']' => '', '.' => '__']);
327
                }
328
            }
329
330
            $args[$key] = $value;
331
        }
332
333
        return $args;
334
    }
335
336
    private function convertFilterArgsToTypes(array $args): array
337
    {
338
        foreach ($args as $key => $value) {
339
            if (strpos($key, '.')) {
340
                // Declare relations/nested fields in a GraphQL compatible syntax.
341
                $args[str_replace('.', '_', $key)] = $value;
342
                unset($args[$key]);
343
            }
344
        }
345
346
        foreach ($args as $key => $value) {
347
            if (!\is_array($value) || !isset($value['#name'])) {
348
                continue;
349
            }
350
351
            if (isset($this->graphqlTypes[$value['#name']])) {
352
                $args[$key] = $this->graphqlTypes[$value['#name']];
353
                continue;
354
            }
355
356
            $name = $value['#name'];
357
            unset($value['#name']);
358
359
            $this->graphqlTypes[$name] = $args[$key] = new InputObjectType([
360
                'name' => $name,
361
                'fields' => $this->convertFilterArgsToTypes($value),
362
            ]);
363
        }
364
365
        return $args;
366
    }
367
368
    /**
369
     * Converts a built-in type to its GraphQL equivalent.
370
     *
371
     * @throws InvalidTypeException
372
     */
373
    private function convertType(Type $type, bool $input = false, string $mutationName = null, int $depth = 0)
374
    {
375
        switch ($builtinType = $type->getBuiltinType()) {
376
            case Type::BUILTIN_TYPE_BOOL:
377
                $graphqlType = GraphQLType::boolean();
378
                break;
379
            case Type::BUILTIN_TYPE_INT:
380
                $graphqlType = GraphQLType::int();
381
                break;
382
            case Type::BUILTIN_TYPE_FLOAT:
383
                $graphqlType = GraphQLType::float();
384
                break;
385
            case Type::BUILTIN_TYPE_STRING:
386
                $graphqlType = GraphQLType::string();
387
                break;
388
            case Type::BUILTIN_TYPE_ARRAY:
389
            case Type::BUILTIN_TYPE_ITERABLE:
390
                $graphqlType = $this->graphqlTypes['Iterable'];
391
                break;
392
            case Type::BUILTIN_TYPE_OBJECT:
393
                if (($input && $depth > 0) || is_a($type->getClassName(), \DateTimeInterface::class, true)) {
394
                    $graphqlType = GraphQLType::string();
395
                    break;
396
                }
397
398
                $resourceClass = $this->isCollection($type) && ($collectionValueType = $type->getCollectionValueType()) ? $collectionValueType->getClassName() : $type->getClassName();
399
                if (null === $resourceClass) {
400
                    return null;
401
                }
402
403
                try {
404
                    $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
405
                    if ([] === $resourceMetadata->getGraphql() ?? []) {
406
                        return null;
407
                    }
408
                } catch (ResourceClassNotFoundException $e) {
409
                    // Skip objects that are not resources for now
410
                    return null;
411
                }
412
413
                $graphqlType = $this->getResourceObjectType($resourceClass, $resourceMetadata, $input, $mutationName, false, $depth);
414
                break;
415
            default:
416
                throw new InvalidTypeException(sprintf('The type "%s" is not supported.', $builtinType));
417
        }
418
419
        if ($this->isCollection($type)) {
420
            return $this->paginationEnabled && !$input ? $this->getResourcePaginatedCollectionType($graphqlType) : GraphQLType::listOf($graphqlType);
421
        }
422
423
        return $type->isNullable() || (null !== $mutationName && 'update' === $mutationName) ? $graphqlType : GraphQLType::nonNull($graphqlType);
424
    }
425
426
    /**
427
     * Gets the object type of the given resource.
428
     *
429
     * @return ObjectType|InputObjectType
430
     */
431
    private function getResourceObjectType(?string $resourceClass, ResourceMetadata $resourceMetadata, bool $input = false, string $mutationName = null, bool $wrapped = false, int $depth = 0): GraphQLType
432
    {
433
        $shortName = $resourceMetadata->getShortName();
434
435
        if (null !== $mutationName) {
436
            $shortName = $mutationName.ucfirst($shortName);
437
        }
438
        if ($input) {
439
            $shortName .= 'Input';
440
        } elseif (null !== $mutationName) {
441
            if ($depth > 0) {
442
                $shortName .= 'Nested';
443
            }
444
            $shortName .= 'Payload';
445
        }
446
        if ($wrapped && null !== $mutationName) {
447
            $shortName .= 'Data';
448
        }
449
450
        if (isset($this->graphqlTypes[$shortName])) {
451
            return $this->graphqlTypes[$shortName];
452
        }
453
454
        $ioMetadata = $resourceMetadata->getAttribute($input ? 'input' : 'output');
455
456
        if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null !== $ioMetadata['class']) {
457
            $resourceClass = $ioMetadata['class'];
458
        }
459
460
        $wrapData = !$wrapped && null !== $mutationName && !$input && $depth < 1;
461
462
        $configuration = [
463
            'name' => $shortName,
464
            'description' => $resourceMetadata->getDescription(),
465
            'resolveField' => $this->defaultFieldResolver,
466
            'fields' => function () use ($resourceClass, $resourceMetadata, $input, $mutationName, $wrapData, $depth, $ioMetadata) {
467
                if ($wrapData) {
468
                    $queryNormalizationContext = $resourceMetadata->getGraphqlAttribute('query', 'normalization_context', [], true);
469
                    $mutationNormalizationContext = $resourceMetadata->getGraphqlAttribute($mutationName ?? '', 'normalization_context', [], true);
470
                    // Use a new type for the wrapped object only if there is a specific normalization context for the mutation.
471
                    // If not, use the query type in order to ensure the client cache could be used.
472
                    $useWrappedType = $queryNormalizationContext !== $mutationNormalizationContext;
473
474
                    return [
475
                        lcfirst($resourceMetadata->getShortName()) => $useWrappedType ?
476
                            $this->getResourceObjectType($resourceClass, $resourceMetadata, $input, $mutationName, true, $depth) :
477
                            $this->getResourceObjectType($resourceClass, $resourceMetadata, $input, null, true, $depth),
478
                        'clientMutationId' => GraphQLType::string(),
479
                    ];
480
                }
481
482
                return $this->getResourceObjectTypeFields($resourceClass, $resourceMetadata, $input, $mutationName, $depth, $ioMetadata);
483
            },
484
            'interfaces' => $wrapData ? [] : [$this->getNodeInterface()],
485
        ];
486
487
        return $this->graphqlTypes[$shortName] = $input ? new InputObjectType($configuration) : new ObjectType($configuration);
488
    }
489
490
    /**
491
     * Gets the fields of the type of the given resource.
492
     */
493
    private function getResourceObjectTypeFields(?string $resourceClass, ResourceMetadata $resourceMetadata, bool $input = false, string $mutationName = null, int $depth = 0, ?array $ioMetadata = null): array
494
    {
495
        $fields = [];
496
        $idField = ['type' => GraphQLType::nonNull(GraphQLType::id())];
497
        $clientMutationId = GraphQLType::string();
498
499
        if (null !== $ioMetadata && null === $ioMetadata['class']) {
500
            if ($input) {
501
                return ['clientMutationId' => $clientMutationId];
502
            }
503
504
            return [];
505
        }
506
507
        if ('delete' === $mutationName) {
508
            $fields = [
509
                'id' => $idField,
510
            ];
511
512
            if ($input) {
513
                $fields['clientMutationId'] = $clientMutationId;
514
            }
515
516
            return $fields;
517
        }
518
519
        if (!$input || 'create' !== $mutationName) {
520
            $fields['id'] = $idField;
521
        }
522
523
        ++$depth; // increment the depth for the call to getResourceFieldConfiguration.
524
525
        if (null !== $resourceClass) {
526
            foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
527
                $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, ['graphql_operation_name' => $mutationName ?? 'query']);
528
                if (
529
                    null === ($propertyType = $propertyMetadata->getType())
530
                    || (!$input && false === $propertyMetadata->isReadable())
531
                    || ($input && null !== $mutationName && false === $propertyMetadata->isWritable())
532
                ) {
533
                    continue;
534
                }
535
536
                $rootResource = $resourceClass;
537
                if (null !== $propertyMetadata->getSubresource()) {
538
                    $resourceClass = $propertyMetadata->getSubresource()->getResourceClass();
539
                    $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
540
                }
541
                if ($fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, $propertyMetadata->getDescription(), $propertyMetadata->getAttribute('deprecation_reason', ''), $propertyType, $rootResource, $input, $mutationName, $depth)) {
542
                    $fields['id' === $property ? '_id' : $property] = $fieldConfiguration;
543
                }
544
                $resourceClass = $rootResource;
545
            }
546
        }
547
548
        if (null !== $mutationName && $input) {
549
            $fields['clientMutationId'] = $clientMutationId;
550
        }
551
552
        return $fields;
553
    }
554
555
    /**
556
     * Gets the type of a paginated collection of the given resource type.
557
     *
558
     * @param ObjectType $resourceType
559
     *
560
     * @return ObjectType
561
     */
562
    private function getResourcePaginatedCollectionType(GraphQLType $resourceType): GraphQLType
563
    {
564
        $shortName = $resourceType->name;
565
566
        if (isset($this->graphqlTypes["{$shortName}Connection"])) {
567
            return $this->graphqlTypes["{$shortName}Connection"];
568
        }
569
570
        $edgeObjectTypeConfiguration = [
571
            'name' => "{$shortName}Edge",
572
            'description' => "Edge of $shortName.",
573
            'fields' => [
574
                'node' => $resourceType,
575
                'cursor' => GraphQLType::nonNull(GraphQLType::string()),
576
            ],
577
        ];
578
        $edgeObjectType = new ObjectType($edgeObjectTypeConfiguration);
579
        $this->graphqlTypes["{$shortName}Edge"] = $edgeObjectType;
580
581
        $pageInfoObjectTypeConfiguration = [
582
            'name' => "{$shortName}PageInfo",
583
            'description' => 'Information about the current page.',
584
            'fields' => [
585
                'endCursor' => GraphQLType::string(),
586
                'startCursor' => GraphQLType::string(),
587
                'hasNextPage' => GraphQLType::nonNull(GraphQLType::boolean()),
588
                'hasPreviousPage' => GraphQLType::nonNull(GraphQLType::boolean()),
589
            ],
590
        ];
591
        $pageInfoObjectType = new ObjectType($pageInfoObjectTypeConfiguration);
592
        $this->graphqlTypes["{$shortName}PageInfo"] = $pageInfoObjectType;
593
594
        $configuration = [
595
            'name' => "{$shortName}Connection",
596
            'description' => "Connection for $shortName.",
597
            'fields' => [
598
                'edges' => GraphQLType::listOf($edgeObjectType),
599
                'pageInfo' => GraphQLType::nonNull($pageInfoObjectType),
600
                'totalCount' => GraphQLType::nonNull(GraphQLType::int()),
601
            ],
602
        ];
603
604
        return $this->graphqlTypes["{$shortName}Connection"] = new ObjectType($configuration);
605
    }
606
607
    private function isCollection(Type $type): bool
608
    {
609
        return $type->isCollection() && Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType();
610
    }
611
}
612