DefinitionBuilder   C
last analyzed

Complexity

Total Complexity 54

Size/Duplication

Total Lines 491
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 176
c 1
b 0
f 0
dl 0
loc 491
rs 6.4799
wmc 54

25 Methods

Rating   Name   Duplication   Size   Complexity  
A getFieldResolver() 0 5 2
A buildFields() 0 12 1
A buildArguments() 0 17 2
A registerTypes() 0 11 2
A registerDirectives() 0 11 2
A buildWrappedTypeRecursive() 0 13 3
A getTypeResolver() 0 5 2
B buildNamedType() 0 23 7
A defaultTypeResolver() 0 3 1
A resolveType() 0 3 1
A buildType() 0 21 4
A buildObjectType() 0 18 3
A getNamedTypeNode() 0 10 3
A buildScalarType() 0 10 1
A buildField() 0 10 2
A __construct() 0 13 1
A buildDirective() 0 20 3
A buildEnumType() 0 20 2
A buildUnionType() 0 11 2
A buildWrappedType() 0 5 1
A buildInputObjectType() 0 26 3
A getTypeDefinition() 0 3 1
A buildInterfaceType() 0 11 2
A buildTypes() 0 5 1
A getDeprecationReason() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like DefinitionBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DefinitionBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Digia\GraphQL\Schema;
4
5
use Digia\GraphQL\Error\InvalidTypeException;
6
use Digia\GraphQL\Error\InvariantException;
7
use Digia\GraphQL\Execution\ExecutionException;
8
use Digia\GraphQL\Execution\ValuesResolver;
9
use Digia\GraphQL\Language\LanguageException;
10
use Digia\GraphQL\Language\Node\DirectiveDefinitionNode;
11
use Digia\GraphQL\Language\Node\EnumTypeDefinitionNode;
12
use Digia\GraphQL\Language\Node\EnumValueDefinitionNode;
13
use Digia\GraphQL\Language\Node\FieldDefinitionNode;
14
use Digia\GraphQL\Language\Node\InputObjectTypeDefinitionNode;
15
use Digia\GraphQL\Language\Node\InputValueDefinitionNode;
16
use Digia\GraphQL\Language\Node\InterfaceTypeDefinitionNode;
17
use Digia\GraphQL\Language\Node\ListTypeNode;
18
use Digia\GraphQL\Language\Node\NamedTypeNode;
19
use Digia\GraphQL\Language\Node\NamedTypeNodeInterface;
20
use Digia\GraphQL\Language\Node\NameNode;
21
use Digia\GraphQL\Language\Node\NodeInterface;
22
use Digia\GraphQL\Language\Node\NonNullTypeNode;
23
use Digia\GraphQL\Language\Node\ObjectTypeDefinitionNode;
24
use Digia\GraphQL\Language\Node\ScalarTypeDefinitionNode;
25
use Digia\GraphQL\Language\Node\TypeNodeInterface;
26
use Digia\GraphQL\Language\Node\UnionTypeDefinitionNode;
27
use Digia\GraphQL\Schema\Resolver\ResolverRegistryInterface;
28
use Digia\GraphQL\Type\Definition\Directive;
29
use Digia\GraphQL\Type\Definition\EnumType;
30
use Digia\GraphQL\Type\Definition\InputObjectType;
31
use Digia\GraphQL\Type\Definition\InterfaceType;
32
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
33
use Digia\GraphQL\Type\Definition\ObjectType;
34
use Digia\GraphQL\Type\Definition\ScalarType;
35
use Digia\GraphQL\Type\Definition\TypeInterface;
36
use Digia\GraphQL\Type\Definition\UnionType;
37
use Digia\GraphQL\Util\ValueASTConverter;
38
use function Digia\GraphQL\Type\introspectionTypes;
39
use function Digia\GraphQL\Type\newDirective;
40
use function Digia\GraphQL\Type\newEnumType;
41
use function Digia\GraphQL\Type\newInputObjectType;
42
use function Digia\GraphQL\Type\newInterfaceType;
43
use function Digia\GraphQL\Type\newList;
44
use function Digia\GraphQL\Type\newNonNull;
45
use function Digia\GraphQL\Type\newObjectType;
46
use function Digia\GraphQL\Type\newScalarType;
47
use function Digia\GraphQL\Type\newUnionType;
48
use function Digia\GraphQL\Type\specifiedScalarTypes;
49
use function Digia\GraphQL\Util\keyMap;
50
use function Digia\GraphQL\Util\keyValueMap;
51
52
class DefinitionBuilder implements DefinitionBuilderInterface
53
{
54
    /**
55
     * @var NamedTypeNodeInterface[]
56
     */
57
    protected $typeDefinitionsMap;
58
59
    /**
60
     * @var ResolverRegistryInterface
61
     */
62
    protected $resolverRegistry;
63
64
    /**
65
     * @var callable
66
     */
67
    protected $resolveTypeFunction;
68
69
    /**
70
     * @var NamedTypeInterface[]
71
     */
72
    protected $types;
73
74
    /**
75
     * @var Directive[]
76
     */
77
    protected $directives;
78
79
    /**
80
     * DefinitionBuilder constructor.
81
     * @param array                          $typeDefinitionsMap
82
     * @param ResolverRegistryInterface|null $resolverRegistry
83
     * @param array                          $types
84
     * @param array                          $directives
85
     * @param callable|null                  $resolveTypeCallback
86
     */
87
    public function __construct(
88
        array $typeDefinitionsMap,
89
        ?ResolverRegistryInterface $resolverRegistry = null,
90
        array $types = [],
91
        array $directives = [],
92
        ?callable $resolveTypeCallback = null
93
    ) {
94
        $this->typeDefinitionsMap  = $typeDefinitionsMap;
95
        $this->resolverRegistry    = $resolverRegistry;
96
        $this->resolveTypeFunction = $resolveTypeCallback ?? [$this, 'defaultTypeResolver'];
97
98
        $this->registerTypes($types);
99
        $this->registerDirectives($directives);
100
    }
101
102
    /**
103
     * @inheritdoc
104
     */
105
    public function buildTypes(array $nodes): array
106
    {
107
        return \array_map(function (NamedTypeNodeInterface $node) {
108
            return $this->buildType($node);
109
        }, $nodes);
110
    }
111
112
    /**
113
     * @inheritdoc
114
     */
115
    public function buildType(NamedTypeNodeInterface $node): NamedTypeInterface
116
    {
117
        $typeName = $node->getNameValue();
118
119
        if (isset($this->types[$typeName])) {
120
            return $this->types[$typeName];
121
        }
122
123
        if ($node instanceof NamedTypeNode) {
124
            $definition = $this->getTypeDefinition($typeName);
0 ignored issues
show
Bug introduced by
It seems like $typeName can also be of type null; however, parameter $typeName of Digia\GraphQL\Schema\Def...er::getTypeDefinition() 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

124
            $definition = $this->getTypeDefinition(/** @scrutinizer ignore-type */ $typeName);
Loading history...
125
126
            /** @noinspection PhpUnhandledExceptionInspection */
127
            $type = null !== $definition
128
                ? $this->buildNamedType($definition)
129
                : $this->resolveType($node);
130
        } else {
131
            /** @noinspection PhpUnhandledExceptionInspection */
132
            $type = $this->buildNamedType($node);
133
        }
134
135
        return $this->types[$typeName] = $type;
136
    }
137
138
    /**
139
     * @inheritdoc
140
     */
141
    public function buildDirective(DirectiveDefinitionNode $node): Directive
142
    {
143
        $directiveName = $node->getNameValue();
144
145
        if (isset($this->directives[$directiveName])) {
146
            return $this->directives[$directiveName];
147
        }
148
149
        /** @noinspection PhpUnhandledExceptionInspection */
150
        $directive = newDirective([
151
            'name'        => $node->getNameValue(),
152
            'description' => $node->getDescriptionValue(),
153
            'locations'   => \array_map(function (NameNode $node) {
154
                return $node->getValue();
155
            }, $node->getLocations()),
156
            'args'        => $node->hasArguments() ? $this->buildArguments($node->getArguments()) : [],
157
            'astNode'     => $node,
158
        ]);
159
160
        return $this->directives[$directiveName] = $directive;
161
    }
162
163
    /**
164
     * @inheritdoc
165
     */
166
    public function buildField($node, ?callable $resolve = null): array
167
    {
168
        /** @noinspection PhpUnhandledExceptionInspection */
169
        return [
170
            'type'              => $this->buildWrappedType($node->getType()),
171
            'description'       => $node->getDescriptionValue(),
172
            'args'              => $node->hasArguments() ? $this->buildArguments($node->getArguments()) : [],
173
            'deprecationReason' => $this->getDeprecationReason($node),
174
            'resolve'           => $resolve,
175
            'astNode'           => $node,
176
        ];
177
    }
178
179
    /**
180
     * @param TypeNodeInterface $typeNode
181
     * @return TypeInterface
182
     * @throws InvariantException
183
     * @throws InvalidTypeException
184
     */
185
    protected function buildWrappedType(TypeNodeInterface $typeNode): TypeInterface
186
    {
187
        /** @noinspection PhpUnhandledExceptionInspection */
188
        $typeDefinition = $this->buildType($this->getNamedTypeNode($typeNode));
189
        return $this->buildWrappedTypeRecursive($typeDefinition, $typeNode);
190
    }
191
192
    /**
193
     * @param NamedTypeInterface $innerType
194
     * @param TypeNodeInterface  $inputTypeNode
195
     * @return TypeInterface
196
     * @throws InvariantException
197
     * @throws InvalidTypeException
198
     */
199
    protected function buildWrappedTypeRecursive(
200
        NamedTypeInterface $innerType,
201
        TypeNodeInterface $inputTypeNode
202
    ): TypeInterface {
203
        if ($inputTypeNode instanceof ListTypeNode) {
204
            return newList($this->buildWrappedTypeRecursive($innerType, $inputTypeNode->getType()));
205
        }
206
207
        if ($inputTypeNode instanceof NonNullTypeNode) {
208
            return newNonNull($this->buildWrappedTypeRecursive($innerType, $inputTypeNode->getType()));
209
        }
210
211
        return $innerType;
212
    }
213
214
    /**
215
     * @param array $customTypes
216
     */
217
    protected function registerTypes(array $customTypes)
218
    {
219
        $typesMap = keyMap(
220
            \array_merge($customTypes, specifiedScalarTypes(), introspectionTypes()),
221
            function (NamedTypeInterface $type) {
222
                return $type->getName();
223
            }
224
        );
225
226
        foreach ($typesMap as $typeName => $type) {
227
            $this->types[$typeName] = $type;
228
        }
229
    }
230
231
    /**
232
     * @param array $customDirectives
233
     */
234
    protected function registerDirectives(array $customDirectives)
235
    {
236
        $directivesMap = keyMap(
237
            \array_merge($customDirectives, specifiedDirectives()),
238
            function (Directive $directive) {
239
                return $directive->getName();
240
            }
241
        );
242
243
        foreach ($directivesMap as $directiveName => $directive) {
244
            $this->directives[$directiveName] = $directive;
245
        }
246
    }
247
248
    /**
249
     * @param array $nodes
250
     * @return array
251
     */
252
    protected function buildArguments(array $nodes): array
253
    {
254
        return keyValueMap(
255
            $nodes,
256
            function (InputValueDefinitionNode $value) {
257
                return $value->getNameValue();
258
            },
259
            function (InputValueDefinitionNode $value): array {
260
                $type         = $this->buildWrappedType($value->getType());
261
                $defaultValue = $value->getDefaultValue();
262
                return [
263
                    'type'         => $type,
264
                    'description'  => $value->getDescriptionValue(),
265
                    'defaultValue' => null !== $defaultValue
266
                        ? ValueASTConverter::convert($defaultValue, $type)
267
                        : null,
268
                    'astNode'      => $value,
269
                ];
270
            });
271
    }
272
273
    /**
274
     * @param TypeNodeInterface $node
275
     * @return NamedTypeInterface
276
     * @throws LanguageException
277
     */
278
    protected function buildNamedType(TypeNodeInterface $node): NamedTypeInterface
279
    {
280
        if ($node instanceof ObjectTypeDefinitionNode) {
281
            return $this->buildObjectType($node);
282
        }
283
        if ($node instanceof InterfaceTypeDefinitionNode) {
284
            return $this->buildInterfaceType($node);
285
        }
286
        if ($node instanceof EnumTypeDefinitionNode) {
287
            return $this->buildEnumType($node);
288
        }
289
        if ($node instanceof UnionTypeDefinitionNode) {
290
            return $this->buildUnionType($node);
291
        }
292
        if ($node instanceof ScalarTypeDefinitionNode) {
293
            return $this->buildScalarType($node);
294
        }
295
        if ($node instanceof InputObjectTypeDefinitionNode) {
296
            /** @noinspection PhpUnhandledExceptionInspection */
297
            return $this->buildInputObjectType($node);
298
        }
299
300
        throw new LanguageException(\sprintf('Type kind "%s" not supported.', $node->getKind()));
301
    }
302
303
    /**
304
     * @param ObjectTypeDefinitionNode $node
305
     * @return ObjectType
306
     */
307
    protected function buildObjectType(ObjectTypeDefinitionNode $node): ObjectType
308
    {
309
        /** @noinspection PhpUnhandledExceptionInspection */
310
        return newObjectType([
311
            'name'        => $node->getNameValue(),
312
            'description' => $node->getDescriptionValue(),
313
            'fields'      => $node->hasFields() ? function () use ($node) {
314
                return $this->buildFields($node);
315
            } : [],
316
            // Note: While this could make early assertions to get the correctly
317
            // typed values, that would throw immediately while type system
318
            // validation with validateSchema() will produce more actionable results.
319
            'interfaces'  => function () use ($node) {
320
                return $node->hasInterfaces() ? \array_map(function (NamedTypeNodeInterface $interface) {
321
                    return $this->buildType($interface);
322
                }, $node->getInterfaces()) : [];
323
            },
324
            'astNode'     => $node,
325
        ]);
326
    }
327
328
    /**
329
     * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|InputObjectTypeDefinitionNode $node
330
     * @return array
331
     */
332
    protected function buildFields($node): array
333
    {
334
        return keyValueMap(
335
            $node->getFields(),
336
            function ($value) {
337
                /** @var FieldDefinitionNode|InputValueDefinitionNode $value */
338
                return $value->getNameValue();
339
            },
340
            function ($value) use ($node) {
341
                /** @var FieldDefinitionNode|InputValueDefinitionNode $value */
342
                return $this->buildField($value,
343
                    $this->getFieldResolver($node->getNameValue(), $value->getNameValue()));
0 ignored issues
show
Bug introduced by
It seems like $node->getNameValue() can also be of type null; however, parameter $typeName of Digia\GraphQL\Schema\Def...der::getFieldResolver() 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

343
                    $this->getFieldResolver(/** @scrutinizer ignore-type */ $node->getNameValue(), $value->getNameValue()));
Loading history...
Bug introduced by
It seems like $value->getNameValue() can also be of type null; however, parameter $fieldName of Digia\GraphQL\Schema\Def...der::getFieldResolver() 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

343
                    $this->getFieldResolver($node->getNameValue(), /** @scrutinizer ignore-type */ $value->getNameValue()));
Loading history...
344
            }
345
        );
346
    }
347
348
    /**
349
     * @param string $typeName
350
     * @param string $fieldName
351
     * @return callable|null
352
     */
353
    protected function getFieldResolver(string $typeName, string $fieldName): ?callable
354
    {
355
        return null !== $this->resolverRegistry
356
            ? $this->resolverRegistry->getFieldResolver($typeName, $fieldName)
357
            : null;
358
    }
359
360
    /**
361
     * @param InterfaceTypeDefinitionNode $node
362
     * @return InterfaceType
363
     */
364
    protected function buildInterfaceType(InterfaceTypeDefinitionNode $node): InterfaceType
365
    {
366
        /** @noinspection PhpUnhandledExceptionInspection */
367
        return newInterfaceType([
368
            'name'        => $node->getNameValue(),
369
            'description' => $node->getDescriptionValue(),
370
            'fields'      => $node->hasFields() ? function () use ($node): array {
371
                return $this->buildFields($node);
372
            } : [],
373
            'resolveType' => $this->getTypeResolver($node->getNameValue()),
0 ignored issues
show
Bug introduced by
It seems like $node->getNameValue() can also be of type null; however, parameter $typeName of Digia\GraphQL\Schema\Def...lder::getTypeResolver() 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

373
            'resolveType' => $this->getTypeResolver(/** @scrutinizer ignore-type */ $node->getNameValue()),
Loading history...
374
            'astNode'     => $node,
375
        ]);
376
    }
377
378
    /**
379
     * @param EnumTypeDefinitionNode $node
380
     * @return EnumType
381
     */
382
    protected function buildEnumType(EnumTypeDefinitionNode $node): EnumType
383
    {
384
        /** @noinspection PhpUnhandledExceptionInspection */
385
        return newEnumType([
386
            'name'        => $node->getNameValue(),
387
            'description' => $node->getDescriptionValue(),
388
            'values'      => $node->hasValues() ? keyValueMap(
389
                $node->getValues(),
390
                function (EnumValueDefinitionNode $value): ?string {
391
                    return $value->getNameValue();
392
                },
393
                function (EnumValueDefinitionNode $value): array {
394
                    return [
395
                        'description'       => $value->getDescriptionValue(),
396
                        'deprecationReason' => $this->getDeprecationReason($value),
397
                        'astNode'           => $value,
398
                    ];
399
                }
400
            ) : [],
401
            'astNode'     => $node,
402
        ]);
403
    }
404
405
    /**
406
     * @param UnionTypeDefinitionNode $node
407
     * @return UnionType
408
     */
409
    protected function buildUnionType(UnionTypeDefinitionNode $node): UnionType
410
    {
411
        /** @noinspection PhpUnhandledExceptionInspection */
412
        return newUnionType([
413
            'name'        => $node->getNameValue(),
414
            'description' => $node->getDescriptionValue(),
415
            'types'       => $node->hasTypes() ? \array_map(function (NamedTypeNodeInterface $type) {
416
                return $this->buildType($type);
417
            }, $node->getTypes()) : [],
418
            'resolveType' => $this->getTypeResolver($node->getNameValue()),
0 ignored issues
show
Bug introduced by
It seems like $node->getNameValue() can also be of type null; however, parameter $typeName of Digia\GraphQL\Schema\Def...lder::getTypeResolver() 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

418
            'resolveType' => $this->getTypeResolver(/** @scrutinizer ignore-type */ $node->getNameValue()),
Loading history...
419
            'astNode'     => $node,
420
        ]);
421
    }
422
423
    /**
424
     * @param string $typeName
425
     * @return callable|null
426
     */
427
    protected function getTypeResolver(string $typeName): ?callable
428
    {
429
        return null !== $this->resolverRegistry
430
            ? $this->resolverRegistry->getTypeResolver($typeName)
431
            : null;
432
    }
433
434
    /**
435
     * @param ScalarTypeDefinitionNode $node
436
     * @return ScalarType
437
     */
438
    protected function buildScalarType(ScalarTypeDefinitionNode $node): ScalarType
439
    {
440
        /** @noinspection PhpUnhandledExceptionInspection */
441
        return newScalarType([
442
            'name'        => $node->getNameValue(),
443
            'description' => $node->getDescriptionValue(),
444
            'serialize'   => function ($value) {
445
                return $value;
446
            },
447
            'astNode'     => $node,
448
        ]);
449
    }
450
451
    /**
452
     * @param InputObjectTypeDefinitionNode $node
453
     * @return InputObjectType
454
     * @throws InvariantException
455
     */
456
    protected function buildInputObjectType(InputObjectTypeDefinitionNode $node): InputObjectType
457
    {
458
        return newInputObjectType([
459
            'name'        => $node->getNameValue(),
460
            'description' => $node->getDescriptionValue(),
461
            'fields'      => $node->hasFields() ? function () use ($node) {
462
                return keyValueMap(
463
                    $node->getFields(),
464
                    function (InputValueDefinitionNode $value): ?string {
465
                        return $value->getNameValue();
466
                    },
467
                    function (InputValueDefinitionNode $value): array {
468
                        $type         = $this->buildWrappedType($value->getType());
469
                        $defaultValue = $value->getDefaultValue();
470
                        return [
471
                            'type'         => $type,
472
                            'description'  => $value->getDescriptionValue(),
473
                            'defaultValue' => null !== $defaultValue
474
                                ? ValueASTConverter::convert($defaultValue, $type)
475
                                : null,
476
                            'astNode'      => $value,
477
                        ];
478
                    }
479
                );
480
            } : [],
481
            'astNode'     => $node,
482
        ]);
483
    }
484
485
    /**
486
     * @param NamedTypeNode $node
487
     * @return NamedTypeInterface
488
     */
489
    protected function resolveType(NamedTypeNode $node): NamedTypeInterface
490
    {
491
        return \call_user_func($this->resolveTypeFunction, $node);
492
    }
493
494
    /**
495
     * @param NamedTypeNode $node
496
     * @return NamedTypeInterface|null
497
     */
498
    public function defaultTypeResolver(NamedTypeNode $node): ?NamedTypeInterface
499
    {
500
        return $this->types[$node->getNameValue()] ?? null;
501
    }
502
503
    /**
504
     * @param string $typeName
505
     * @return NamedTypeNodeInterface|null
506
     */
507
    protected function getTypeDefinition(string $typeName): ?NamedTypeNodeInterface
508
    {
509
        return $this->typeDefinitionsMap[$typeName] ?? null;
510
    }
511
512
    /**
513
     * @param TypeNodeInterface $typeNode
514
     * @return NamedTypeNodeInterface
515
     */
516
    protected function getNamedTypeNode(TypeNodeInterface $typeNode): NamedTypeNodeInterface
517
    {
518
        $namedType = $typeNode;
519
520
        while ($namedType instanceof ListTypeNode || $namedType instanceof NonNullTypeNode) {
521
            $namedType = $namedType->getType();
522
        }
523
524
        /** @var NamedTypeNodeInterface $namedType */
525
        return $namedType;
526
    }
527
528
    /**
529
     * @param NodeInterface|EnumValueDefinitionNode|FieldDefinitionNode $node
530
     * @return null|string
531
     * @throws InvariantException
532
     * @throws ExecutionException
533
     */
534
    protected function getDeprecationReason(NodeInterface $node): ?string
535
    {
536
        if (isset($this->directives['deprecated'])) {
537
            $deprecated = ValuesResolver::coerceDirectiveValues($this->directives['deprecated'], $node);
538
539
            return $deprecated['reason'] ?? null;
540
        }
541
542
        return null;
543
    }
544
}
545