FieldsBuilder::getResourceFieldConfiguration()   F
last analyzed

Complexity

Conditions 17
Paths 1140

Size

Total Lines 69
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 17
eloc 44
c 1
b 0
f 0
nc 1140
nop 9
dl 0
loc 69
rs 1.0499

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
33
34
/**
35
 * Builds the GraphQL fields.
36
 *
37
 * @experimental
38
 *
39
 * @author Alan Poulain <[email protected]>
40
 */
41
final class FieldsBuilder implements FieldsBuilderInterface
42
{
43
    private $propertyNameCollectionFactory;
44
    private $propertyMetadataFactory;
45
    private $resourceMetadataFactory;
46
    private $typesContainer;
47
    private $typeBuilder;
48
    private $typeConverter;
49
    private $itemResolverFactory;
50
    private $collectionResolverFactory;
51
    private $itemMutationResolverFactory;
52
    private $filterLocator;
53
    private $pagination;
54
    private $nameConverter;
55
    private $nestingSeparator;
56
57
    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, ?NameConverterInterface $nameConverter, string $nestingSeparator)
58
    {
59
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
60
        $this->propertyMetadataFactory = $propertyMetadataFactory;
61
        $this->resourceMetadataFactory = $resourceMetadataFactory;
62
        $this->typesContainer = $typesContainer;
63
        $this->typeBuilder = $typeBuilder;
64
        $this->typeConverter = $typeConverter;
65
        $this->itemResolverFactory = $itemResolverFactory;
66
        $this->collectionResolverFactory = $collectionResolverFactory;
67
        $this->itemMutationResolverFactory = $itemMutationResolverFactory;
68
        $this->filterLocator = $filterLocator;
69
        $this->pagination = $pagination;
70
        $this->nameConverter = $nameConverter;
71
        $this->nestingSeparator = $nestingSeparator;
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public function getNodeQueryFields(): array
78
    {
79
        return [
80
            'type' => $this->typeBuilder->getNodeInterface(),
81
            'args' => [
82
                'id' => ['type' => GraphQLType::nonNull(GraphQLType::id())],
83
            ],
84
            'resolve' => ($this->itemResolverFactory)(),
85
        ];
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91
    public function getItemQueryFields(string $resourceClass, ResourceMetadata $resourceMetadata, string $queryName, array $configuration): array
92
    {
93
        $shortName = $resourceMetadata->getShortName();
94
        $fieldName = lcfirst('item_query' === $queryName ? $shortName : $queryName.$shortName);
95
96
        $deprecationReason = (string) $resourceMetadata->getGraphqlAttribute($queryName, 'deprecation_reason', '', true);
97
98
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, null, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass, false, $queryName, null)) {
99
            $args = $this->resolveResourceArgs($configuration['args'] ?? [], $queryName, $shortName);
0 ignored issues
show
Bug introduced by Alan Poulain
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

99
            $args = $this->resolveResourceArgs($configuration['args'] ?? [], $queryName, /** @scrutinizer ignore-type */ $shortName);
Loading history...
100
            $configuration['args'] = $args ?: $configuration['args'] ?? ['id' => ['type' => GraphQLType::nonNull(GraphQLType::id())]];
101
102
            return [$fieldName => array_merge($fieldConfiguration, $configuration)];
103
        }
104
105
        return [];
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111
    public function getCollectionQueryFields(string $resourceClass, ResourceMetadata $resourceMetadata, string $queryName, array $configuration): array
112
    {
113
        $shortName = $resourceMetadata->getShortName();
114
        $fieldName = lcfirst('collection_query' === $queryName ? $shortName : $queryName.$shortName);
115
116
        $deprecationReason = (string) $resourceMetadata->getGraphqlAttribute($queryName, 'deprecation_reason', '', true);
117
118
        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)) {
119
            $args = $this->resolveResourceArgs($configuration['args'] ?? [], $queryName, $shortName);
0 ignored issues
show
Bug introduced by Alan Poulain
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

119
            $args = $this->resolveResourceArgs($configuration['args'] ?? [], $queryName, /** @scrutinizer ignore-type */ $shortName);
Loading history...
120
            $configuration['args'] = $args ?: $configuration['args'] ?? $fieldConfiguration['args'];
121
122
            return [Inflector::pluralize($fieldName) => array_merge($fieldConfiguration, $configuration)];
123
        }
124
125
        return [];
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function getMutationFields(string $resourceClass, ResourceMetadata $resourceMetadata, string $mutationName): array
132
    {
133
        $mutationFields = [];
134
        $shortName = $resourceMetadata->getShortName();
135
        $resourceType = new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass);
136
        $deprecationReason = $resourceMetadata->getGraphqlAttribute($mutationName, 'deprecation_reason', '', true);
137
138
        if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, ucfirst("{$mutationName}s a $shortName."), $deprecationReason, $resourceType, $resourceClass, false, null, $mutationName)) {
139
            $fieldConfiguration['args'] += ['input' => $this->getResourceFieldConfiguration(null, null, $deprecationReason, $resourceType, $resourceClass, true, null, $mutationName)];
140
141
            if (!$this->typeBuilder->isCollection($resourceType)) {
142
                $fieldConfiguration['resolve'] = ($this->itemMutationResolverFactory)($resourceClass, null, $mutationName);
143
            }
144
        }
145
146
        $mutationFields[$mutationName.$resourceMetadata->getShortName()] = $fieldConfiguration ?? [];
147
148
        return $mutationFields;
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     */
154
    public function getResourceObjectTypeFields(?string $resourceClass, ResourceMetadata $resourceMetadata, bool $input, ?string $queryName, ?string $mutationName, int $depth = 0, ?array $ioMetadata = null): array
155
    {
156
        $fields = [];
157
        $idField = ['type' => GraphQLType::nonNull(GraphQLType::id())];
158
        $clientMutationId = GraphQLType::string();
159
160
        if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null === $ioMetadata['class']) {
161
            if ($input) {
162
                return ['clientMutationId' => $clientMutationId];
163
            }
164
165
            return [];
166
        }
167
168
        if ('delete' === $mutationName) {
169
            $fields = [
170
                'id' => $idField,
171
            ];
172
173
            if ($input) {
174
                $fields['clientMutationId'] = $clientMutationId;
175
            }
176
177
            return $fields;
178
        }
179
180
        if (!$input || 'create' !== $mutationName) {
181
            $fields['id'] = $idField;
182
        }
183
184
        ++$depth; // increment the depth for the call to getResourceFieldConfiguration.
185
186
        if (null !== $resourceClass) {
187
            foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
188
                $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, ['graphql_operation_name' => $mutationName ?? $queryName]);
189
                if (
190
                    null === ($propertyType = $propertyMetadata->getType())
191
                    || (!$input && false === $propertyMetadata->isReadable())
192
                    || ($input && null !== $mutationName && false === $propertyMetadata->isWritable())
193
                ) {
194
                    continue;
195
                }
196
197
                if ($fieldConfiguration = $this->getResourceFieldConfiguration($property, $propertyMetadata->getDescription(), $propertyMetadata->getAttribute('deprecation_reason', ''), $propertyType, $resourceClass, $input, $queryName, $mutationName, $depth)) {
198
                    $fields['id' === $property ? '_id' : $this->normalizePropertyName($property)] = $fieldConfiguration;
199
                }
200
            }
201
        }
202
203
        if (null !== $mutationName && $input) {
204
            $fields['clientMutationId'] = $clientMutationId;
205
        }
206
207
        return $fields;
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213
    public function resolveResourceArgs(array $args, string $operationName, string $shortName): array
214
    {
215
        foreach ($args as $id => $arg) {
216
            if (!isset($arg['type'])) {
217
                throw new \InvalidArgumentException(sprintf('The argument "%s" of the custom operation "%s" in %s needs a "type" option.', $id, $operationName, $shortName));
218
            }
219
220
            $args[$id]['type'] = $this->typeConverter->resolveType($arg['type']);
221
        }
222
223
        return $args;
224
    }
225
226
    /**
227
     * Get the field configuration of a resource.
228
     *
229
     * @see http://webonyx.github.io/graphql-php/type-system/object-types/
230
     */
231
    private function getResourceFieldConfiguration(?string $property, ?string $fieldDescription, string $deprecationReason, Type $type, string $rootResource, bool $input, ?string $queryName, ?string $mutationName, int $depth = 0): ?array
232
    {
233
        try {
234
            $resourceClass = $this->typeBuilder->isCollection($type) && ($collectionValueType = $type->getCollectionValueType()) ? $collectionValueType->getClassName() : $type->getClassName();
235
236
            if (null === $graphqlType = $this->convertType($type, $input, $queryName, $mutationName, $resourceClass ?? '', $rootResource, $property, $depth)) {
0 ignored issues
show
introduced by Alan Poulain
The condition null === $graphqlType = ...rce, $property, $depth) is always false.
Loading history...
237
                return null;
238
            }
239
240
            $graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType() : $graphqlType;
241
            $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true);
242
            if ($isStandardGraphqlType) {
243
                $resourceClass = '';
244
            }
245
246
            $resourceMetadata = null;
247
            if (!empty($resourceClass)) {
248
                try {
249
                    $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
250
                } catch (ResourceClassNotFoundException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by soyuka
Consider adding a comment why this CATCH block is empty.
Loading history...
251
                }
252
            }
253
254
            $args = [];
255
            if (!$input && null === $mutationName && !$isStandardGraphqlType && $this->typeBuilder->isCollection($type)) {
256
                if ($this->pagination->isGraphQlEnabled($resourceClass, $queryName)) {
257
                    $args = [
258
                        'first' => [
259
                            'type' => GraphQLType::int(),
260
                            'description' => 'Returns the first n elements from the list.',
261
                        ],
262
                        'last' => [
263
                            'type' => GraphQLType::int(),
264
                            'description' => 'Returns the last n elements from the list.',
265
                        ],
266
                        'before' => [
267
                            'type' => GraphQLType::string(),
268
                            'description' => 'Returns the elements in the list that come before the specified cursor.',
269
                        ],
270
                        'after' => [
271
                            'type' => GraphQLType::string(),
272
                            'description' => 'Returns the elements in the list that come after the specified cursor.',
273
                        ],
274
                    ];
275
                }
276
277
                $args = $this->getFilterArgs($args, $resourceClass, $resourceMetadata, $rootResource, $property, $queryName, $mutationName, $depth);
278
            }
279
280
            if ($isStandardGraphqlType || $input) {
281
                $resolve = null;
282
            } elseif ($this->typeBuilder->isCollection($type)) {
283
                $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $queryName);
284
            } else {
285
                $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $queryName);
286
            }
287
288
            return [
289
                'type' => $graphqlType,
290
                'description' => $fieldDescription,
291
                'args' => $args,
292
                'resolve' => $resolve,
293
                'deprecationReason' => $deprecationReason,
294
            ];
295
        } catch (InvalidTypeException $e) {
296
            // just ignore invalid types
297
        }
298
299
        return null;
300
    }
301
302
    private function getFilterArgs(array $args, ?string $resourceClass, ?ResourceMetadata $resourceMetadata, string $rootResource, ?string $property, ?string $queryName, ?string $mutationName, int $depth): array
303
    {
304
        if (null === $resourceMetadata || null === $resourceClass) {
305
            return $args;
306
        }
307
308
        foreach ($resourceMetadata->getGraphqlAttribute($queryName, 'filters', [], true) as $filterId) {
0 ignored issues
show
Bug introduced by Alan Poulain
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

308
        foreach ($resourceMetadata->getGraphqlAttribute(/** @scrutinizer ignore-type */ $queryName, 'filters', [], true) as $filterId) {
Loading history...
309
            if (null === $this->filterLocator || !$this->filterLocator->has($filterId)) {
310
                continue;
311
            }
312
313
            foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) {
314
                $nullable = isset($value['required']) ? !$value['required'] : true;
315
                $filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']);
316
                $graphqlFilterType = $this->convertType($filterType, false, $queryName, $mutationName, $resourceClass, $rootResource, $property, $depth);
317
318
                if ('[]' === substr($key, -2)) {
319
                    $graphqlFilterType = GraphQLType::listOf($graphqlFilterType);
320
                    $key = substr($key, 0, -2).'_list';
321
                }
322
323
                /** @var string $key */
324
                $key = str_replace('.', $this->nestingSeparator, $key);
325
326
                parse_str($key, $parsed);
327
                if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) {
328
                    $parsed = [$key => ''];
329
                }
330
                array_walk_recursive($parsed, function (&$value) use ($graphqlFilterType) {
331
                    $value = $graphqlFilterType;
332
                });
333
                $args = $this->mergeFilterArgs($args, $parsed, $resourceMetadata, $key);
334
            }
335
        }
336
337
        return $this->convertFilterArgsToTypes($args);
338
    }
339
340
    private function mergeFilterArgs(array $args, array $parsed, ResourceMetadata $resourceMetadata = null, $original = ''): array
341
    {
342
        foreach ($parsed as $key => $value) {
343
            // Never override keys that cannot be merged
344
            if (isset($args[$key]) && !\is_array($args[$key])) {
345
                continue;
346
            }
347
348
            if (\is_array($value)) {
349
                $value = $this->mergeFilterArgs($args[$key] ?? [], $value);
350
                if (!isset($value['#name'])) {
351
                    $name = (false === $pos = strrpos($original, '[')) ? $original : substr($original, 0, (int) $pos);
352
                    $value['#name'] = ($resourceMetadata ? $resourceMetadata->getShortName() : '').'Filter_'.strtr($name, ['[' => '_', ']' => '', '.' => '__']);
353
                }
354
            }
355
356
            $args[$key] = $value;
357
        }
358
359
        return $args;
360
    }
361
362
    private function convertFilterArgsToTypes(array $args): array
363
    {
364
        foreach ($args as $key => $value) {
365
            if (strpos($key, '.')) {
366
                // Declare relations/nested fields in a GraphQL compatible syntax.
367
                $args[str_replace('.', $this->nestingSeparator, $key)] = $value;
368
                unset($args[$key]);
369
            }
370
        }
371
372
        foreach ($args as $key => $value) {
373
            if (!\is_array($value) || !isset($value['#name'])) {
374
                continue;
375
            }
376
377
            $name = $value['#name'];
378
379
            if ($this->typesContainer->has($name)) {
380
                $args[$key] = $this->typesContainer->get($name);
381
                continue;
382
            }
383
384
            unset($value['#name']);
385
386
            $filterArgType = new InputObjectType([
387
                'name' => $name,
388
                'fields' => $this->convertFilterArgsToTypes($value),
389
            ]);
390
391
            $this->typesContainer->set($name, $filterArgType);
392
393
            $args[$key] = $filterArgType;
394
        }
395
396
        return $args;
397
    }
398
399
    /**
400
     * Converts a built-in type to its GraphQL equivalent.
401
     *
402
     * @throws InvalidTypeException
403
     */
404
    private function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, string $rootResource, ?string $property, int $depth)
405
    {
406
        $graphqlType = $this->typeConverter->convertType($type, $input, $queryName, $mutationName, $resourceClass, $rootResource, $property, $depth);
407
408
        if (null === $graphqlType) {
409
            throw new InvalidTypeException(sprintf('The type "%s" is not supported.', $type->getBuiltinType()));
410
        }
411
412
        if (\is_string($graphqlType)) {
413
            if (!$this->typesContainer->has($graphqlType)) {
414
                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));
415
            }
416
417
            $graphqlType = $this->typesContainer->get($graphqlType);
418
        }
419
420
        if ($this->typeBuilder->isCollection($type)) {
421
            return $this->pagination->isGraphQlEnabled($resourceClass, $queryName ?? $mutationName) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType) : GraphQLType::listOf($graphqlType);
422
        }
423
424
        return !$graphqlType instanceof NullableType || $type->isNullable() || (null !== $mutationName && 'update' === $mutationName)
425
            ? $graphqlType
426
            : GraphQLType::nonNull($graphqlType);
427
    }
428
429
    private function normalizePropertyName(string $property): string
430
    {
431
        return null !== $this->nameConverter ? $this->nameConverter->normalize($property) : $property;
432
    }
433
}
434